tidy(ui): update canvas classes, organise location of konva nodes

This commit is contained in:
psychedelicious 2024-07-17 14:18:38 +10:00
parent 2ef8a8cf5a
commit 9483c8cc29
18 changed files with 675 additions and 598 deletions

View File

@ -33,16 +33,19 @@ export class CanvasBackground {
static BASE_NAME = 'background'; static BASE_NAME = 'background';
static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`;
layer: Konva.Layer; konva: {
layer: Konva.Layer;
};
manager: CanvasManager; manager: CanvasManager;
constructor(manager: CanvasManager) { constructor(manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }); this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) };
} }
render() { render() {
this.layer.zIndex(0); this.konva.layer.zIndex(0);
const scale = this.manager.stage.scaleX(); const scale = this.manager.stage.scaleX();
const gridSpacing = getGridSpacing(scale); const gridSpacing = getGridSpacing(scale);
const x = this.manager.stage.x(); const x = this.manager.stage.x();
@ -86,11 +89,11 @@ export class CanvasBackground {
let _x = 0; let _x = 0;
let _y = 0; let _y = 0;
this.layer.destroyChildren(); this.konva.layer.destroyChildren();
for (let i = 0; i < xSteps; i++) { for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing; _x = gridFullRect.x1 + i * gridSpacing;
this.layer.add( this.konva.layer.add(
new Konva.Line({ new Konva.Line({
x: _x, x: _x,
y: gridFullRect.y1, y: gridFullRect.y1,
@ -103,7 +106,7 @@ export class CanvasBackground {
} }
for (let i = 0; i < ySteps; i++) { for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing; _y = gridFullRect.y1 + i * gridSpacing;
this.layer.add( this.konva.layer.add(
new Konva.Line({ new Konva.Line({
x: gridFullRect.x1, x: gridFullRect.x1,
y: _y, y: _y,

View File

@ -11,11 +11,10 @@ export class CanvasBbox {
static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`;
static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`;
group: Konva.Group;
rect: Konva.Rect;
transformer: Konva.Transformer;
manager: CanvasManager; manager: CanvasManager;
konva: { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer };
ALL_ANCHORS: string[] = [ ALL_ANCHORS: string[] = [
'top-left', 'top-left',
'top-center', 'top-center',
@ -36,88 +35,89 @@ export class CanvasBbox {
const bbox = this.manager.stateApi.getBbox(); const bbox = this.manager.stateApi.getBbox();
const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height); const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height);
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully this.konva = {
// transparent rect for this purpose. group: new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }),
this.group = new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }); // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
this.rect = new Konva.Rect({ // transparent rect for this purpose.
name: CanvasBbox.RECT_NAME, rect: new Konva.Rect({
listening: false, name: CanvasBbox.RECT_NAME,
strokeEnabled: false, listening: false,
draggable: true, strokeEnabled: false,
...this.manager.stateApi.getBbox(), draggable: true,
}); ...this.manager.stateApi.getBbox(),
this.rect.on('dragmove', () => { }),
transformer: new Konva.Transformer({
name: CanvasBbox.TRANSFORMER_NAME,
borderDash: [5, 5],
borderStroke: 'rgba(212,216,234,1)',
borderEnabled: true,
rotateEnabled: false,
keepRatio: false,
ignoreStroke: true,
listening: false,
flipEnabled: false,
anchorFill: 'rgba(212,216,234,1)',
anchorStroke: 'rgb(42,42,42)',
anchorSize: 12,
anchorCornerRadius: 3,
shiftBehavior: 'none', // we will implement our own shift behavior
centeredScaling: false,
anchorStyleFunc: (anchor) => {
// Make the x/y resize anchors little bars
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
anchor.height(8);
anchor.offsetY(4);
anchor.width(30);
anchor.offsetX(15);
}
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
anchor.height(30);
anchor.offsetY(15);
anchor.width(8);
anchor.offsetX(4);
}
},
anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => {
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
// to konva's internal coordinate system.
const stage = this.konva.transformer.getStage();
assert(stage, 'Stage must exist');
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
const scaledGridSize = gridSize * stage.scaleX();
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
const stageAbsPos = stage.getAbsolutePosition();
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
const offsetX = stageAbsPos.x % scaledGridSize;
const offsetY = stageAbsPos.y % scaledGridSize;
// Finally, calculate the position by rounding to the grid and adding the offset.
return {
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
};
},
}),
};
this.konva.rect.on('dragmove', () => {
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
const bbox = this.manager.stateApi.getBbox(); const bbox = this.manager.stateApi.getBbox();
const bboxRect: Rect = { const bboxRect: Rect = {
...bbox.rect, ...bbox.rect,
x: roundToMultiple(this.rect.x(), gridSize), x: roundToMultiple(this.konva.rect.x(), gridSize),
y: roundToMultiple(this.rect.y(), gridSize), y: roundToMultiple(this.konva.rect.y(), gridSize),
}; };
this.rect.setAttrs(bboxRect); this.konva.rect.setAttrs(bboxRect);
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) { if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
this.manager.stateApi.onBboxTransformed(bboxRect); this.manager.stateApi.onBboxTransformed(bboxRect);
} }
}); });
this.transformer = new Konva.Transformer({ this.konva.transformer.on('transform', () => {
name: CanvasBbox.TRANSFORMER_NAME,
borderDash: [5, 5],
borderStroke: 'rgba(212,216,234,1)',
borderEnabled: true,
rotateEnabled: false,
keepRatio: false,
ignoreStroke: true,
listening: false,
flipEnabled: false,
anchorFill: 'rgba(212,216,234,1)',
anchorStroke: 'rgb(42,42,42)',
anchorSize: 12,
anchorCornerRadius: 3,
shiftBehavior: 'none', // we will implement our own shift behavior
centeredScaling: false,
anchorStyleFunc: (anchor) => {
// Make the x/y resize anchors little bars
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
anchor.height(8);
anchor.offsetY(4);
anchor.width(30);
anchor.offsetX(15);
}
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
anchor.height(30);
anchor.offsetY(15);
anchor.width(8);
anchor.offsetX(4);
}
},
anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => {
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
// to konva's internal coordinate system.
const stage = this.transformer.getStage();
assert(stage, 'Stage must exist');
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
const scaledGridSize = gridSize * stage.scaleX();
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
const stageAbsPos = stage.getAbsolutePosition();
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
const offsetX = stageAbsPos.x % scaledGridSize;
const offsetY = stageAbsPos.y % scaledGridSize;
// Finally, calculate the position by rounding to the grid and adding the offset.
return {
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
};
},
});
this.transformer.on('transform', () => {
// In the transform callback, we calculate the bbox's new dims and pos and update the konva object. // In the transform callback, we calculate the bbox's new dims and pos and update the konva object.
// Some special handling is needed depending on the anchor being dragged. // Some special handling is needed depending on the anchor being dragged.
const anchor = this.transformer.getActiveAnchor(); const anchor = this.konva.transformer.getActiveAnchor();
if (!anchor) { if (!anchor) {
// Pretty sure we should always have an anchor here? // Pretty sure we should always have an anchor here?
return; return;
@ -140,14 +140,14 @@ export class CanvasBbox {
} }
// The coords should be correct per the anchorDragBoundFunc. // The coords should be correct per the anchorDragBoundFunc.
let x = this.rect.x(); let x = this.konva.rect.x();
let y = this.rect.y(); let y = this.konva.rect.y();
// Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height
// *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap
// them to the grid. // them to the grid.
let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), gridSize);
let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); let height = roundToMultipleMin(this.konva.rect.height() * this.konva.rect.scaleY(), gridSize);
// If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this
// if alt/opt is held - this requires math too big for my brain. // if alt/opt is held - this requires math too big for my brain.
@ -187,7 +187,7 @@ export class CanvasBbox {
// Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
// TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly.
// Gotta be a way to avoid setting it twice... // Gotta be a way to avoid setting it twice...
this.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
// Update the bbox in internal state. // Update the bbox in internal state.
this.manager.stateApi.onBboxTransformed(bboxRect); this.manager.stateApi.onBboxTransformed(bboxRect);
@ -199,16 +199,16 @@ export class CanvasBbox {
} }
}); });
this.transformer.on('transformend', () => { this.konva.transformer.on('transformend', () => {
// Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held,
// we have the correct aspect ratio to start from. // we have the correct aspect ratio to start from.
$aspectRatioBuffer.set(this.rect.width() / this.rect.height()); $aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height());
}); });
// The transformer will always be transforming the dummy rect // The transformer will always be transforming the dummy rect
this.transformer.nodes([this.rect]); this.konva.transformer.nodes([this.konva.rect]);
this.group.add(this.rect); this.konva.group.add(this.konva.rect);
this.group.add(this.transformer); this.konva.group.add(this.konva.transformer);
} }
render() { render() {
@ -217,14 +217,14 @@ export class CanvasBbox {
const toolState = this.manager.stateApi.getToolState(); const toolState = this.manager.stateApi.getToolState();
if (!session.isActive) { if (!session.isActive) {
this.group.listening(false); this.konva.group.listening(false);
this.group.visible(false); this.konva.group.visible(false);
return; return;
} }
this.group.visible(true); this.konva.group.visible(true);
this.group.listening(toolState.selected === 'bbox'); this.konva.group.listening(toolState.selected === 'bbox');
this.rect.setAttrs({ this.konva.rect.setAttrs({
x: bbox.rect.x, x: bbox.rect.x,
y: bbox.rect.y, y: bbox.rect.y,
width: bbox.rect.width, width: bbox.rect.width,
@ -233,7 +233,7 @@ export class CanvasBbox {
scaleY: 1, scaleY: 1,
listening: toolState.selected === 'bbox', listening: toolState.selected === 'bbox',
}); });
this.transformer.setAttrs({ this.konva.transformer.setAttrs({
listening: toolState.selected === 'bbox', listening: toolState.selected === 'bbox',
enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS,
}); });

View File

@ -8,40 +8,44 @@ export class CanvasBrushLine {
static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`;
id: string; id: string;
konvaLineGroup: Konva.Group; konva: {
konvaLine: Konva.Line; group: Konva.Group;
line: Konva.Line;
};
lastBrushLine: BrushLine; lastBrushLine: BrushLine;
constructor(brushLine: BrushLine) { constructor(brushLine: BrushLine) {
const { id, strokeWidth, clip, color, points } = brushLine; const { id, strokeWidth, clip, color, points } = brushLine;
this.id = id; this.id = id;
this.konvaLineGroup = new Konva.Group({ this.konva = {
name: CanvasBrushLine.GROUP_NAME, group: new Konva.Group({
clip, name: CanvasBrushLine.GROUP_NAME,
listening: false, clip,
}); listening: false,
this.konvaLine = new Konva.Line({ }),
name: CanvasBrushLine.LINE_NAME, line: new Konva.Line({
id, name: CanvasBrushLine.LINE_NAME,
listening: false, id,
shadowForStrokeEnabled: false, listening: false,
strokeWidth, shadowForStrokeEnabled: false,
tension: 0, strokeWidth,
lineCap: 'round', tension: 0,
lineJoin: 'round', lineCap: 'round',
globalCompositeOperation: 'source-over', lineJoin: 'round',
stroke: rgbaColorToString(color), globalCompositeOperation: 'source-over',
// A line with only one point will not be rendered, so we duplicate the points to make it visible stroke: rgbaColorToString(color),
points: points.length === 2 ? [...points, ...points] : points, // A line with only one point will not be rendered, so we duplicate the points to make it visible
}); points: points.length === 2 ? [...points, ...points] : points,
this.konvaLineGroup.add(this.konvaLine); }),
};
this.konva.group.add(this.konva.line);
this.lastBrushLine = brushLine; this.lastBrushLine = brushLine;
} }
update(brushLine: BrushLine, force?: boolean): boolean { update(brushLine: BrushLine, force?: boolean): boolean {
if (this.lastBrushLine !== brushLine || force) { if (this.lastBrushLine !== brushLine || force) {
const { points, color, clip, strokeWidth } = brushLine; const { points, color, clip, strokeWidth } = brushLine;
this.konvaLine.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
points: points.length === 2 ? [...points, ...points] : points, points: points.length === 2 ? [...points, ...points] : points,
stroke: rgbaColorToString(color), stroke: rgbaColorToString(color),
@ -56,6 +60,6 @@ export class CanvasBrushLine {
} }
destroy() { destroy() {
this.konvaLineGroup.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -14,51 +14,60 @@ export class CanvasControlAdapter {
id: string; id: string;
manager: CanvasManager; manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group; konva: {
objectsGroup: Konva.Group; layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
};
image: CanvasImage | null; image: CanvasImage | null;
transformer: Konva.Transformer;
constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) {
const { id } = controlAdapterState; const { id } = controlAdapterState;
this.id = id; this.id = id;
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ this.konva = {
name: CanvasControlAdapter.LAYER_NAME, layer: new Konva.Layer({
imageSmoothingEnabled: false, name: CanvasControlAdapter.LAYER_NAME,
listening: false, imageSmoothingEnabled: false,
}); listening: false,
this.group = new Konva.Group({ }),
name: CanvasControlAdapter.GROUP_NAME, group: new Konva.Group({
listening: false, name: CanvasControlAdapter.GROUP_NAME,
}); listening: false,
this.objectsGroup = new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }); }),
this.group.add(this.objectsGroup); objectGroup: new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }),
this.layer.add(this.group); transformer: new Konva.Transformer({
name: CanvasControlAdapter.TRANSFORMER_NAME,
this.transformer = new Konva.Transformer({ shouldOverdrawWholeArea: true,
name: CanvasControlAdapter.TRANSFORMER_NAME, draggable: true,
shouldOverdrawWholeArea: true, dragDistance: 0,
draggable: true, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
dragDistance: 0, rotateEnabled: false,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], flipEnabled: false,
rotateEnabled: false, }),
flipEnabled: false, };
}); this.konva.transformer.on('transformend', () => {
this.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged( this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, {
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'control_adapter' 'control_adapter'
); );
}); });
this.transformer.on('dragend', () => { this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged( this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'control_adapter' 'control_adapter'
); );
}); });
this.layer.add(this.transformer); this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.layer.add(this.konva.transformer);
this.image = null; this.image = null;
this.controlAdapterState = controlAdapterState; this.controlAdapterState = controlAdapterState;
@ -68,7 +77,7 @@ export class CanvasControlAdapter {
this.controlAdapterState = controlAdapterState; this.controlAdapterState = controlAdapterState;
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.konva.group.setAttrs({
x: controlAdapterState.position.x, x: controlAdapterState.position.x,
y: controlAdapterState.position.y, y: controlAdapterState.position.y,
scaleX: 1, scaleX: 1,
@ -81,13 +90,13 @@ export class CanvasControlAdapter {
if (!imageObject) { if (!imageObject) {
if (this.image) { if (this.image) {
this.image.konvaImageGroup.visible(false); this.image.konva.group.visible(false);
didDraw = true; didDraw = true;
} }
} else if (!this.image) { } else if (!this.image) {
this.image = new CanvasImage(imageObject); this.image = new CanvasImage(imageObject);
this.updateGroup(true); this.updateGroup(true);
this.objectsGroup.add(this.image.konvaImageGroup); this.konva.objectGroup.add(this.image.konva.group);
await this.image.updateImageSource(imageObject.image.name); await this.image.updateImageSource(imageObject.image.name);
} else if (!this.image.isLoading && !this.image.isError) { } else if (!this.image.isLoading && !this.image.isError) {
if (await this.image.update(imageObject)) { if (await this.image.update(imageObject)) {
@ -99,18 +108,18 @@ export class CanvasControlAdapter {
} }
updateGroup(didDraw: boolean) { updateGroup(didDraw: boolean) {
this.layer.visible(this.controlAdapterState.isEnabled); this.konva.layer.visible(this.controlAdapterState.isEnabled);
this.group.opacity(this.controlAdapterState.opacity); this.konva.group.opacity(this.controlAdapterState.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;
if (!this.image?.konvaImage) { if (!this.image?.image) {
// 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.layer.listening(false); this.konva.layer.listening(false);
this.transformer.nodes([]); this.konva.transformer.nodes([]);
if (this.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.clearCache();
} }
return; return;
} }
@ -118,32 +127,32 @@ export class CanvasControlAdapter {
if (isSelected && selectedTool === 'move') { if (isSelected && 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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
// Activate the transformer // Activate the transformer
this.layer.listening(true); this.konva.layer.listening(true);
this.transformer.nodes([this.group]); this.konva.transformer.nodes([this.konva.group]);
this.transformer.forceUpdate(); this.konva.transformer.forceUpdate();
return; return;
} }
if (isSelected && selectedTool !== 'move') { if (isSelected && selectedTool !== 'move') {
// If the layer is selected but not using the move tool, we don't want the layer to be listening. // If the layer is selected but not using the move tool, we don't want the layer to be listening.
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.transformer.nodes([]); this.konva.transformer.nodes([]);
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.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
} }
return; return;
@ -151,12 +160,12 @@ export class CanvasControlAdapter {
if (!isSelected) { if (!isSelected) {
// Unselected layers should not be listening // Unselected layers should not be listening
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
return; return;
@ -164,6 +173,6 @@ export class CanvasControlAdapter {
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.konva.layer.destroy();
} }
} }

View File

@ -9,40 +9,44 @@ export class CanvasEraserLine {
static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`;
id: string; id: string;
konvaLineGroup: Konva.Group; konva: {
konvaLine: Konva.Line; group: Konva.Group;
line: Konva.Line;
};
lastEraserLine: EraserLine; lastEraserLine: EraserLine;
constructor(eraserLine: EraserLine) { constructor(eraserLine: EraserLine) {
const { id, strokeWidth, clip, points } = eraserLine; const { id, strokeWidth, clip, points } = eraserLine;
this.id = id; this.id = id;
this.konvaLineGroup = new Konva.Group({ this.konva = {
name: CanvasEraserLine.GROUP_NAME, group: new Konva.Group({
clip, name: CanvasEraserLine.GROUP_NAME,
listening: false, clip,
}); listening: false,
this.konvaLine = new Konva.Line({ }),
name: CanvasEraserLine.LINE_NAME, line: new Konva.Line({
id, name: CanvasEraserLine.LINE_NAME,
listening: false, id,
shadowForStrokeEnabled: false, listening: false,
strokeWidth, shadowForStrokeEnabled: false,
tension: 0, strokeWidth,
lineCap: 'round', tension: 0,
lineJoin: 'round', lineCap: 'round',
globalCompositeOperation: 'destination-out', lineJoin: 'round',
stroke: rgbaColorToString(RGBA_RED), globalCompositeOperation: 'destination-out',
// A line with only one point will not be rendered, so we duplicate the points to make it visible stroke: rgbaColorToString(RGBA_RED),
points: points.length === 2 ? [...points, ...points] : points, // A line with only one point will not be rendered, so we duplicate the points to make it visible
}); points: points.length === 2 ? [...points, ...points] : points,
this.konvaLineGroup.add(this.konvaLine); }),
};
this.konva.group.add(this.konva.line);
this.lastEraserLine = eraserLine; this.lastEraserLine = eraserLine;
} }
update(eraserLine: EraserLine, force?: boolean): boolean { update(eraserLine: EraserLine, force?: boolean): boolean {
if (this.lastEraserLine !== eraserLine || force) { if (this.lastEraserLine !== eraserLine || force) {
const { points, clip, strokeWidth } = eraserLine; const { points, clip, strokeWidth } = eraserLine;
this.konvaLine.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
points: points.length === 2 ? [...points, ...points] : points, points: points.length === 2 ? [...points, ...points] : points,
clip, clip,
@ -56,6 +60,6 @@ export class CanvasEraserLine {
} }
destroy() { destroy() {
this.konvaLineGroup.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -15,48 +15,51 @@ export class CanvasImage {
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`;
id: string; id: string;
konvaImageGroup: Konva.Group; konva: {
konvaPlaceholderGroup: Konva.Group; group: Konva.Group;
konvaPlaceholderRect: Konva.Rect; placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text };
konvaPlaceholderText: Konva.Text; };
imageName: string | null; imageName: string | null;
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
isLoading: boolean; isLoading: boolean;
isError: boolean; isError: boolean;
lastImageObject: ImageObject; lastImageObject: ImageObject;
constructor(imageObject: ImageObject) { constructor(imageObject: ImageObject) {
const { id, width, height, x, y } = imageObject; const { id, width, height, x, y } = imageObject;
this.konvaImageGroup = new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }); this.konva = {
this.konvaPlaceholderGroup = new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }); group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
this.konvaPlaceholderRect = new Konva.Rect({ placeholder: {
name: CanvasImage.PLACEHOLDER_RECT_NAME, group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }),
fill: 'hsl(220 12% 45% / 1)', // 'base.500' rect: new Konva.Rect({
width, name: CanvasImage.PLACEHOLDER_RECT_NAME,
height, fill: 'hsl(220 12% 45% / 1)', // 'base.500'
listening: false, width,
}); height,
this.konvaPlaceholderText = new Konva.Text({ listening: false,
name: CanvasImage.PLACEHOLDER_TEXT_NAME, }),
fill: 'hsl(220 12% 10% / 1)', // 'base.900' text: new Konva.Text({
width, name: CanvasImage.PLACEHOLDER_TEXT_NAME,
height, fill: 'hsl(220 12% 10% / 1)', // 'base.900'
align: 'center', width,
verticalAlign: 'middle', height,
fontFamily: '"Inter Variable", sans-serif', align: 'center',
fontSize: width / 16, verticalAlign: 'middle',
fontStyle: '600', fontFamily: '"Inter Variable", sans-serif',
text: t('common.loadingImage', 'Loading Image'), fontSize: width / 16,
listening: false, fontStyle: '600',
}); text: t('common.loadingImage', 'Loading Image'),
listening: false,
this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); }),
this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); },
this.konvaImageGroup.add(this.konvaPlaceholderGroup); };
this.konva.placeholder.group.add(this.konva.placeholder.rect);
this.konva.placeholder.group.add(this.konva.placeholder.text);
this.konva.group.add(this.konva.placeholder.group);
this.id = id; this.id = id;
this.imageName = null; this.imageName = null;
this.konvaImage = null; this.image = null;
this.isLoading = false; this.isLoading = false;
this.isError = false; this.isError = false;
this.lastImageObject = imageObject; this.lastImageObject = imageObject;
@ -65,51 +68,51 @@ export class CanvasImage {
async updateImageSource(imageName: string) { async updateImageSource(imageName: string) {
try { try {
this.isLoading = true; this.isLoading = true;
this.konvaImageGroup.visible(true); this.konva.group.visible(true);
if (!this.konvaImage) { if (!this.image) {
this.konvaPlaceholderGroup.visible(true); this.konva.placeholder.group.visible(true);
this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
} }
const imageDTO = await getImageDTO(imageName); const imageDTO = await getImageDTO(imageName);
assert(imageDTO !== null, 'imageDTO is null'); assert(imageDTO !== null, 'imageDTO is null');
const imageEl = await loadImage(imageDTO.image_url); const imageEl = await loadImage(imageDTO.image_url);
if (this.konvaImage) { if (this.image) {
this.konvaImage.setAttrs({ this.image.setAttrs({
image: imageEl, image: imageEl,
}); });
} else { } else {
this.konvaImage = new Konva.Image({ this.image = new Konva.Image({
name: CanvasImage.IMAGE_NAME, name: CanvasImage.IMAGE_NAME,
listening: false, listening: false,
image: imageEl, image: imageEl,
width: this.lastImageObject.width, width: this.lastImageObject.width,
height: this.lastImageObject.height, height: this.lastImageObject.height,
}); });
this.konvaImageGroup.add(this.konvaImage); this.konva.group.add(this.image);
} }
if (this.lastImageObject.filters.length > 0) { if (this.lastImageObject.filters.length > 0) {
this.konvaImage.cache(); this.image.cache();
this.konvaImage.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); this.image.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f]));
} else { } else {
this.konvaImage.clearCache(); this.image.clearCache();
this.konvaImage.filters([]); this.image.filters([]);
} }
this.imageName = imageName; this.imageName = imageName;
this.isLoading = false; this.isLoading = false;
this.isError = false; this.isError = false;
this.konvaPlaceholderGroup.visible(false); this.konva.placeholder.group.visible(false);
} catch { } catch {
this.konvaImage?.visible(false); this.image?.visible(false);
this.imageName = null; this.imageName = null;
this.isLoading = false; this.isLoading = false;
this.isError = true; this.isError = true;
this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
this.konvaPlaceholderGroup.visible(true); this.konva.placeholder.group.visible(true);
} }
} }
@ -119,16 +122,16 @@ export class CanvasImage {
if (this.lastImageObject.image.name !== image.name || force) { if (this.lastImageObject.image.name !== image.name || force) {
await this.updateImageSource(image.name); await this.updateImageSource(image.name);
} }
this.konvaImage?.setAttrs({ x, y, width, height }); this.image?.setAttrs({ x, y, width, height });
if (filters.length > 0) { if (filters.length > 0) {
this.konvaImage?.cache(); this.image?.cache();
this.konvaImage?.filters(filters.map((f) => FILTER_MAP[f])); this.image?.filters(filters.map((f) => FILTER_MAP[f]));
} else { } else {
this.konvaImage?.clearCache(); this.image?.clearCache();
this.konvaImage?.filters([]); this.image?.filters([]);
} }
this.konvaPlaceholderRect.setAttrs({ width, height }); this.konva.placeholder.rect.setAttrs({ width, height });
this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
this.lastImageObject = imageObject; this.lastImageObject = imageObject;
return true; return true;
} else { } else {
@ -137,6 +140,6 @@ export class CanvasImage {
} }
destroy() { destroy() {
this.konvaImageGroup.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -1,33 +1,37 @@
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import type { InitialImageEntity } from 'features/controlLayers/store/types'; import type { InitialImageEntity } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { v4 as uuidv4 } from 'uuid';
export class CanvasInitialImage { export class CanvasInitialImage {
static NAME_PREFIX = 'initial-image';
static LAYER_NAME = `${CanvasInitialImage.NAME_PREFIX}_layer`;
static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`;
static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`;
id = 'initial_image'; id = 'initial_image';
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
image: CanvasImage | null;
private initialImageState: InitialImageEntity; private initialImageState: InitialImageEntity;
manager: CanvasManager;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
};
image: CanvasImage | null;
constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { constructor(initialImageState: InitialImageEntity, manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ this.konva = {
id: this.id, layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }),
imageSmoothingEnabled: true, group: new Konva.Group({ name: CanvasInitialImage.GROUP_NAME, listening: false }),
listening: false, objectGroup: new Konva.Group({ name: CanvasInitialImage.OBJECT_GROUP_NAME, listening: false }),
}); };
this.group = new Konva.Group({ this.konva.group.add(this.konva.objectGroup);
id: getObjectGroupId(this.layer.id(), uuidv4()), this.konva.layer.add(this.konva.group);
listening: false,
});
this.objectsGroup = new Konva.Group({ listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.image = null; this.image = null;
this.initialImageState = initialImageState; this.initialImageState = initialImageState;
@ -37,26 +41,26 @@ export class CanvasInitialImage {
this.initialImageState = initialImageState; this.initialImageState = initialImageState;
if (!this.initialImageState.imageObject) { if (!this.initialImageState.imageObject) {
this.layer.visible(false); this.konva.layer.visible(false);
return; return;
} }
if (!this.image) { if (!this.image) {
this.image = new CanvasImage(this.initialImageState.imageObject); this.image = new CanvasImage(this.initialImageState.imageObject);
this.objectsGroup.add(this.image.konvaImageGroup); this.konva.objectGroup.add(this.image.konva.group);
await this.image.update(this.initialImageState.imageObject, true); await this.image.update(this.initialImageState.imageObject, true);
} else if (!this.image.isLoading && !this.image.isError) { } else if (!this.image.isLoading && !this.image.isError) {
await this.image.update(this.initialImageState.imageObject); await this.image.update(this.initialImageState.imageObject);
} }
if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) { if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) {
this.layer.visible(true); this.konva.layer.visible(true);
} else { } else {
this.layer.visible(false); this.konva.layer.visible(false);
} }
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.konva.layer.destroy();
} }
} }

View File

@ -23,57 +23,64 @@ export class CanvasInpaintMask {
id = 'inpaint_mask'; id = 'inpaint_mask';
manager: CanvasManager; manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group; konva: {
objectsGroup: Konva.Group; layer: Konva.Layer;
compositingRect: Konva.Rect; group: Konva.Group;
transformer: Konva.Transformer; objectGroup: Konva.Group;
transformer: Konva.Transformer;
compositingRect: Konva.Rect;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
constructor(entity: InpaintMaskEntity, manager: CanvasManager) { constructor(entity: InpaintMaskEntity, manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME });
this.group = new Konva.Group({ this.konva = {
name: CanvasInpaintMask.GROUP_NAME, layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }),
listening: false, group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }),
}); objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }),
this.objectsGroup = new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }); transformer: new Konva.Transformer({
this.group.add(this.objectsGroup); name: CanvasInpaintMask.TRANSFORMER_NAME,
this.layer.add(this.group); shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
compositingRect: new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }),
};
this.transformer = new Konva.Transformer({ this.konva.group.add(this.konva.objectGroup);
name: CanvasInpaintMask.TRANSFORMER_NAME, this.konva.layer.add(this.konva.group);
shouldOverdrawWholeArea: true,
draggable: true, this.konva.transformer.on('transformend', () => {
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged( this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, {
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'inpaint_mask' 'inpaint_mask'
); );
}); });
this.transformer.on('dragend', () => { this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged( this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'inpaint_mask' 'inpaint_mask'
); );
}); });
this.layer.add(this.transformer); this.konva.layer.add(this.konva.transformer);
this.compositingRect = new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }); this.konva.group.add(this.konva.compositingRect);
this.group.add(this.compositingRect);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null; this.drawingBuffer = null;
this.inpaintMaskState = entity; this.inpaintMaskState = entity;
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.konva.layer.destroy();
} }
getDrawingBuffer() { getDrawingBuffer() {
@ -112,7 +119,7 @@ export class CanvasInpaintMask {
this.inpaintMaskState = inpaintMaskState; this.inpaintMaskState = inpaintMaskState;
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.konva.group.setAttrs({
x: inpaintMaskState.position.x, x: inpaintMaskState.position.x,
y: inpaintMaskState.position.y, y: inpaintMaskState.position.y,
scaleX: 1, scaleX: 1,
@ -154,7 +161,7 @@ export class CanvasInpaintMask {
if (!brushLine) { if (!brushLine) {
brushLine = new CanvasBrushLine(obj); brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup); this.konva.objectGroup.add(brushLine.konva.group);
return true; return true;
} else { } else {
if (brushLine.update(obj, force)) { if (brushLine.update(obj, force)) {
@ -168,7 +175,7 @@ export class CanvasInpaintMask {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj); eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup); this.konva.objectGroup.add(eraserLine.konva.group);
return true; return true;
} else { } else {
if (eraserLine.update(obj, force)) { if (eraserLine.update(obj, force)) {
@ -182,7 +189,7 @@ export class CanvasInpaintMask {
if (!rect) { if (!rect) {
rect = new CanvasRect(obj); rect = new CanvasRect(obj);
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect); this.konva.objectGroup.add(rect.konva.group);
return true; return true;
} else { } else {
if (rect.update(obj, force)) { if (rect.update(obj, force)) {
@ -195,19 +202,19 @@ export class CanvasInpaintMask {
} }
updateGroup(didDraw: boolean) { updateGroup(didDraw: boolean) {
this.layer.visible(this.inpaintMaskState.isEnabled); this.konva.layer.visible(this.inpaintMaskState.isEnabled);
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
this.group.opacity(1); this.konva.group.opacity(1);
if (didDraw) { if (didDraw) {
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(this.inpaintMaskState.fill); const rgbColor = rgbColorToString(this.inpaintMaskState.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity(); const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.compositingRect.setAttrs({ this.konva.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.objectsGroup), ...getNodeBboxFast(this.konva.objectGroup),
fill: rgbColor, fill: rgbColor,
opacity: maskOpacity, opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -223,10 +230,10 @@ export class CanvasInpaintMask {
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.layer.listening(false); this.konva.layer.listening(false);
this.transformer.nodes([]); this.konva.transformer.nodes([]);
if (this.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.clearCache();
} }
return; return;
} }
@ -234,32 +241,32 @@ export class CanvasInpaintMask {
if (isSelected && selectedTool === 'move') { if (isSelected && 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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
// Activate the transformer // Activate the transformer
this.layer.listening(true); this.konva.layer.listening(true);
this.transformer.nodes([this.group]); this.konva.transformer.nodes([this.konva.group]);
this.transformer.forceUpdate(); this.konva.transformer.forceUpdate();
return; return;
} }
if (isSelected && selectedTool !== 'move') { if (isSelected && selectedTool !== 'move') {
// If the layer is selected but not using the move tool, we don't want the layer to be listening. // If the layer is selected but not using the move tool, we don't want the layer to be listening.
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.transformer.nodes([]); this.konva.transformer.nodes([]);
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.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
} }
return; return;
@ -267,12 +274,12 @@ export class CanvasInpaintMask {
if (!isSelected) { if (!isSelected) {
// Unselected layers should not be listening // Unselected layers should not be listening
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
return; return;

View File

@ -21,40 +21,53 @@ export class CanvasLayer {
id: string; id: string;
manager: CanvasManager; manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group; konva: {
objectsGroup: Konva.Group; layer: Konva.Layer;
transformer: Konva.Transformer; group: Konva.Group;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
constructor(entity: LayerEntity, manager: CanvasManager) { constructor(entity: LayerEntity, manager: CanvasManager) {
this.id = entity.id; this.id = entity.id;
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }); this.konva = {
this.group = new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }); layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }),
this.objectsGroup = new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }); group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }),
this.group.add(this.objectsGroup); objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
this.layer.add(this.group); transformer: new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
};
this.transformer = new Konva.Transformer({ this.konva.group.add(this.konva.objectGroup);
name: CanvasLayer.TRANSFORMER_NAME, this.konva.layer.add(this.konva.group);
shouldOverdrawWholeArea: true,
draggable: true, this.konva.transformer.on('transformend', () => {
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged( this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, {
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'layer' 'layer'
); );
}); });
this.transformer.on('dragend', () => { this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged({ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, 'layer'); this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'layer'
);
}); });
this.layer.add(this.transformer); this.konva.layer.add(this.konva.transformer);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null; this.drawingBuffer = null;
@ -62,7 +75,7 @@ export class CanvasLayer {
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.konva.layer.destroy();
} }
getDrawingBuffer() { getDrawingBuffer() {
@ -97,7 +110,7 @@ export class CanvasLayer {
this.layerState = layerState; this.layerState = layerState;
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.konva.group.setAttrs({
x: layerState.position.x, x: layerState.position.x,
y: layerState.position.y, y: layerState.position.y,
scaleX: 1, scaleX: 1,
@ -139,7 +152,7 @@ export class CanvasLayer {
if (!brushLine) { if (!brushLine) {
brushLine = new CanvasBrushLine(obj); brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup); this.konva.objectGroup.add(brushLine.konva.group);
return true; return true;
} else { } else {
if (brushLine.update(obj, force)) { if (brushLine.update(obj, force)) {
@ -153,7 +166,7 @@ export class CanvasLayer {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj); eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup); this.konva.objectGroup.add(eraserLine.konva.group);
return true; return true;
} else { } else {
if (eraserLine.update(obj, force)) { if (eraserLine.update(obj, force)) {
@ -167,7 +180,7 @@ export class CanvasLayer {
if (!rect) { if (!rect) {
rect = new CanvasRect(obj); rect = new CanvasRect(obj);
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect); this.konva.objectGroup.add(rect.konva.group);
return true; return true;
} else { } else {
if (rect.update(obj, force)) { if (rect.update(obj, force)) {
@ -181,7 +194,7 @@ export class CanvasLayer {
if (!image) { if (!image) {
image = new CanvasImage(obj); image = new CanvasImage(obj);
this.objects.set(image.id, image); this.objects.set(image.id, image);
this.objectsGroup.add(image.konvaImageGroup); this.konva.objectGroup.add(image.konva.group);
await image.updateImageSource(obj.image.name); await image.updateImageSource(obj.image.name);
return true; return true;
} else { } else {
@ -196,58 +209,58 @@ export class CanvasLayer {
updateGroup(didDraw: boolean) { updateGroup(didDraw: boolean) {
if (!this.layerState.isEnabled) { if (!this.layerState.isEnabled) {
this.layer.visible(false); this.konva.layer.visible(false);
return; return;
} }
this.layer.visible(true); this.konva.layer.visible(true);
this.group.opacity(this.layerState.opacity); this.konva.group.opacity(this.layerState.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;
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.layer.listening(false); this.konva.layer.listening(false);
this.transformer.nodes([]); this.konva.transformer.nodes([]);
if (this.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.clearCache();
} }
} else if (isSelected && selectedTool === 'move') { } else if (isSelected && 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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
// Activate the transformer // Activate the transformer
this.layer.listening(true); this.konva.layer.listening(true);
this.transformer.nodes([this.group]); this.konva.transformer.nodes([this.konva.group]);
this.transformer.forceUpdate(); this.konva.transformer.forceUpdate();
} else if (isSelected && selectedTool !== 'move') { } else if (isSelected && selectedTool !== 'move') {
// If the layer is selected but not using the move tool, we don't want the layer to be listening. // If the layer is selected but not using the move tool, we don't want the layer to be listening.
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.transformer.nodes([]); this.konva.transformer.nodes([]);
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.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
} }
} else if (!isSelected) { } else if (!isSelected) {
// Unselected layers should not be listening // Unselected layers should not be listening
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
} }
} }

View File

@ -97,17 +97,17 @@ export class CanvasManager {
this.stage.add(this.preview.layer); this.stage.add(this.preview.layer);
this.background = new CanvasBackground(this); this.background = new CanvasBackground(this);
this.stage.add(this.background.layer); this.stage.add(this.background.konva.layer);
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
this.stage.add(this.inpaintMask.layer); this.stage.add(this.inpaintMask.konva.layer);
this.layers = new Map(); this.layers = new Map();
this.regions = new Map(); this.regions = new Map();
this.controlAdapters = new Map(); this.controlAdapters = new Map();
this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
this.stage.add(this.initialImage.layer); this.stage.add(this.initialImage.konva.layer);
} }
async renderInitialImage() { async renderInitialImage() {
@ -129,7 +129,7 @@ export class CanvasManager {
if (!adapter) { if (!adapter) {
adapter = new CanvasLayer(entity, this); adapter = new CanvasLayer(entity, this);
this.layers.set(adapter.id, adapter); this.layers.set(adapter.id, adapter);
this.stage.add(adapter.layer); this.stage.add(adapter.konva.layer);
} }
await adapter.render(entity); await adapter.render(entity);
} }
@ -151,7 +151,7 @@ export class CanvasManager {
if (!adapter) { if (!adapter) {
adapter = new CanvasRegion(entity, this); adapter = new CanvasRegion(entity, this);
this.regions.set(adapter.id, adapter); this.regions.set(adapter.id, adapter);
this.stage.add(adapter.layer); this.stage.add(adapter.konva.layer);
} }
await adapter.render(entity); await adapter.render(entity);
} }
@ -181,7 +181,7 @@ export class CanvasManager {
if (!adapter) { if (!adapter) {
adapter = new CanvasControlAdapter(entity, this); adapter = new CanvasControlAdapter(entity, this);
this.controlAdapters.set(adapter.id, adapter); this.controlAdapters.set(adapter.id, adapter);
this.stage.add(adapter.layer); this.stage.add(adapter.konva.layer);
} }
await adapter.render(entity); await adapter.render(entity);
} }
@ -193,18 +193,18 @@ export class CanvasManager {
const controlAdapters = getControlAdaptersState().entities; const controlAdapters = getControlAdaptersState().entities;
const regions = getRegionsState().entities; const regions = getRegionsState().entities;
let zIndex = 0; let zIndex = 0;
this.background.layer.zIndex(++zIndex); this.background.konva.layer.zIndex(++zIndex);
this.initialImage.layer.zIndex(++zIndex); this.initialImage.konva.layer.zIndex(++zIndex);
for (const layer of layers) { for (const layer of layers) {
this.layers.get(layer.id)?.layer.zIndex(++zIndex); this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex);
} }
for (const ca of controlAdapters) { for (const ca of controlAdapters) {
this.controlAdapters.get(ca.id)?.layer.zIndex(++zIndex); this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex);
} }
for (const rg of regions) { for (const rg of regions) {
this.regions.get(rg.id)?.layer.zIndex(++zIndex); this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex);
} }
this.inpaintMask.layer.zIndex(++zIndex); this.inpaintMask.konva.layer.zIndex(++zIndex);
this.preview.layer.zIndex(++zIndex); this.preview.layer.zIndex(++zIndex);
} }

View File

@ -21,15 +21,15 @@ export class CanvasPreview {
this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false });
this.stagingArea = stagingArea; this.stagingArea = stagingArea;
this.layer.add(this.stagingArea.group); this.layer.add(this.stagingArea.konva.group);
this.bbox = bbox; this.bbox = bbox;
this.layer.add(this.bbox.group); this.layer.add(this.bbox.konva.group);
this.tool = tool; this.tool = tool;
this.layer.add(this.tool.group); this.layer.add(this.tool.konva.group);
this.progressPreview = progressPreview; this.progressPreview = progressPreview;
this.layer.add(this.progressPreview.group); this.layer.add(this.progressPreview.konva.group);
} }
} }

View File

@ -8,17 +8,21 @@ export class CanvasProgressImage {
id: string; id: string;
progressImageId: string | null; progressImageId: string | null;
konvaImageGroup: Konva.Group; konva: {
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately group: Konva.Group;
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
};
isLoading: boolean; isLoading: boolean;
isError: boolean; isError: boolean;
constructor(arg: { id: string }) { constructor(arg: { id: string }) {
const { id } = arg; const { id } = arg;
this.konvaImageGroup = new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }); this.konva = {
group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }),
image: null,
};
this.id = id; this.id = id;
this.progressImageId = null; this.progressImageId = null;
this.konvaImage = null;
this.isLoading = false; this.isLoading = false;
this.isError = false; this.isError = false;
} }
@ -37,8 +41,8 @@ export class CanvasProgressImage {
this.isLoading = true; this.isLoading = true;
try { try {
const imageEl = await loadImage(dataURL); const imageEl = await loadImage(dataURL);
if (this.konvaImage) { if (this.konva.image) {
this.konvaImage.setAttrs({ this.konva.image.setAttrs({
image: imageEl, image: imageEl,
x, x,
y, y,
@ -46,7 +50,7 @@ export class CanvasProgressImage {
height, height,
}); });
} else { } else {
this.konvaImage = new Konva.Image({ this.konva.image = new Konva.Image({
name: CanvasProgressImage.IMAGE_NAME, name: CanvasProgressImage.IMAGE_NAME,
listening: false, listening: false,
image: imageEl, image: imageEl,
@ -55,7 +59,7 @@ export class CanvasProgressImage {
width, width,
height, height,
}); });
this.konvaImageGroup.add(this.konvaImage); this.konva.group.add(this.konva.image);
} }
this.isLoading = false; this.isLoading = false;
this.id = progressImageId; this.id = progressImageId;
@ -65,6 +69,6 @@ export class CanvasProgressImage {
} }
destroy() { destroy() {
this.konvaImageGroup.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -7,15 +7,19 @@ export class CanvasProgressPreview {
static NAME_PREFIX = 'progress-preview'; static NAME_PREFIX = 'progress-preview';
static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`;
group: Konva.Group; konva: {
progressImage: CanvasProgressImage; group: Konva.Group;
progressImage: CanvasProgressImage;
};
manager: CanvasManager; manager: CanvasManager;
constructor(manager: CanvasManager) { constructor(manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.group = new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }); this.konva = {
this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }),
this.group.add(this.progressImage.konvaImageGroup); progressImage: new CanvasProgressImage({ id: 'progress-image' }),
};
this.konva.group.add(this.konva.progressImage.konva.group);
} }
async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) {
@ -28,15 +32,15 @@ export class CanvasProgressPreview {
const { x, y, width, height } = bboxRect; const { x, y, width, height } = bboxRect;
const progressImageId = `${invocation.id}_${step}`; const progressImageId = `${invocation.id}_${step}`;
if ( if (
!this.progressImage.isLoading && !this.konva.progressImage.isLoading &&
!this.progressImage.isError && !this.konva.progressImage.isError &&
this.progressImage.progressImageId !== progressImageId this.konva.progressImage.progressImageId !== progressImageId
) { ) {
await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height);
this.progressImage.konvaImageGroup.visible(true); this.konva.progressImage.konva.group.visible(true);
} }
} else { } else {
this.progressImage.konvaImageGroup.visible(false); this.konva.progressImage.konva.group.visible(false);
} }
} }
} }

View File

@ -4,33 +4,40 @@ import Konva from 'konva';
export class CanvasRect { export class CanvasRect {
static NAME_PREFIX = 'canvas-rect'; static NAME_PREFIX = 'canvas-rect';
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
id: string; id: string;
konvaRect: Konva.Rect; konva: {
group: Konva.Group;
rect: Konva.Rect;
};
lastRectShape: RectShape; lastRectShape: RectShape;
constructor(rectShape: RectShape) { constructor(rectShape: RectShape) {
const { id, x, y, width, height } = rectShape; const { id, x, y, width, height } = rectShape;
this.id = id; this.id = id;
const konvaRect = new Konva.Rect({ this.konva = {
name: CanvasRect.RECT_NAME, group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
id, rect: new Konva.Rect({
x, name: CanvasRect.RECT_NAME,
y, id,
width, x,
height, y,
listening: false, width,
fill: rgbaColorToString(rectShape.color), height,
}); listening: false,
this.konvaRect = konvaRect; fill: rgbaColorToString(rectShape.color),
}),
};
this.konva.group.add(this.konva.rect);
this.lastRectShape = rectShape; this.lastRectShape = rectShape;
} }
update(rectShape: RectShape, force?: boolean): boolean { update(rectShape: RectShape, force?: boolean): boolean {
if (this.lastRectShape !== rectShape || force) { if (this.lastRectShape !== rectShape || force) {
const { x, y, width, height, color } = rectShape; const { x, y, width, height, color } = rectShape;
this.konvaRect.setAttrs({ this.konva.rect.setAttrs({
x, x,
y, y,
width, width,
@ -45,6 +52,6 @@ export class CanvasRect {
} }
destroy() { destroy() {
this.konvaRect.destroy(); this.konva.group.destroy();
} }
} }

View File

@ -23,54 +23,63 @@ export class CanvasRegion {
id: string; id: string;
manager: CanvasManager; manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group; konva: {
objectsGroup: Konva.Group; layer: Konva.Layer;
compositingRect: Konva.Rect; group: Konva.Group;
transformer: Konva.Transformer; objectGroup: Konva.Group;
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
constructor(entity: RegionEntity, manager: CanvasManager) { constructor(entity: RegionEntity, manager: CanvasManager) {
this.id = entity.id; this.id = entity.id;
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false });
this.group = new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false });
this.objectsGroup = new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.transformer = new Konva.Transformer({ this.konva = {
name: CanvasRegion.TRANSFORMER_NAME, layer: new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }),
shouldOverdrawWholeArea: true, group: new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }),
draggable: true, objectGroup: new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }),
dragDistance: 0, transformer: new Konva.Transformer({
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], name: CanvasRegion.TRANSFORMER_NAME,
rotateEnabled: false, shouldOverdrawWholeArea: true,
flipEnabled: false, draggable: true,
}); dragDistance: 0,
this.transformer.on('transformend', () => { enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
compositingRect: new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }),
};
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged( this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, {
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'regional_guidance' 'regional_guidance'
); );
}); });
this.transformer.on('dragend', () => { this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged( this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'regional_guidance' 'regional_guidance'
); );
}); });
this.layer.add(this.transformer); this.konva.layer.add(this.konva.transformer);
this.konva.group.add(this.konva.compositingRect);
this.compositingRect = new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false });
this.group.add(this.compositingRect);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null; this.drawingBuffer = null;
this.regionState = entity; this.regionState = entity;
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.konva.layer.destroy();
} }
getDrawingBuffer() { getDrawingBuffer() {
@ -109,7 +118,7 @@ export class CanvasRegion {
this.regionState = regionState; this.regionState = regionState;
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.konva.group.setAttrs({
x: regionState.position.x, x: regionState.position.x,
y: regionState.position.y, y: regionState.position.y,
scaleX: 1, scaleX: 1,
@ -151,7 +160,7 @@ export class CanvasRegion {
if (!brushLine) { if (!brushLine) {
brushLine = new CanvasBrushLine(obj); brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup); this.konva.objectGroup.add(brushLine.konva.group);
return true; return true;
} else { } else {
if (brushLine.update(obj, force)) { if (brushLine.update(obj, force)) {
@ -165,7 +174,7 @@ export class CanvasRegion {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj); eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup); this.konva.objectGroup.add(eraserLine.konva.group);
return true; return true;
} else { } else {
if (eraserLine.update(obj, force)) { if (eraserLine.update(obj, force)) {
@ -179,7 +188,7 @@ export class CanvasRegion {
if (!rect) { if (!rect) {
rect = new CanvasRect(obj); rect = new CanvasRect(obj);
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect); this.konva.objectGroup.add(rect.konva.group);
return true; return true;
} else { } else {
if (rect.update(obj, force)) { if (rect.update(obj, force)) {
@ -192,18 +201,18 @@ export class CanvasRegion {
} }
updateGroup(didDraw: boolean) { updateGroup(didDraw: boolean) {
this.layer.visible(this.regionState.isEnabled); this.konva.layer.visible(this.regionState.isEnabled);
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
this.group.opacity(1); this.konva.group.opacity(1);
if (didDraw) { if (didDraw) {
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(this.regionState.fill); const rgbColor = rgbColorToString(this.regionState.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity(); const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.compositingRect.setAttrs({ this.konva.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.objectsGroup), ...getNodeBboxFast(this.konva.objectGroup),
fill: rgbColor, fill: rgbColor,
opacity: maskOpacity, opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -218,10 +227,10 @@ export class CanvasRegion {
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.layer.listening(false); this.konva.layer.listening(false);
this.transformer.nodes([]); this.konva.transformer.nodes([]);
if (this.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.clearCache();
} }
return; return;
} }
@ -229,32 +238,32 @@ export class CanvasRegion {
if (isSelected && selectedTool === 'move') { if (isSelected && 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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
// Activate the transformer // Activate the transformer
this.layer.listening(true); this.konva.layer.listening(true);
this.transformer.nodes([this.group]); this.konva.transformer.nodes([this.konva.group]);
this.transformer.forceUpdate(); this.konva.transformer.forceUpdate();
return; return;
} }
if (isSelected && selectedTool !== 'move') { if (isSelected && selectedTool !== 'move') {
// If the layer is selected but not using the move tool, we don't want the layer to be listening. // If the layer is selected but not using the move tool, we don't want the layer to be listening.
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.transformer.nodes([]); this.konva.transformer.nodes([]);
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.group.isCached()) { if (this.konva.group.isCached()) {
this.group.clearCache(); this.konva.group.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
} }
return; return;
@ -262,12 +271,12 @@ export class CanvasRegion {
if (!isSelected) { if (!isSelected) {
// Unselected layers should not be listening // Unselected layers should not be listening
this.layer.listening(false); this.konva.layer.listening(false);
// The transformer also does not need to be active. // The transformer also does not need to be active.
this.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.group.isCached() || didDraw) { if (!this.konva.group.isCached() || didDraw) {
this.group.cache(); this.konva.group.cache();
} }
return; return;

View File

@ -4,14 +4,18 @@ import type { StagingAreaImage } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
export class CanvasStagingArea { export class CanvasStagingArea {
group: Konva.Group; static NAME_PREFIX = 'staging-area';
static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`;
konva: { group: Konva.Group };
image: CanvasImage | null; image: CanvasImage | null;
selectedImage: StagingAreaImage | null; selectedImage: StagingAreaImage | null;
manager: CanvasManager; manager: CanvasManager;
constructor(manager: CanvasManager) { constructor(manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.group = new Konva.Group({ listening: false }); this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) };
this.image = null; this.image = null;
this.selectedImage = null; this.selectedImage = null;
} }
@ -42,20 +46,20 @@ export class CanvasStagingArea {
height, height,
}, },
}); });
this.group.add(this.image.konvaImageGroup); this.konva.group.add(this.image.konva.group);
} }
if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
this.image.konvaImage?.width(imageDTO.width); this.image.image?.width(imageDTO.width);
this.image.konvaImage?.height(imageDTO.height); this.image.image?.height(imageDTO.height);
this.image.konvaImageGroup.x(bboxRect.x + offsetX); this.image.konva.group.x(bboxRect.x + offsetX);
this.image.konvaImageGroup.y(bboxRect.y + offsetY); this.image.konva.group.y(bboxRect.y + offsetY);
await this.image.updateImageSource(imageDTO.image_name); await this.image.updateImageSource(imageDTO.image_name);
this.manager.stateApi.resetLastProgressEvent(); this.manager.stateApi.resetLastProgressEvent();
} }
this.image.konvaImageGroup.visible(shouldShowStagedImage); this.image.konva.group.visible(shouldShowStagedImage);
} else { } else {
this.image?.konvaImageGroup.visible(false); this.image?.konva.group.visible(false);
} }
} }
} }

View File

@ -25,80 +25,82 @@ export class CanvasTool {
static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`;
manager: CanvasManager; manager: CanvasManager;
group: Konva.Group; konva: {
brush: {
group: Konva.Group; group: Konva.Group;
fillCircle: Konva.Circle; brush: {
innerBorderCircle: Konva.Circle; group: Konva.Group;
outerBorderCircle: Konva.Circle; fillCircle: Konva.Circle;
}; innerBorderCircle: Konva.Circle;
eraser: { outerBorderCircle: Konva.Circle;
group: Konva.Group; };
fillCircle: Konva.Circle; eraser: {
innerBorderCircle: Konva.Circle; group: Konva.Group;
outerBorderCircle: Konva.Circle; fillCircle: Konva.Circle;
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
};
}; };
constructor(manager: CanvasManager) { constructor(manager: CanvasManager) {
this.manager = manager; this.manager = manager;
this.group = new Konva.Group({ name: CanvasTool.GROUP_NAME }); this.konva = {
group: new Konva.Group({ name: CanvasTool.GROUP_NAME }),
// Create the brush preview group & circles brush: {
this.brush = { group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }),
group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), fillCircle: new Konva.Circle({
fillCircle: new Konva.Circle({ name: CanvasTool.BRUSH_FILL_CIRCLE_NAME,
name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, listening: false,
listening: false, strokeEnabled: false,
strokeEnabled: false, }),
}), innerBorderCircle: new Konva.Circle({
innerBorderCircle: new Konva.Circle({ name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME,
name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, listening: false,
listening: false, stroke: BRUSH_BORDER_INNER_COLOR,
stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true,
strokeEnabled: true, }),
}), outerBorderCircle: new Konva.Circle({
outerBorderCircle: new Konva.Circle({ name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME,
name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, listening: false,
listening: false, stroke: BRUSH_BORDER_OUTER_COLOR,
stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true,
strokeEnabled: true, }),
}), },
eraser: {
group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }),
fillCircle: new Konva.Circle({
name: CanvasTool.ERASER_FILL_CIRCLE_NAME,
listening: false,
strokeEnabled: false,
fill: 'white',
globalCompositeOperation: 'destination-out',
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
},
}; };
this.brush.group.add(this.brush.fillCircle); this.konva.brush.group.add(this.konva.brush.fillCircle);
this.brush.group.add(this.brush.innerBorderCircle); this.konva.brush.group.add(this.konva.brush.innerBorderCircle);
this.brush.group.add(this.brush.outerBorderCircle); this.konva.brush.group.add(this.konva.brush.outerBorderCircle);
this.group.add(this.brush.group); this.konva.group.add(this.konva.brush.group);
this.eraser = { this.konva.eraser.group.add(this.konva.eraser.fillCircle);
group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle);
fillCircle: new Konva.Circle({ this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
name: CanvasTool.ERASER_FILL_CIRCLE_NAME, this.konva.group.add(this.konva.eraser.group);
listening: false,
strokeEnabled: false,
fill: 'white',
globalCompositeOperation: 'destination-out',
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
};
this.eraser.group.add(this.eraser.fillCircle);
this.eraser.group.add(this.eraser.innerBorderCircle);
this.eraser.group.add(this.eraser.outerBorderCircle);
this.group.add(this.eraser.group);
// // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position // // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
// this.rect = { // this.rect = {
@ -110,7 +112,7 @@ export class CanvasTool {
// }), // }),
// }; // };
// this.rect.group.add(this.rect.fillRect); // this.rect.group.add(this.rect.fillRect);
// this.group.add(this.rect.group); // this.konva.group.add(this.rect.group);
} }
scaleTool = () => { scaleTool = () => {
@ -118,15 +120,15 @@ export class CanvasTool {
const scale = this.manager.stage.scaleX(); const scale = this.manager.stage.scaleX();
const brushRadius = toolState.brush.width / 2; const brushRadius = toolState.brush.width / 2;
this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); this.konva.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.brush.outerBorderCircle.setAttrs({ this.konva.brush.outerBorderCircle.setAttrs({
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
}); });
const eraserRadius = toolState.eraser.width / 2; const eraserRadius = toolState.eraser.width / 2;
this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); this.konva.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.eraser.outerBorderCircle.setAttrs({ this.konva.eraser.outerBorderCircle.setAttrs({
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
}); });
@ -175,16 +177,16 @@ export class CanvasTool {
if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) {
// We can bail early if the mouse isn't over the stage or there are no layers // We can bail early if the mouse isn't over the stage or there are no layers
this.group.visible(false); this.konva.group.visible(false);
} else { } else {
this.group.visible(true); this.konva.group.visible(true);
// No need to render the brush preview if the cursor position or color is missing // No need to render the brush preview if the cursor position or color is missing
if (cursorPos && tool === 'brush') { if (cursorPos && tool === 'brush') {
const scale = stage.scaleX(); const scale = stage.scaleX();
// Update the fill circle // Update the fill circle
const radius = toolState.brush.width / 2; const radius = toolState.brush.width / 2;
this.brush.fillCircle.setAttrs({ this.konva.brush.fillCircle.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius, radius,
@ -192,10 +194,10 @@ export class CanvasTool {
}); });
// Update the inner border of the brush preview // Update the inner border of the brush preview
this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the brush preview // Update the outer border of the brush preview
this.brush.outerBorderCircle.setAttrs({ this.konva.brush.outerBorderCircle.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
@ -203,14 +205,14 @@ export class CanvasTool {
this.scaleTool(); this.scaleTool();
this.brush.group.visible(true); this.konva.brush.group.visible(true);
this.eraser.group.visible(false); this.konva.eraser.group.visible(false);
// this.rect.group.visible(false); // this.rect.group.visible(false);
} else if (cursorPos && tool === 'eraser') { } else if (cursorPos && tool === 'eraser') {
const scale = stage.scaleX(); const scale = stage.scaleX();
// Update the fill circle // Update the fill circle
const radius = toolState.eraser.width / 2; const radius = toolState.eraser.width / 2;
this.eraser.fillCircle.setAttrs({ this.konva.eraser.fillCircle.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius, radius,
@ -218,10 +220,10 @@ export class CanvasTool {
}); });
// Update the inner border of the eraser preview // Update the inner border of the eraser preview
this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the eraser preview // Update the outer border of the eraser preview
this.eraser.outerBorderCircle.setAttrs({ this.konva.eraser.outerBorderCircle.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
@ -229,8 +231,8 @@ export class CanvasTool {
this.scaleTool(); this.scaleTool();
this.brush.group.visible(false); this.konva.brush.group.visible(false);
this.eraser.group.visible(true); this.konva.eraser.group.visible(true);
// this.rect.group.visible(false); // this.rect.group.visible(false);
// } else if (cursorPos && lastMouseDownPos && tool === 'rect') { // } else if (cursorPos && lastMouseDownPos && tool === 'rect') {
// this.rect.fillRect.setAttrs({ // this.rect.fillRect.setAttrs({
@ -241,12 +243,12 @@ export class CanvasTool {
// fill: rgbaColorToString(currentFill), // fill: rgbaColorToString(currentFill),
// visible: true, // visible: true,
// }); // });
// this.brush.group.visible(false); // this.konva.brush.group.visible(false);
// this.eraser.group.visible(false); // this.konva.eraser.group.visible(false);
// this.rect.group.visible(true); // this.rect.group.visible(true);
} else { } else {
this.brush.group.visible(false); this.konva.brush.group.visible(false);
this.eraser.group.visible(false); this.konva.eraser.group.visible(false);
// this.rect.group.visible(false); // this.rect.group.visible(false);
} }
} }

View File

@ -269,8 +269,8 @@ export const previewBlob = async (blob: Blob, label?: string) => {
export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer {
const { manager } = arg; const { manager } = arg;
const layerClone = manager.inpaintMask.layer.clone(); const layerClone = manager.inpaintMask.konva.layer.clone();
const objectGroupClone = manager.inpaintMask.group.clone(); const objectGroupClone = manager.inpaintMask.konva.group.clone();
layerClone.destroyChildren(); layerClone.destroyChildren();
layerClone.add(objectGroupClone); layerClone.add(objectGroupClone);
@ -287,8 +287,8 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin
const canvasRegion = manager.regions.get(id); const canvasRegion = manager.regions.get(id);
assert(canvasRegion, `Canvas region with id ${id} not found`); assert(canvasRegion, `Canvas region with id ${id} not found`);
const layerClone = canvasRegion.layer.clone(); const layerClone = canvasRegion.konva.layer.clone();
const objectGroupClone = canvasRegion.group.clone(); const objectGroupClone = canvasRegion.konva.group.clone();
layerClone.destroyChildren(); layerClone.destroyChildren();
layerClone.add(objectGroupClone); layerClone.add(objectGroupClone);
@ -305,8 +305,8 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s
const controlAdapter = manager.controlAdapters.get(id); const controlAdapter = manager.controlAdapters.get(id);
assert(controlAdapter, `Canvas region with id ${id} not found`); assert(controlAdapter, `Canvas region with id ${id} not found`);
const controlAdapterClone = controlAdapter.layer.clone(); const controlAdapterClone = controlAdapter.konva.layer.clone();
const objectGroupClone = controlAdapter.group.clone(); const objectGroupClone = controlAdapter.konva.group.clone();
controlAdapterClone.destroyChildren(); controlAdapterClone.destroyChildren();
controlAdapterClone.add(objectGroupClone); controlAdapterClone.add(objectGroupClone);
@ -322,8 +322,8 @@ export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konv
const initialImage = manager.initialImage; const initialImage = manager.initialImage;
const initialImageClone = initialImage.layer.clone(); const initialImageClone = initialImage.konva.layer.clone();
const objectGroupClone = initialImage.group.clone(); const objectGroupClone = initialImage.konva.group.clone();
initialImageClone.destroyChildren(); initialImageClone.destroyChildren();
initialImageClone.add(objectGroupClone); initialImageClone.add(objectGroupClone);