mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): inpaint mask rendering
This commit is contained in:
parent
55d7f0ff5b
commit
2998287f61
@ -150,7 +150,7 @@ export class KonvaNodeManager {
|
|||||||
this.background = new CanvasBackground();
|
this.background = new CanvasBackground();
|
||||||
this.stage.add(this.background.layer);
|
this.stage.add(this.background.layer);
|
||||||
|
|
||||||
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this.stateApi.onPosChanged);
|
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
|
||||||
this.stage.add(this.inpaintMask.layer);
|
this.stage.add(this.inpaintMask.layer);
|
||||||
|
|
||||||
this.layers = new Map();
|
this.layers = new Map();
|
||||||
@ -206,11 +206,7 @@ export class KonvaNodeManager {
|
|||||||
|
|
||||||
renderInpaintMask() {
|
renderInpaintMask() {
|
||||||
const inpaintMaskState = this.stateApi.getInpaintMaskState();
|
const inpaintMaskState = this.stateApi.getInpaintMaskState();
|
||||||
const toolState = this.stateApi.getToolState();
|
this.inpaintMask.render(inpaintMaskState);
|
||||||
const selectedEntity = this.stateApi.getSelectedEntity();
|
|
||||||
const maskOpacity = this.stateApi.getMaskOpacity();
|
|
||||||
|
|
||||||
this.inpaintMask.render(inpaintMaskState, toolState.selected, selectedEntity, maskOpacity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderControlAdapters() {
|
renderControlAdapters() {
|
||||||
@ -250,7 +246,7 @@ export class KonvaNodeManager {
|
|||||||
for (const rg of regions) {
|
for (const rg of regions) {
|
||||||
this.regions.get(rg.id)?.layer.zIndex(++zIndex);
|
this.regions.get(rg.id)?.layer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
this.inpaintMask?.layer.zIndex(++zIndex);
|
this.inpaintMask.layer.zIndex(++zIndex);
|
||||||
this.preview.layer.zIndex(++zIndex);
|
this.preview.layer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,13 +165,13 @@ const getLayerBboxPixels = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It
|
* Get the bounding box of a konva node. This function is faster than `getLayerBboxPixels` but less accurate. It
|
||||||
* should only be used when there are no eraser strokes or shapes in the layer.
|
* should only be used when there are no eraser strokes or shapes in the node.
|
||||||
* @param layer The konva layer to get the bounding box of.
|
* @param node The konva node to get the bounding box of.
|
||||||
* @returns The bounding box of the layer.
|
* @returns The bounding box of the node.
|
||||||
*/
|
*/
|
||||||
export const getLayerBboxFast = (layer: Konva.Layer): IRect => {
|
export const getNodeBboxFast = (node: Konva.Node): IRect => {
|
||||||
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
const bbox = node.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||||
return {
|
return {
|
||||||
x: Math.floor(bbox.x),
|
x: Math.floor(bbox.x),
|
||||||
y: Math.floor(bbox.y),
|
y: Math.floor(bbox.y),
|
||||||
@ -210,7 +210,7 @@ export const updateBboxes = (
|
|||||||
|
|
||||||
if (entityState.type === 'layer') {
|
if (entityState.type === 'layer') {
|
||||||
if (entityState.objects.length === 0) {
|
if (entityState.objects.length === 0) {
|
||||||
// No objects - no bbox to calculate
|
// No objects - no bbox to calculate
|
||||||
onBboxChanged({ id: entityState.id, bbox: null }, 'layer');
|
onBboxChanged({ id: entityState.id, bbox: null }, 'layer');
|
||||||
} else {
|
} else {
|
||||||
onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer');
|
onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer');
|
||||||
|
@ -1,42 +1,62 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
||||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type { CanvasEntityIdentifier, InpaintMaskEntity, Tool } from 'features/controlLayers/store/types';
|
import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export class CanvasInpaintMask {
|
export class CanvasInpaintMask {
|
||||||
id: string;
|
id: string;
|
||||||
|
manager: KonvaNodeManager;
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
|
objectsGroup: Konva.Group;
|
||||||
compositingRect: Konva.Rect;
|
compositingRect: Konva.Rect;
|
||||||
|
transformer: Konva.Transformer;
|
||||||
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
||||||
|
|
||||||
constructor(entity: InpaintMaskEntity, onPosChanged: StateApi['onPosChanged']) {
|
constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) {
|
||||||
this.id = entity.id;
|
this.id = entity.id;
|
||||||
|
this.manager = manager;
|
||||||
this.layer = new Konva.Layer({
|
this.layer = new Konva.Layer({
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
dragDistance: 0,
|
dragDistance: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
|
||||||
// the position - we do not need to call this on the `dragmove` event.
|
|
||||||
this.layer.on('dragend', function (e) {
|
|
||||||
onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask');
|
|
||||||
});
|
|
||||||
this.group = new Konva.Group({
|
this.group = new Konva.Group({
|
||||||
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
|
this.objectsGroup = new Konva.Group({});
|
||||||
|
this.group.add(this.objectsGroup);
|
||||||
this.layer.add(this.group);
|
this.layer.add(this.group);
|
||||||
|
|
||||||
|
this.transformer = new Konva.Transformer({
|
||||||
|
shouldOverdrawWholeArea: true,
|
||||||
|
draggable: true,
|
||||||
|
dragDistance: 0,
|
||||||
|
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
|
rotateEnabled: false,
|
||||||
|
flipEnabled: false,
|
||||||
|
});
|
||||||
|
this.transformer.on('transformend', () => {
|
||||||
|
this.manager.stateApi.onScaleChanged(
|
||||||
|
{ id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() },
|
||||||
|
'inpaint_mask'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.transformer.on('dragend', () => {
|
||||||
|
this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'inpaint_mask');
|
||||||
|
});
|
||||||
|
this.layer.add(this.transformer);
|
||||||
|
|
||||||
this.compositingRect = new Konva.Rect({ listening: false });
|
this.compositingRect = new Konva.Rect({ listening: false });
|
||||||
this.layer.add(this.compositingRect);
|
this.group.add(this.compositingRect);
|
||||||
this.objects = new Map();
|
this.objects = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,24 +64,16 @@ export class CanvasInpaintMask {
|
|||||||
this.layer.destroy();
|
this.layer.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(
|
async render(inpaintMaskState: InpaintMaskEntity) {
|
||||||
inpaintMaskState: InpaintMaskEntity,
|
|
||||||
selectedTool: Tool,
|
|
||||||
selectedEntityIdentifier: CanvasEntityIdentifier | null,
|
|
||||||
maskOpacity: number
|
|
||||||
) {
|
|
||||||
// Update the layer's position and listening state
|
// Update the layer's position and listening state
|
||||||
this.layer.setAttrs({
|
this.group.setAttrs({
|
||||||
listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
x: inpaintMaskState.x,
|
||||||
x: Math.floor(inpaintMaskState.x),
|
y: inpaintMaskState.y,
|
||||||
y: Math.floor(inpaintMaskState.y),
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
let didDraw = false;
|
||||||
const rgbColor = rgbColorToString(inpaintMaskState.fill);
|
|
||||||
|
|
||||||
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
|
||||||
let groupNeedsCache = false;
|
|
||||||
|
|
||||||
const objectIds = inpaintMaskState.objects.map(mapId);
|
const objectIds = inpaintMaskState.objects.map(mapId);
|
||||||
// Destroy any objects that are no longer in state
|
// Destroy any objects that are no longer in state
|
||||||
@ -69,7 +81,7 @@ export class CanvasInpaintMask {
|
|||||||
if (!objectIds.includes(object.id)) {
|
if (!objectIds.includes(object.id)) {
|
||||||
this.objects.delete(object.id);
|
this.objects.delete(object.id);
|
||||||
object.destroy();
|
object.destroy();
|
||||||
groupNeedsCache = true;
|
didDraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,13 +93,12 @@ export class CanvasInpaintMask {
|
|||||||
if (!brushLine) {
|
if (!brushLine) {
|
||||||
brushLine = new KonvaBrushLine(obj);
|
brushLine = new KonvaBrushLine(obj);
|
||||||
this.objects.set(brushLine.id, brushLine);
|
this.objects.set(brushLine.id, brushLine);
|
||||||
this.group.add(brushLine.konvaLineGroup);
|
this.objectsGroup.add(brushLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
didDraw = true;
|
||||||
}
|
} else {
|
||||||
|
if (brushLine.update(obj)) {
|
||||||
if (obj.points.length !== brushLine.konvaLine.points().length) {
|
didDraw = true;
|
||||||
brushLine.konvaLine.points(obj.points);
|
}
|
||||||
groupNeedsCache = true;
|
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'eraser_line') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
let eraserLine = this.objects.get(obj.id);
|
let eraserLine = this.objects.get(obj.id);
|
||||||
@ -96,13 +107,12 @@ export class CanvasInpaintMask {
|
|||||||
if (!eraserLine) {
|
if (!eraserLine) {
|
||||||
eraserLine = new KonvaEraserLine(obj);
|
eraserLine = new KonvaEraserLine(obj);
|
||||||
this.objects.set(eraserLine.id, eraserLine);
|
this.objects.set(eraserLine.id, eraserLine);
|
||||||
this.group.add(eraserLine.konvaLineGroup);
|
this.objectsGroup.add(eraserLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
didDraw = true;
|
||||||
}
|
} else {
|
||||||
|
if (eraserLine.update(obj)) {
|
||||||
if (obj.points.length !== eraserLine.konvaLine.points().length) {
|
didDraw = true;
|
||||||
eraserLine.konvaLine.points(obj.points);
|
}
|
||||||
groupNeedsCache = true;
|
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'rect_shape') {
|
} else if (obj.type === 'rect_shape') {
|
||||||
let rect = this.objects.get(obj.id);
|
let rect = this.objects.get(obj.id);
|
||||||
@ -111,8 +121,12 @@ export class CanvasInpaintMask {
|
|||||||
if (!rect) {
|
if (!rect) {
|
||||||
rect = new KonvaRect(obj);
|
rect = new KonvaRect(obj);
|
||||||
this.objects.set(rect.id, rect);
|
this.objects.set(rect.id, rect);
|
||||||
this.group.add(rect.konvaRect);
|
this.objectsGroup.add(rect.konvaRect);
|
||||||
groupNeedsCache = true;
|
didDraw = true;
|
||||||
|
} else {
|
||||||
|
if (rect.update(obj)) {
|
||||||
|
didDraw = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,96 +134,91 @@ export class CanvasInpaintMask {
|
|||||||
// Only update layer visibility if it has changed.
|
// Only update layer visibility if it has changed.
|
||||||
if (this.layer.visible() !== inpaintMaskState.isEnabled) {
|
if (this.layer.visible() !== inpaintMaskState.isEnabled) {
|
||||||
this.layer.visible(inpaintMaskState.isEnabled);
|
this.layer.visible(inpaintMaskState.isEnabled);
|
||||||
groupNeedsCache = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.objects.size === 0) {
|
|
||||||
// No objects - clear the cache to reset the previous pixel data
|
|
||||||
this.group.clearCache();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
|
||||||
if (this.group.isCached()) {
|
|
||||||
this.group.clearCache();
|
|
||||||
}
|
|
||||||
// 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.group.opacity(1);
|
||||||
|
|
||||||
this.compositingRect.setAttrs({
|
if (didDraw) {
|
||||||
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||||
...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox
|
const rgbColor = rgbColorToString(inpaintMaskState.fill);
|
||||||
? inpaintMaskState.bbox
|
const maskOpacity = this.manager.stateApi.getMaskOpacity();
|
||||||
: getLayerBboxFast(this.layer)),
|
|
||||||
fill: rgbColor,
|
|
||||||
opacity: maskOpacity,
|
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
|
||||||
globalCompositeOperation: 'source-in',
|
|
||||||
visible: true,
|
|
||||||
// This rect must always be on top of all other shapes
|
|
||||||
zIndex: this.objects.size + 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id;
|
this.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
|
||||||
|
...getNodeBboxFast(this.objectsGroup),
|
||||||
|
fill: rgbColor,
|
||||||
|
opacity: maskOpacity,
|
||||||
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
|
globalCompositeOperation: 'source-in',
|
||||||
|
visible: true,
|
||||||
|
// This rect must always be on top of all other shapes
|
||||||
|
zIndex: this.objects.size + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// /**
|
this.updateGroup(didDraw);
|
||||||
// * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
|
}
|
||||||
// * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
|
|
||||||
// *
|
|
||||||
// * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
|
|
||||||
// * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
|
|
||||||
// * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
|
|
||||||
// *
|
|
||||||
// * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
|
|
||||||
// * a single raster image, and _then_ applied the 50% opacity.
|
|
||||||
// */
|
|
||||||
// if (isSelected && selectedTool !== 'move') {
|
|
||||||
// // We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
|
||||||
// if (this.konvaObjectGroup.isCached()) {
|
|
||||||
// this.konvaObjectGroup.clearCache();
|
|
||||||
// }
|
|
||||||
// // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
|
||||||
// this.konvaObjectGroup.opacity(1);
|
|
||||||
|
|
||||||
// this.compositingRect.setAttrs({
|
updateGroup(didDraw: boolean) {
|
||||||
// // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||||
// ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox
|
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||||
// ? inpaintMaskState.bbox
|
|
||||||
// : getLayerBboxFast(this.konvaLayer)),
|
|
||||||
// fill: rgbColor,
|
|
||||||
// opacity: maskOpacity,
|
|
||||||
// // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
|
||||||
// globalCompositeOperation: 'source-in',
|
|
||||||
// visible: true,
|
|
||||||
// // This rect must always be on top of all other shapes
|
|
||||||
// zIndex: this.objects.size + 1,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// // The compositing rect should only be shown when the layer is selected.
|
|
||||||
// this.compositingRect.visible(false);
|
|
||||||
// // Cache only if needed - or if we are on this code path and _don't_ have a cache
|
|
||||||
// if (groupNeedsCache || !this.konvaObjectGroup.isCached()) {
|
|
||||||
// this.konvaObjectGroup.cache();
|
|
||||||
// }
|
|
||||||
// // Updating group opacity does not require re-caching
|
|
||||||
// this.konvaObjectGroup.opacity(maskOpacity);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const bboxRect =
|
if (this.objects.size === 0) {
|
||||||
// regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer);
|
// If the layer is totally empty, reset the cache and bail out.
|
||||||
// if (rg.bbox) {
|
this.layer.listening(false);
|
||||||
// const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move';
|
this.transformer.nodes([]);
|
||||||
// bboxRect.setAttrs({
|
if (this.group.isCached()) {
|
||||||
// visible: active,
|
this.group.clearCache();
|
||||||
// listening: active,
|
}
|
||||||
// x: rg.bbox.x,
|
return;
|
||||||
// y: rg.bbox.y,
|
}
|
||||||
// width: rg.bbox.width,
|
|
||||||
// height: rg.bbox.height,
|
if (isSelected && selectedTool === 'move') {
|
||||||
// stroke: isSelected ? BBOX_SELECTED_STROKE : '',
|
// When the layer is selected and being moved, we should always cache it.
|
||||||
// });
|
// We should update the cache if we drew to the layer.
|
||||||
// } else {
|
if (!this.group.isCached() || didDraw) {
|
||||||
// bboxRect.visible(false);
|
this.group.cache();
|
||||||
// }
|
}
|
||||||
|
// Activate the transformer
|
||||||
|
this.layer.listening(true);
|
||||||
|
this.transformer.nodes([this.group]);
|
||||||
|
this.transformer.forceUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected && selectedTool !== 'move') {
|
||||||
|
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
|
||||||
|
this.layer.listening(false);
|
||||||
|
// The transformer also does not need to be active.
|
||||||
|
this.transformer.nodes([]);
|
||||||
|
if (isDrawingTool(selectedTool)) {
|
||||||
|
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
||||||
|
// should never be cached.
|
||||||
|
if (this.group.isCached()) {
|
||||||
|
this.group.clearCache();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
|
||||||
|
// We should update the cache if we drew to the layer.
|
||||||
|
if (!this.group.isCached() || didDraw) {
|
||||||
|
this.group.cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelected) {
|
||||||
|
// Unselected layers should not be listening
|
||||||
|
this.layer.listening(false);
|
||||||
|
// The transformer also does not need to be active.
|
||||||
|
this.transformer.nodes([]);
|
||||||
|
// Update the layer's cache if it's not already cached or we drew to it.
|
||||||
|
if (!this.group.isCached() || didDraw) {
|
||||||
|
this.group.cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
||||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types';
|
||||||
@ -138,7 +138,7 @@ export class CanvasRegion {
|
|||||||
|
|
||||||
this.compositingRect.setAttrs({
|
this.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
|
||||||
...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.layer)),
|
...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getNodeBboxFast(this.layer)),
|
||||||
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)
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
imImageCacheChanged,
|
imImageCacheChanged,
|
||||||
imLinePointAdded,
|
imLinePointAdded,
|
||||||
imRectAdded,
|
imRectAdded,
|
||||||
|
imScaled,
|
||||||
imTranslated,
|
imTranslated,
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerBrushLineAdded,
|
layerBrushLineAdded,
|
||||||
@ -107,6 +108,8 @@ export const initializeRenderer = (
|
|||||||
logIfDebugging('onScaleChanged');
|
logIfDebugging('onScaleChanged');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
dispatch(layerScaled(arg));
|
dispatch(layerScaled(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
dispatch(imScaled(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
@ -441,6 +444,7 @@ export const initializeRenderer = (
|
|||||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
||||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
||||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
||||||
|
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
||||||
) {
|
) {
|
||||||
logIfDebugging('Arranging entities');
|
logIfDebugging('Arranging entities');
|
||||||
|
@ -336,6 +336,7 @@ export const {
|
|||||||
imEraserLineAdded,
|
imEraserLineAdded,
|
||||||
imLinePointAdded,
|
imLinePointAdded,
|
||||||
imRectAdded,
|
imRectAdded,
|
||||||
|
imScaled,
|
||||||
// Staging
|
// Staging
|
||||||
stagingAreaStartedStaging,
|
stagingAreaStartedStaging,
|
||||||
stagingAreaImageAdded,
|
stagingAreaImageAdded,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||||
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
||||||
import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types';
|
import type { CanvasV2State, InpaintMaskEntity, ScaleChangedArg } from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types';
|
import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -29,6 +29,27 @@ export const inpaintMaskReducers = {
|
|||||||
state.inpaintMask.x = x;
|
state.inpaintMask.x = x;
|
||||||
state.inpaintMask.y = y;
|
state.inpaintMask.y = y;
|
||||||
},
|
},
|
||||||
|
imScaled: (state, action: PayloadAction<ScaleChangedArg>) => {
|
||||||
|
const { scale, x, y } = action.payload;
|
||||||
|
for (const obj of state.inpaintMask.objects) {
|
||||||
|
if (obj.type === 'brush_line') {
|
||||||
|
obj.points = obj.points.map((point) => point * scale);
|
||||||
|
obj.strokeWidth *= scale;
|
||||||
|
} else if (obj.type === 'eraser_line') {
|
||||||
|
obj.points = obj.points.map((point) => point * scale);
|
||||||
|
obj.strokeWidth *= scale;
|
||||||
|
} else if (obj.type === 'rect_shape') {
|
||||||
|
obj.x *= scale;
|
||||||
|
obj.y *= scale;
|
||||||
|
obj.height *= scale;
|
||||||
|
obj.width *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.inpaintMask.x = x;
|
||||||
|
state.inpaintMask.y = y;
|
||||||
|
state.inpaintMask.bboxNeedsUpdate = true;
|
||||||
|
state.inpaintMask.imageCache = null;
|
||||||
|
},
|
||||||
imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => {
|
imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => {
|
||||||
const { bbox } = action.payload;
|
const { bbox } = action.payload;
|
||||||
state.inpaintMask.bbox = bbox;
|
state.inpaintMask.bbox = bbox;
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
LayerEntity,
|
LayerEntity,
|
||||||
PointAddedToLineArg,
|
PointAddedToLineArg,
|
||||||
RectShapeAddedArg,
|
RectShapeAddedArg,
|
||||||
|
ScaleChangedArg,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types';
|
import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types';
|
||||||
|
|
||||||
@ -174,7 +175,7 @@ export const layersReducers = {
|
|||||||
payload: { ...payload, lineId: uuidv4() },
|
payload: { ...payload, lineId: uuidv4() },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
layerScaled: (state, action: PayloadAction<{ id: string; scale: number; x: number; y: number }>) => {
|
layerScaled: (state, action: PayloadAction<ScaleChangedArg>) => {
|
||||||
const { id, scale, x, y } = action.payload;
|
const { id, scale, x, y } = action.payload;
|
||||||
const layer = selectLayer(state, id);
|
const layer = selectLayer(state, id);
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
|
Loading…
Reference in New Issue
Block a user