mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-02 11:55:17 -05:00
docs(ui): docstrings for classes (wip)
This commit is contained in:
@@ -15,6 +15,11 @@ const DEFAULT_CONFIG: CanvasBackgroundModuleConfig = {
|
||||
GRID_LINE_COLOR_FINE: getArbitraryBaseColor(18),
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a background grid on the canvas, where the grid spacing changes based on the stage scale.
|
||||
*
|
||||
* The grid is only visible when the dynamic grid setting is enabled.
|
||||
*/
|
||||
export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
readonly type = 'background';
|
||||
|
||||
@@ -22,13 +27,19 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
path: string[];
|
||||
parent: CanvasManager;
|
||||
manager: CanvasManager;
|
||||
subscriptions = new Set<() => void>();
|
||||
log: Logger;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
config: CanvasBackgroundModuleConfig = DEFAULT_CONFIG;
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the background grid:
|
||||
* - A layer to hold the grid lines
|
||||
* - An array of grid lines
|
||||
*/
|
||||
konva: {
|
||||
layer: Konva.Layer;
|
||||
lines: Konva.Line[];
|
||||
};
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
@@ -41,11 +52,20 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
|
||||
this.log.debug('Creating module');
|
||||
|
||||
this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }) };
|
||||
this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }), lines: [] };
|
||||
|
||||
/**
|
||||
* The background grid should be rendered when the stage attributes change:
|
||||
* - scale
|
||||
* - position
|
||||
* - size
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stateApi.$stageAttrs.listen(this.render));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the background grid.
|
||||
*/
|
||||
render = () => {
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
@@ -56,11 +76,10 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
|
||||
this.konva.layer.visible(true);
|
||||
|
||||
this.konva.layer.zIndex(0);
|
||||
const scale = this.manager.stage.getScale();
|
||||
const { x, y } = this.manager.stage.getPosition();
|
||||
const { width, height } = this.manager.stage.getSize();
|
||||
const gridSpacing = this.getGridSpacing(scale);
|
||||
const gridSpacing = CanvasBackgroundModule.getGridSpacing(scale);
|
||||
const stageRect = {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
@@ -99,41 +118,44 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
let _y = 0;
|
||||
|
||||
this.konva.layer.destroyChildren();
|
||||
this.konva.lines = [];
|
||||
|
||||
for (let i = 0; i < xSteps; i++) {
|
||||
_x = gridFullRect.x1 + i * gridSpacing;
|
||||
this.konva.layer.add(
|
||||
new Konva.Line({
|
||||
x: _x,
|
||||
y: gridFullRect.y1,
|
||||
points: [0, 0, 0, ySize],
|
||||
stroke: _x % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
||||
strokeWidth,
|
||||
listening: false,
|
||||
})
|
||||
);
|
||||
const line = new Konva.Line({
|
||||
x: _x,
|
||||
y: gridFullRect.y1,
|
||||
points: [0, 0, 0, ySize],
|
||||
stroke: _x % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
||||
strokeWidth,
|
||||
listening: false,
|
||||
});
|
||||
this.konva.lines.push(line);
|
||||
this.konva.layer.add(line);
|
||||
}
|
||||
for (let i = 0; i < ySteps; i++) {
|
||||
_y = gridFullRect.y1 + i * gridSpacing;
|
||||
this.konva.layer.add(
|
||||
new Konva.Line({
|
||||
x: gridFullRect.x1,
|
||||
y: _y,
|
||||
points: [0, 0, xSize, 0],
|
||||
stroke: _y % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
||||
strokeWidth,
|
||||
listening: false,
|
||||
})
|
||||
);
|
||||
const line = new Konva.Line({
|
||||
x: gridFullRect.x1,
|
||||
y: _y,
|
||||
points: [0, 0, xSize, 0],
|
||||
stroke: _y % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
||||
strokeWidth,
|
||||
listening: false,
|
||||
});
|
||||
this.konva.lines.push(line);
|
||||
this.konva.layer.add(line);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
||||
* Gets the grid line spacing for the dynamic grid.
|
||||
*
|
||||
* The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
||||
*
|
||||
* @param scale The stage scale
|
||||
* @returns The grid spacing based on the stage scale
|
||||
*/
|
||||
getGridSpacing = (scale: number): number => {
|
||||
static getGridSpacing = (scale: number): number => {
|
||||
if (scale >= 2) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMult
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Rect } from 'features/controlLayers/store/types';
|
||||
import type { Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@@ -21,6 +21,9 @@ const ALL_ANCHORS: string[] = [
|
||||
const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
const NO_ANCHORS: string[] = [];
|
||||
|
||||
/**
|
||||
* Renders the bounding box. The bounding box can be transformed by the user.
|
||||
*/
|
||||
export class CanvasBboxModule extends CanvasModuleBase {
|
||||
readonly type = 'bbox';
|
||||
|
||||
@@ -32,12 +35,24 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
|
||||
subscriptions: Set<() => void> = new Set();
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the bbox:
|
||||
* - A group to hold all the objects
|
||||
* - A transformer to allow the bbox to be transformed
|
||||
* - A transparent rect so the transformer has something to transform
|
||||
*/
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
rect: Konva.Rect;
|
||||
transformer: Konva.Transformer;
|
||||
proxyRect: Konva.Rect;
|
||||
};
|
||||
|
||||
/**
|
||||
* Buffer to store the last aspect ratio of the bbox. When the users holds shift while transforming the bbox, this is
|
||||
* used to lock the aspect ratio.
|
||||
*/
|
||||
$aspectRatioBuffer = atom(0);
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
super();
|
||||
this.id = getPrefixedId(this.type);
|
||||
@@ -48,16 +63,15 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
|
||||
this.log.debug('Creating bbox module');
|
||||
|
||||
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
|
||||
// transforming the bbox.
|
||||
// Set the initial aspect ratio buffer per app state.
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height);
|
||||
this.$aspectRatioBuffer.set(bbox.rect.width / bbox.rect.height);
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
||||
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
||||
// transparent rect for this purpose.
|
||||
rect: new Konva.Rect({
|
||||
group: new Konva.Group({ name: `${this.type}:group`, listening: true }),
|
||||
// We will use a Konva.Transformer for the generation bbox. Transformers need some shape to transform, so we will
|
||||
// create a transparent rect for this purpose.
|
||||
proxyRect: new Konva.Rect({
|
||||
name: `${this.type}:rect`,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
@@ -83,173 +97,43 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
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.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 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,
|
||||
};
|
||||
},
|
||||
anchorStyleFunc: this.anchorStyleFunc,
|
||||
anchorDragBoundFunc: this.anchorDragBoundFunc,
|
||||
}),
|
||||
};
|
||||
this.konva.rect.on('dragmove', () => {
|
||||
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const bboxRect: Rect = {
|
||||
...bbox.rect,
|
||||
x: roundToMultiple(this.konva.rect.x(), gridSize),
|
||||
y: roundToMultiple(this.konva.rect.y(), gridSize),
|
||||
};
|
||||
this.konva.rect.setAttrs(bboxRect);
|
||||
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
|
||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||
}
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transform', () => {
|
||||
// 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.
|
||||
const anchor = this.konva.transformer.getActiveAnchor();
|
||||
if (!anchor) {
|
||||
// Pretty sure we should always have an anchor here?
|
||||
return;
|
||||
}
|
||||
this.konva.proxyRect.on('dragmove', this.onDragMove);
|
||||
this.konva.transformer.on('transform', this.onTransform);
|
||||
this.konva.transformer.on('transformend', this.onTransformEnd);
|
||||
|
||||
const alt = this.manager.stateApi.$altKey.get();
|
||||
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
||||
const meta = this.manager.stateApi.$metaKey.get();
|
||||
const shift = this.manager.stateApi.$shiftKey.get();
|
||||
|
||||
// Grid size depends on the modifier keys
|
||||
let gridSize = ctrl || meta ? 8 : 64;
|
||||
|
||||
// Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the
|
||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||
if (this.manager.stateApi.$altKey.get()) {
|
||||
gridSize = gridSize * 2;
|
||||
}
|
||||
|
||||
// The coords should be correct per the anchorDragBoundFunc.
|
||||
let x = this.konva.rect.x();
|
||||
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
|
||||
// *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.
|
||||
let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), 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 alt/opt is held - this requires math too big for my brain.
|
||||
if (shift && CORNER_ANCHORS.includes(anchor) && !alt) {
|
||||
// Fit the bbox to the last aspect ratio
|
||||
let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get());
|
||||
let fittedHeight = fittedWidth / $aspectRatioBuffer.get();
|
||||
fittedWidth = roundToMultipleMin(fittedWidth, gridSize);
|
||||
fittedHeight = roundToMultipleMin(fittedHeight, gridSize);
|
||||
|
||||
// We need to adjust the x and y coords to have the resize occur from the right origin.
|
||||
if (anchor === 'top-left') {
|
||||
// The transform origin is the bottom-right anchor. Both x and y need to be updated.
|
||||
x = x - (fittedWidth - width);
|
||||
y = y - (fittedHeight - height);
|
||||
}
|
||||
if (anchor === 'top-right') {
|
||||
// The transform origin is the bottom-left anchor. Only y needs to be updated.
|
||||
y = y - (fittedHeight - height);
|
||||
}
|
||||
if (anchor === 'bottom-left') {
|
||||
// The transform origin is the top-right anchor. Only x needs to be updated.
|
||||
x = x - (fittedWidth - width);
|
||||
}
|
||||
// Update the width and height to the fitted dims.
|
||||
width = fittedWidth;
|
||||
height = fittedHeight;
|
||||
}
|
||||
|
||||
const bboxRect = {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
// 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.
|
||||
// Gotta be a way to avoid setting it twice...
|
||||
this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
|
||||
|
||||
// Update the bbox in internal state.
|
||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||
|
||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||
if (!shift) {
|
||||
$aspectRatioBuffer.set(bboxRect.width / bboxRect.height);
|
||||
}
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transformend', () => {
|
||||
// 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.
|
||||
$aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height());
|
||||
});
|
||||
|
||||
// The transformer will always be transforming the dummy rect
|
||||
this.konva.transformer.nodes([this.konva.rect]);
|
||||
this.konva.group.add(this.konva.rect);
|
||||
// The transformer will always be transforming the proxy rect
|
||||
this.konva.transformer.nodes([this.konva.proxyRect]);
|
||||
this.konva.group.add(this.konva.proxyRect);
|
||||
this.konva.group.add(this.konva.transformer);
|
||||
|
||||
// We will listen to the tool state to determine if the bbox should be visible or not.
|
||||
this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the bbox. The bbox is only visible when the tool is set to 'bbox'.
|
||||
*/
|
||||
render = () => {
|
||||
this.log.trace('Rendering bbox module');
|
||||
this.log.trace('Rendering');
|
||||
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const tool = this.manager.stateApi.$tool.get();
|
||||
|
||||
this.konva.group.visible(true);
|
||||
|
||||
// We need to reach up to the preview layer to enable/disable listening so that the bbox can be interacted with.
|
||||
this.manager.konva.previewLayer.listening(tool === 'bbox');
|
||||
|
||||
this.konva.group.listening(tool === 'bbox');
|
||||
this.konva.rect.setAttrs({
|
||||
x: bbox.rect.x,
|
||||
y: bbox.rect.y,
|
||||
width: bbox.rect.width,
|
||||
height: bbox.rect.height,
|
||||
this.konva.proxyRect.setAttrs({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
listening: tool === 'bbox',
|
||||
@@ -261,8 +145,173 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
this.log.trace('Destroying bbox module');
|
||||
this.log.trace('Destroying module');
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the dragmove event on the bbox rect:
|
||||
* - Snaps the bbox position to the grid (determined by ctrl/meta key)
|
||||
* - Pushes the new bbox rect into app state
|
||||
*/
|
||||
onDragMove = () => {
|
||||
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
const bboxRect: Rect = {
|
||||
...bbox.rect,
|
||||
x: roundToMultiple(this.konva.proxyRect.x(), gridSize),
|
||||
y: roundToMultiple(this.konva.proxyRect.y(), gridSize),
|
||||
};
|
||||
this.konva.proxyRect.setAttrs(bboxRect);
|
||||
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
|
||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the transform event on the bbox transformer:
|
||||
* - Snaps the bbox dimensions to the grid (determined by ctrl/meta key)
|
||||
* - Centered scaling when alt is held
|
||||
* - Aspect ratio locking when shift is held
|
||||
* - Pushes the new bbox rect into app state
|
||||
* - Syncs the aspect ratio buffer
|
||||
*/
|
||||
onTransform = () => {
|
||||
// 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.
|
||||
const anchor = this.konva.transformer.getActiveAnchor();
|
||||
if (!anchor) {
|
||||
// Pretty sure we should always have an anchor here?
|
||||
return;
|
||||
}
|
||||
|
||||
const alt = this.manager.stateApi.$altKey.get();
|
||||
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
||||
const meta = this.manager.stateApi.$metaKey.get();
|
||||
const shift = this.manager.stateApi.$shiftKey.get();
|
||||
|
||||
// Grid size depends on the modifier keys
|
||||
let gridSize = ctrl || meta ? 8 : 64;
|
||||
|
||||
// Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the
|
||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||
if (this.manager.stateApi.$altKey.get()) {
|
||||
gridSize = gridSize * 2;
|
||||
}
|
||||
|
||||
// The coords should be correct per the anchorDragBoundFunc.
|
||||
let x = this.konva.proxyRect.x();
|
||||
let y = this.konva.proxyRect.y();
|
||||
|
||||
// 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
|
||||
// them to the grid.
|
||||
let width = roundToMultipleMin(this.konva.proxyRect.width() * this.konva.proxyRect.scaleX(), gridSize);
|
||||
let height = roundToMultipleMin(this.konva.proxyRect.height() * this.konva.proxyRect.scaleY(), gridSize);
|
||||
|
||||
// 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 (shift && CORNER_ANCHORS.includes(anchor) && !alt) {
|
||||
// Fit the bbox to the last aspect ratio
|
||||
let fittedWidth = Math.sqrt(width * height * this.$aspectRatioBuffer.get());
|
||||
let fittedHeight = fittedWidth / this.$aspectRatioBuffer.get();
|
||||
fittedWidth = roundToMultipleMin(fittedWidth, gridSize);
|
||||
fittedHeight = roundToMultipleMin(fittedHeight, gridSize);
|
||||
|
||||
// We need to adjust the x and y coords to have the resize occur from the right origin.
|
||||
if (anchor === 'top-left') {
|
||||
// The transform origin is the bottom-right anchor. Both x and y need to be updated.
|
||||
x = x - (fittedWidth - width);
|
||||
y = y - (fittedHeight - height);
|
||||
}
|
||||
if (anchor === 'top-right') {
|
||||
// The transform origin is the bottom-left anchor. Only y needs to be updated.
|
||||
y = y - (fittedHeight - height);
|
||||
}
|
||||
if (anchor === 'bottom-left') {
|
||||
// The transform origin is the top-right anchor. Only x needs to be updated.
|
||||
x = x - (fittedWidth - width);
|
||||
}
|
||||
// Update the width and height to the fitted dims.
|
||||
width = fittedWidth;
|
||||
height = fittedHeight;
|
||||
}
|
||||
|
||||
const bboxRect = {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
// Update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
|
||||
this.konva.proxyRect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
|
||||
|
||||
// Update the bbox in internal state.
|
||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||
|
||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||
if (!shift) {
|
||||
this.$aspectRatioBuffer.set(bboxRect.width / bboxRect.height);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the transformend event on the bbox transformer:
|
||||
* - Updates the aspect ratio buffer with the new aspect ratio
|
||||
*/
|
||||
onTransformEnd = () => {
|
||||
// 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.
|
||||
this.$aspectRatioBuffer.set(this.konva.proxyRect.width() / this.konva.proxyRect.height());
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is called for each anchor on the transformer. It sets the style of the anchor based on its name.
|
||||
* We make the x/y resize anchors little bars.
|
||||
*/
|
||||
anchorStyleFunc = (anchor: Konva.Rect): void => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is called for each anchor on the transformer. It sets the drag bounds for the anchor based on the
|
||||
* stage's position and the grid size. Care is taken to ensure the anchor snaps to the grid correctly.
|
||||
*/
|
||||
anchorDragBoundFunc = (oldAbsPos: Coordinate, newAbsPos: Coordinate): Coordinate => {
|
||||
// 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.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ const DEFAULT_CONFIG: BrushToolPreviewConfig = {
|
||||
BORDER_OUTER_COLOR: 'rgba(255,255,255,0.8)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a preview of the brush tool on the canvas.
|
||||
*/
|
||||
export class CanvasBrushToolPreview extends CanvasModuleBase {
|
||||
readonly type = 'brush_tool_preview';
|
||||
|
||||
@@ -33,6 +36,13 @@ export class CanvasBrushToolPreview extends CanvasModuleBase {
|
||||
|
||||
config: BrushToolPreviewConfig = DEFAULT_CONFIG;
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the brush tool preview:
|
||||
* - A group to hold the fill circle and borders
|
||||
* - A circle to fill the brush area
|
||||
* - An inner border ring
|
||||
* - An outer border ring
|
||||
*/
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
fillCircle: Konva.Circle;
|
||||
@@ -80,6 +90,7 @@ export class CanvasBrushToolPreview extends CanvasModuleBase {
|
||||
render = () => {
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
|
||||
// If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity.
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +140,7 @@ export class CanvasBrushToolPreview extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying brush tool preview module');
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ const DEFAULT_CONFIG: CanvasCacheModuleConfig = {
|
||||
generationModeCacheSize: 100,
|
||||
};
|
||||
|
||||
/**
|
||||
* A cache module for storing the results of expensive calculations. For example, when we rasterize a layer and upload
|
||||
* it to the server, we store the resultant image name in this cache for future use.
|
||||
*/
|
||||
export class CanvasCacheModule extends CanvasModuleBase {
|
||||
readonly type = 'cache';
|
||||
|
||||
@@ -37,8 +41,28 @@ export class CanvasCacheModule extends CanvasModuleBase {
|
||||
|
||||
config: CanvasCacheModuleConfig = DEFAULT_CONFIG;
|
||||
|
||||
/**
|
||||
* A cache for storing image names. Used as a cache for results of layer/canvas/entity exports. For example, when we
|
||||
* rasterize a layer and upload it to the server, we store the image name in this cache.
|
||||
*
|
||||
* The cache key is a hash of the exported entity's state and the export rect.
|
||||
*/
|
||||
imageNameCache = new LRUCache<string, string>({ max: this.config.imageNameCacheSize });
|
||||
|
||||
/**
|
||||
* A cache for storing canvas elements. Similar to the image name cache, but for canvas elements. The primary use is
|
||||
* for caching composite layers. For example, the canvas compositor module uses this to store the canvas elements for
|
||||
* individual raster layers when creating a composite of the layers.
|
||||
*
|
||||
* The cache key is a hash of the exported entity's state and the export rect.
|
||||
*/
|
||||
canvasElementCache = new LRUCache<string, HTMLCanvasElement>({ max: this.config.canvasElementCacheSize });
|
||||
/**
|
||||
* A cache for the generation mode calculation, which is fairly expensive.
|
||||
*
|
||||
* The cache key is a hash of all the objects that contribute to the generation mode calculation (e.g. the composite
|
||||
* raster layer, the composite inpaint mask, and bounding box), and the value is the generation mode.
|
||||
*/
|
||||
generationModeCache = new LRUCache<string, GenerationMode>({ max: this.config.generationModeCacheSize });
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
@@ -52,6 +76,9 @@ export class CanvasCacheModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating cache module');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all caches.
|
||||
*/
|
||||
clearAll = () => {
|
||||
this.canvasElementCache.clear();
|
||||
this.imageNameCache.clear();
|
||||
|
||||
@@ -63,6 +63,9 @@ const DEFAULT_CONFIG: ColorPickerToolConfig = {
|
||||
CROSSHAIR_BORDER_COLOR: 'rgba(255,255,255,0.8)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a preview of the color picker tool on the canvas.
|
||||
*/
|
||||
export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
readonly type = 'color_picker_tool_preview';
|
||||
|
||||
@@ -74,10 +77,16 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
|
||||
config: ColorPickerToolConfig = DEFAULT_CONFIG;
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the color picker tool preview:
|
||||
* - A group to hold all the objects
|
||||
* - A ring that shows the candidate and current color
|
||||
* - A crosshair to help with color selection
|
||||
*/
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
ringNewColor: Konva.Ring;
|
||||
ringOldColor: Konva.Arc;
|
||||
ringCandidateColor: Konva.Ring;
|
||||
ringCurrentColor: Konva.Arc;
|
||||
ringInnerBorder: Konva.Ring;
|
||||
ringOuterBorder: Konva.Ring;
|
||||
crosshairNorthInner: Konva.Line;
|
||||
@@ -98,18 +107,18 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug('Creating color picker tool preview module');
|
||||
this.log.debug('Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: `${this.type}:color_picker_group`, listening: false }),
|
||||
ringNewColor: new Konva.Ring({
|
||||
name: `${this.type}:color_picker_new_color_ring`,
|
||||
ringCandidateColor: new Konva.Ring({
|
||||
name: `${this.type}:color_picker_candidate_color_ring`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
ringOldColor: new Konva.Arc({
|
||||
name: `${this.type}:color_picker_old_color_arc`,
|
||||
ringCurrentColor: new Konva.Arc({
|
||||
name: `${this.type}:color_picker_current_color_arc`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
angle: 180,
|
||||
@@ -164,8 +173,8 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
this.konva.group.add(
|
||||
this.konva.ringNewColor,
|
||||
this.konva.ringOldColor,
|
||||
this.konva.ringCandidateColor,
|
||||
this.konva.ringCurrentColor,
|
||||
this.konva.ringInnerBorder,
|
||||
this.konva.ringOuterBorder,
|
||||
this.konva.crosshairNorthOuter,
|
||||
@@ -179,9 +188,13 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the color picker tool preview on the canvas.
|
||||
*/
|
||||
render = () => {
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
|
||||
// If the cursor position is not available, do not render the preview. The tool module will handle visibility.
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
@@ -193,14 +206,14 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
|
||||
this.konva.ringNewColor.setAttrs({
|
||||
this.konva.ringCandidateColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(colorUnderCursor),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.ringOldColor.setAttrs({
|
||||
this.konva.ringCurrentColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(toolState.fill),
|
||||
|
||||
@@ -15,6 +15,12 @@ import type { ImageDTO } from 'services/api/types';
|
||||
import stableHash from 'stable-hash';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* Handles compositing operations:
|
||||
* - Rasterizing and uploading the composite raster layer
|
||||
* - Rasterizing and uploading the composite inpaint mask
|
||||
* - Caclulating the generation mode (which requires the composite raster layer and inpaint mask)
|
||||
*/
|
||||
export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
readonly type = 'compositor';
|
||||
|
||||
@@ -34,6 +40,11 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating compositor module');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity IDs of all raster layers that should be included in the composite raster layer.
|
||||
* A raster layer is included if it is enabled and has objects.
|
||||
* @returns An array of raster layer entity IDs
|
||||
*/
|
||||
getCompositeRasterLayerEntityIds = (): string[] => {
|
||||
const ids = [];
|
||||
for (const adapter of this.manager.adapters.rasterLayers.values()) {
|
||||
@@ -44,16 +55,35 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
return ids;
|
||||
};
|
||||
|
||||
getCompositeInpaintMaskEntityIds = (): string[] => {
|
||||
const ids = [];
|
||||
for (const adapter of this.manager.adapters.inpaintMasks.values()) {
|
||||
if (adapter.state.isEnabled && adapter.renderer.hasObjects()) {
|
||||
ids.push(adapter.id);
|
||||
/**
|
||||
* Gets a hash of the composite raster layer, which includes the state of all raster layers that are included in the
|
||||
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
|
||||
* @param extra Any extra data to include in the hash
|
||||
* @returns A hash for the composite raster layer
|
||||
*/
|
||||
getCompositeRasterLayerHash = (extra: SerializableObject): string => {
|
||||
const data: Record<string, SerializableObject> = {
|
||||
extra,
|
||||
};
|
||||
for (const id of this.getCompositeRasterLayerEntityIds()) {
|
||||
const adapter = this.manager.adapters.rasterLayers.get(id);
|
||||
if (!adapter) {
|
||||
this.log.warn({ id }, 'Raster layer adapter not found');
|
||||
continue;
|
||||
}
|
||||
data[id] = adapter.getHashableState();
|
||||
}
|
||||
return ids;
|
||||
return stableHash(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a canvas element for the composite raster layer. Only the region defined by the rect is included in the canvas.
|
||||
*
|
||||
* If the hash of the composite raster layer is found in the cache, the cached canvas is returned.
|
||||
*
|
||||
* @param rect The region to include in the canvas
|
||||
* @returns A canvas element with the composite raster layer drawn on it
|
||||
*/
|
||||
getCompositeRasterLayerCanvas = (rect: Rect): HTMLCanvasElement => {
|
||||
const hash = this.getCompositeRasterLayerHash({ rect });
|
||||
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
|
||||
@@ -86,6 +116,99 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
return canvas;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rasterizes the composite raster layer and uploads it to the server.
|
||||
*
|
||||
* If the hash of the composite raster layer is found in the cache, the cached image DTO is returned.
|
||||
*
|
||||
* @param rect The region to include in the rasterized image
|
||||
* @param saveToGallery Whether to save the image to the gallery or just return the uploaded image DTO
|
||||
* @returns A promise that resolves to the uploaded image DTO
|
||||
*/
|
||||
rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean): Promise<ImageDTO> => {
|
||||
this.log.trace({ rect }, 'Rasterizing composite raster layer');
|
||||
|
||||
const canvas = this.getCompositeRasterLayerCanvas(rect);
|
||||
const blob = await canvasToBlob(canvas);
|
||||
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Composite raster layer canvas');
|
||||
}
|
||||
|
||||
return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the image DTO for the composite raster layer.
|
||||
*
|
||||
* If the image is found in the cache, the cached image DTO is returned.
|
||||
*
|
||||
* @param rect The region to include in the image
|
||||
* @returns A promise that resolves to the image DTO
|
||||
*/
|
||||
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
|
||||
const hash = this.getCompositeRasterLayerHash({ rect });
|
||||
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
|
||||
|
||||
if (cachedImageName) {
|
||||
imageDTO = await getImageDTO(cachedImageName);
|
||||
if (imageDTO) {
|
||||
this.log.trace({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite raster layer image');
|
||||
return imageDTO;
|
||||
}
|
||||
}
|
||||
|
||||
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
|
||||
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the entity IDs of all inpaint masks that should be included in the composite inpaint mask.
|
||||
* An inpaint mask is included if it is enabled and has objects.
|
||||
* @returns An array of inpaint mask entity IDs
|
||||
*/
|
||||
getCompositeInpaintMaskEntityIds = (): string[] => {
|
||||
const ids = [];
|
||||
for (const adapter of this.manager.adapters.inpaintMasks.values()) {
|
||||
if (adapter.state.isEnabled && adapter.renderer.hasObjects()) {
|
||||
ids.push(adapter.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a hash of the composite inpaint mask, which includes the state of all inpaint masks that are included in the
|
||||
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
|
||||
* @param extra Any extra data to include in the hash
|
||||
* @returns A hash for the composite inpaint mask
|
||||
*/
|
||||
getCompositeInpaintMaskHash = (extra: SerializableObject): string => {
|
||||
const data: Record<string, SerializableObject> = {
|
||||
extra,
|
||||
};
|
||||
for (const id of this.getCompositeInpaintMaskEntityIds()) {
|
||||
const adapter = this.manager.adapters.inpaintMasks.get(id);
|
||||
if (!adapter) {
|
||||
this.log.warn({ id }, 'Inpaint mask adapter not found');
|
||||
continue;
|
||||
}
|
||||
data[id] = adapter.getHashableState();
|
||||
}
|
||||
return stableHash(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a canvas element for the composite inpaint mask. Only the region defined by the rect is included in the canvas.
|
||||
*
|
||||
* If the hash of the composite inpaint mask is found in the cache, the cached canvas is returned.
|
||||
*
|
||||
* @param rect The region to include in the canvas
|
||||
* @returns A canvas element with the composite inpaint mask drawn on it
|
||||
*/
|
||||
getCompositeInpaintMaskCanvas = (rect: Rect): HTMLCanvasElement => {
|
||||
const hash = this.getCompositeInpaintMaskHash({ rect });
|
||||
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
|
||||
@@ -118,68 +241,15 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
return canvas;
|
||||
};
|
||||
|
||||
getCompositeRasterLayerHash = (extra: SerializableObject): string => {
|
||||
const data: Record<string, SerializableObject> = {
|
||||
extra,
|
||||
};
|
||||
for (const id of this.getCompositeRasterLayerEntityIds()) {
|
||||
const adapter = this.manager.adapters.rasterLayers.get(id);
|
||||
if (!adapter) {
|
||||
this.log.warn({ id }, 'Raster layer adapter not found');
|
||||
continue;
|
||||
}
|
||||
data[id] = adapter.getHashableState();
|
||||
}
|
||||
return stableHash(data);
|
||||
};
|
||||
|
||||
getCompositeInpaintMaskHash = (extra: SerializableObject): string => {
|
||||
const data: Record<string, SerializableObject> = {
|
||||
extra,
|
||||
};
|
||||
for (const id of this.getCompositeInpaintMaskEntityIds()) {
|
||||
const adapter = this.manager.adapters.inpaintMasks.get(id);
|
||||
if (!adapter) {
|
||||
this.log.warn({ id }, 'Inpaint mask adapter not found');
|
||||
continue;
|
||||
}
|
||||
data[id] = adapter.getHashableState();
|
||||
}
|
||||
return stableHash(data);
|
||||
};
|
||||
|
||||
rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean) => {
|
||||
this.log.trace({ rect }, 'Rasterizing composite raster layer');
|
||||
|
||||
const canvas = this.getCompositeRasterLayerCanvas(rect);
|
||||
const blob = await canvasToBlob(canvas);
|
||||
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Composite raster layer canvas');
|
||||
}
|
||||
|
||||
return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery);
|
||||
};
|
||||
|
||||
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
|
||||
const hash = this.getCompositeRasterLayerHash({ rect });
|
||||
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
|
||||
|
||||
if (cachedImageName) {
|
||||
imageDTO = await getImageDTO(cachedImageName);
|
||||
if (imageDTO) {
|
||||
this.log.trace({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite raster layer image');
|
||||
return imageDTO;
|
||||
}
|
||||
}
|
||||
|
||||
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
|
||||
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rasterizes the composite inpaint mask and uploads it to the server.
|
||||
*
|
||||
* If the hash of the composite inpaint mask is found in the cache, the cached image DTO is returned.
|
||||
*
|
||||
* @param rect The region to include in the rasterized image
|
||||
* @param saveToGallery Whether to save the image to the gallery or just return the uploaded image DTO
|
||||
* @returns A promise that resolves to the uploaded image DTO
|
||||
*/
|
||||
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
|
||||
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
|
||||
|
||||
@@ -192,6 +262,14 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
return uploadImage(blob, 'composite-inpaint-mask.png', 'general', !saveToGallery);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the image DTO for the composite inpaint mask.
|
||||
*
|
||||
* If the image is found in the cache, the cached image DTO is returned.
|
||||
*
|
||||
* @param rect The region to include in the image
|
||||
* @returns A promise that resolves to the image DTO
|
||||
*/
|
||||
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
|
||||
@@ -211,6 +289,24 @@ export class CanvasCompositorModule extends CanvasModuleBase {
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the generation mode for the current canvas state. This is determined by the transparency of the
|
||||
* composite raster layer and composite inpaint mask:
|
||||
* - Composite raster layer is fully transparent -> txt2img
|
||||
* - Composite raster layer is partially transparent -> outpainting
|
||||
* - Composite raster layer is opaque & composite inpaint mask is fully transparent -> img2img
|
||||
* - Composite raster layer is opaque & composite inpaint mask is partially transparent -> inpainting
|
||||
*
|
||||
* Definitions:
|
||||
* - Fully transparent: all pixels have an alpha value of 0.
|
||||
* - Partially transparent: at least one pixel with an alpha value of 0 & at least one pixel with an alpha value
|
||||
* greater than 0.
|
||||
* - Opaque: all pixels have an alpha value greater than 0.
|
||||
*
|
||||
* The generation mode is cached to avoid recalculating it when the canvas state has not changed.
|
||||
*
|
||||
* @returns The generation mode
|
||||
*/
|
||||
getGenerationMode(): GenerationMode {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
|
||||
|
||||
@@ -22,6 +22,15 @@ import type { Logger } from 'roarr';
|
||||
import stableHash from 'stable-hash';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* Handles the rendering for a single raster or control layer entity.
|
||||
*
|
||||
* This module has two main components:
|
||||
* - A transformer, which handles the positioning and interaction state of the layer
|
||||
* - A renderer, which handles the rendering of the layer's objects
|
||||
*
|
||||
* The canvas rendering module interacts with this module to coordinate the rendering of all raster and control layers.
|
||||
*/
|
||||
export class CanvasEntityLayerAdapter extends CanvasModuleBase {
|
||||
readonly type = 'entity_layer_adapter';
|
||||
|
||||
@@ -31,12 +40,29 @@ export class CanvasEntityLayerAdapter extends CanvasModuleBase {
|
||||
parent: CanvasManager;
|
||||
log: Logger;
|
||||
|
||||
/**
|
||||
* The last known state of the entity.
|
||||
*/
|
||||
state: CanvasRasterLayerState | CanvasControlLayerState;
|
||||
|
||||
/**
|
||||
* The Konva nodes that make up the entity layer:
|
||||
* - A layer to hold the everything
|
||||
*
|
||||
* Note that the transformer and object renderer have their own Konva nodes, but they are not stored here.
|
||||
*/
|
||||
konva: {
|
||||
layer: Konva.Layer;
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer for this entity layer.
|
||||
*/
|
||||
transformer: CanvasEntityTransformer;
|
||||
|
||||
/**
|
||||
* The renderer for this entity layer.
|
||||
*/
|
||||
renderer: CanvasEntityRenderer;
|
||||
|
||||
isFirstRender: boolean = true;
|
||||
|
||||
Reference in New Issue
Block a user