feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering

This commit is contained in:
psychedelicious 2024-06-21 14:47:57 +10:00
parent d045f24014
commit bd5a85bf70
11 changed files with 371 additions and 391 deletions

View File

@ -1,16 +1,6 @@
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type {
BrushLineAddedArg,
CanvasEntity,
CanvasV2State,
EraserLineAddedArg,
PointAddedToLineArg,
RectShapeAddedArg,
RgbaColor,
StageAttrs,
Tool,
} from 'features/controlLayers/store/types';
import type { CanvasEntity } from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es';
@ -25,43 +15,16 @@ import {
} from './constants';
import { PREVIEW_TOOL_GROUP_ID } from './naming';
type Arg = {
manager: KonvaNodeManager;
getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
setTool: (tool: Tool) => void;
setToolBuffer: (tool: Tool | null) => void;
getIsDrawing: () => boolean;
setIsDrawing: (isDrawing: boolean) => void;
getIsMouseDown: () => boolean;
setIsMouseDown: (isMouseDown: boolean) => void;
getLastMouseDownPos: () => Vector2d | null;
setLastMouseDownPos: (pos: Vector2d | null) => void;
getLastCursorPos: () => Vector2d | null;
setLastCursorPos: (pos: Vector2d | null) => void;
getLastAddedPoint: () => Vector2d | null;
setLastAddedPoint: (pos: Vector2d | null) => void;
setStageAttrs: (attrs: StageAttrs) => void;
getSelectedEntity: () => CanvasEntity | null;
getSpaceKey: () => boolean;
setSpaceKey: (val: boolean) => void;
getBbox: () => CanvasV2State['bbox'];
getSettings: () => CanvasV2State['settings'];
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void;
onBrushWidthChanged: (size: number) => void;
onEraserWidthChanged: (size: number) => void;
};
/**
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
* cursor is not over the stage.
* @param stage The konva stage
* @param setLastCursorPos The callback to store the cursor pos
*/
const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastCursorPos']) => {
const updateLastCursorPos = (
stage: Konva.Stage,
setLastCursorPos: KonvaNodeManager['stateApi']['setLastCursorPos']
) => {
const pos = getScaledFlooredCursorPosition(stage);
if (!pos) {
return null;
@ -93,10 +56,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => {
const maybeAddNextPoint = (
selectedEntity: CanvasEntity,
currentPos: Vector2d,
getToolState: Arg['getToolState'],
getLastAddedPoint: Arg['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'],
onPointAddedToLine: Arg['onPointAddedToLine']
getToolState: KonvaNodeManager['stateApi']['getToolState'],
getLastAddedPoint: KonvaNodeManager['stateApi']['getLastAddedPoint'],
setLastAddedPoint: KonvaNodeManager['stateApi']['setLastAddedPoint'],
onPointAddedToLine: KonvaNodeManager['stateApi']['onPointAddedToLine']
) => {
const isDrawableEntity =
selectedEntity?.type === 'regional_guidance' ||
@ -132,42 +95,42 @@ const maybeAddNextPoint = (
);
};
export const setStageEventHandlers = ({
manager,
getToolState,
getCurrentFill,
setTool,
setToolBuffer,
getIsDrawing,
setIsDrawing,
getIsMouseDown,
setIsMouseDown,
getLastMouseDownPos,
setLastMouseDownPos,
getLastCursorPos,
setLastCursorPos,
getLastAddedPoint,
setLastAddedPoint,
setStageAttrs,
getSelectedEntity,
getSpaceKey,
setSpaceKey,
getBbox,
getSettings,
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
onBrushWidthChanged: onBrushSizeChanged,
onEraserWidthChanged: onEraserSizeChanged,
}: Arg): (() => void) => {
const stage = manager.stage;
export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) => {
const { stage, stateApi } = manager;
const {
getToolState,
getCurrentFill,
setTool,
setToolBuffer,
getIsDrawing,
setIsDrawing,
getIsMouseDown,
setIsMouseDown,
getLastMouseDownPos,
setLastMouseDownPos,
getLastCursorPos,
setLastCursorPos,
getLastAddedPoint,
setLastAddedPoint,
setStageAttrs,
getSelectedEntity,
getSpaceKey,
setSpaceKey,
getBbox,
getSettings,
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
onBrushWidthChanged,
onEraserWidthChanged,
} = stateApi;
//#region mouseenter
stage.on('mouseenter', () => {
const tool = getToolState().selected;
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region mousedown
@ -288,7 +251,7 @@ export const setStageEventHandlers = ({
setLastAddedPoint(pos);
}
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region mouseup
@ -327,7 +290,7 @@ export const setStageEventHandlers = ({
setLastMouseDownPos(null);
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region mousemove
@ -433,7 +396,7 @@ export const setStageEventHandlers = ({
}
}
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region mouseleave
@ -462,7 +425,7 @@ export const setStageEventHandlers = ({
}
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region wheel
@ -477,9 +440,9 @@ export const setStageEventHandlers = ({
}
// Holding ctrl or meta while scrolling changes the brush size
if (toolState.selected === 'brush') {
onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta));
onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta));
} else if (toolState.selected === 'eraser') {
onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta));
onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta));
}
} else {
// We need the absolute cursor position - not the scaled position
@ -503,11 +466,11 @@ export const setStageEventHandlers = ({
stage.scaleY(newScale);
stage.position(newPos);
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
manager.renderers.renderBackground();
manager.renderers.renderDocumentOverlay();
manager.konvaApi.renderBackground();
manager.konvaApi.renderDocumentOverlay();
}
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region dragmove
@ -519,9 +482,9 @@ export const setStageEventHandlers = ({
height: stage.height(),
scale: stage.scaleX(),
});
manager.renderers.renderBackground();
manager.renderers.renderDocumentOverlay();
manager.renderers.renderToolPreview();
manager.konvaApi.renderBackground();
manager.konvaApi.renderDocumentOverlay();
manager.konvaApi.renderToolPreview();
});
//#region dragend
@ -534,7 +497,7 @@ export const setStageEventHandlers = ({
height: stage.height(),
scale: stage.scaleX(),
});
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
});
//#region key
@ -555,12 +518,12 @@ export const setStageEventHandlers = ({
setTool('view');
setSpaceKey(true);
} else if (e.key === 'r') {
manager.renderers.fitDocumentToStage();
manager.renderers.renderToolPreview();
manager.renderers.renderBackground();
manager.renderers.renderDocumentOverlay();
manager.konvaApi.fitDocumentToStage();
manager.konvaApi.renderToolPreview();
manager.konvaApi.renderBackground();
manager.konvaApi.renderDocumentOverlay();
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
};
window.addEventListener('keydown', onKeyDown);
@ -578,7 +541,7 @@ export const setStageEventHandlers = ({
setToolBuffer(null);
setSpaceKey(false);
}
manager.renderers.renderToolPreview();
manager.konvaApi.renderToolPreview();
};
window.addEventListener('keyup', onKeyUp);

View File

@ -1,5 +1,22 @@
import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types';
import type {
BrushLine,
BrushLineAddedArg,
CanvasEntity,
CanvasV2State,
EraserLine,
EraserLineAddedArg,
ImageObject,
PointAddedToLineArg,
PosChangedArg,
Rect,
RectShape,
RectShapeAddedArg,
RgbaColor,
StageAttrs,
Tool,
} from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import { assert } from 'tsafe';
export type BrushLineObjectRecord = {
@ -36,7 +53,7 @@ export type ImageObjectRecord = {
type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord;
type KonvaRenderers = {
type KonvaApi = {
renderRegions: () => void;
renderLayers: () => void;
renderControlAdapters: () => void;
@ -45,8 +62,9 @@ type KonvaRenderers = {
renderDocumentOverlay: () => void;
renderBackground: () => void;
renderToolPreview: () => void;
fitDocumentToStage: () => void;
arrangeEntities: () => void;
fitDocumentToStage: () => void;
fitStageToContainer: () => void;
};
type BackgroundLayer = {
@ -79,18 +97,63 @@ type PreviewLayer = {
};
};
type StateApi = {
getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
setTool: (tool: Tool) => void;
setToolBuffer: (tool: Tool | null) => void;
getIsDrawing: () => boolean;
setIsDrawing: (isDrawing: boolean) => void;
getIsMouseDown: () => boolean;
setIsMouseDown: (isMouseDown: boolean) => void;
getLastMouseDownPos: () => Vector2d | null;
setLastMouseDownPos: (pos: Vector2d | null) => void;
getLastCursorPos: () => Vector2d | null;
setLastCursorPos: (pos: Vector2d | null) => void;
getLastAddedPoint: () => Vector2d | null;
setLastAddedPoint: (pos: Vector2d | null) => void;
setStageAttrs: (attrs: StageAttrs) => void;
getSelectedEntity: () => CanvasEntity | null;
getSpaceKey: () => boolean;
setSpaceKey: (val: boolean) => void;
getBbox: () => CanvasV2State['bbox'];
getSettings: () => CanvasV2State['settings'];
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void;
onBrushWidthChanged: (size: number) => void;
onEraserWidthChanged: (size: number) => void;
getMaskOpacity: () => number;
onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
onBboxTransformed: (bbox: Rect) => void;
getShiftKey: () => boolean;
getCtrlKey: () => boolean;
getMetaKey: () => boolean;
getAltKey: () => boolean;
getDocument: () => CanvasV2State['document'];
getLayerEntityStates: () => CanvasV2State['layers']['entities'];
getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities'];
getRegionEntityStates: () => CanvasV2State['regions']['entities'];
getInpaintMaskEntityState: () => CanvasV2State['inpaintMask'];
};
export class KonvaNodeManager {
stage: Konva.Stage;
container: HTMLDivElement;
adapters: Map<string, KonvaEntityAdapter>;
_background: BackgroundLayer | null;
_preview: PreviewLayer | null;
_renderers: KonvaRenderers | null;
_konvaApi: KonvaApi | null;
_stateApi: StateApi | null;
constructor(stage: Konva.Stage) {
constructor(stage: Konva.Stage, container: HTMLDivElement) {
this.stage = stage;
this._renderers = null;
this.container = container;
this._konvaApi = null;
this._preview = null;
this._background = null;
this._stateApi = null;
this.adapters = new Map();
}
@ -121,13 +184,13 @@ export class KonvaNodeManager {
return this.adapters.delete(id);
}
set renderers(renderers: KonvaRenderers) {
this._renderers = renderers;
set konvaApi(konvaApi: KonvaApi) {
this._konvaApi = konvaApi;
}
get renderers(): KonvaRenderers {
assert(this._renderers !== null, 'Konva renderers have not been set');
return this._renderers;
get konvaApi(): KonvaApi {
assert(this._konvaApi !== null, 'Konva API has not been set');
return this._konvaApi;
}
set preview(preview: PreviewLayer) {
@ -147,6 +210,15 @@ export class KonvaNodeManager {
assert(this._background !== null, 'Konva background layer has not been set');
return this._background;
}
set stateApi(stateApi: StateApi) {
this._stateApi = stateApi;
}
get stateApi(): StateApi {
assert(this._stateApi !== null, 'State API has not been set');
return this._stateApi;
}
}
export class KonvaEntityAdapter {

View File

@ -1,23 +1,14 @@
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import type { CanvasV2State } from 'features/controlLayers/store/types';
/**
* Gets a function to arrange the entities in the konva stage.
* @param manager The konva node manager
* @param getLayerEntityStates A function to get all layer entity states
* @param getControlAdapterEntityStates A function to get all control adapter entity states
* @param getRegionEntityStates A function to get all region entity states
* @returns An arrange entities function
*/
export const getArrangeEntities =
(arg: {
manager: KonvaNodeManager;
getLayerEntityStates: () => CanvasV2State['layers']['entities'];
getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities'];
getRegionEntityStates: () => CanvasV2State['regions']['entities'];
}) =>
(): void => {
const { manager, getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = arg;
export const getArrangeEntities = (manager: KonvaNodeManager) => {
const { getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = manager.stateApi;
function arrangeEntities(): void {
const layers = getLayerEntityStates();
const controlAdapters = getControlAdapterEntityStates();
const regions = getRegionEntityStates();
@ -34,4 +25,7 @@ export const getArrangeEntities =
}
manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex);
manager.preview.layer.zIndex(++zIndex);
};
}
return arrangeEntities;
};

View File

@ -38,82 +38,85 @@ export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BA
/**
* Gets a render function for the background layer.
* @param arg.manager The konva node manager
* @param manager The konva node manager
* @returns A function to render the background grid
*/
export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): void => {
const { manager } = arg;
const background = manager.background.layer;
background.zIndex(0);
const scale = manager.stage.scaleX();
const gridSpacing = getGridSpacing(scale);
const x = manager.stage.x();
const y = manager.stage.y();
const width = manager.stage.width();
const height = manager.stage.height();
const stageRect = {
x1: 0,
y1: 0,
x2: width,
y2: height,
};
export const getRenderBackground = (manager: KonvaNodeManager) => {
function renderBackground(): void {
const background = manager.background.layer;
background.zIndex(0);
const scale = manager.stage.scaleX();
const gridSpacing = getGridSpacing(scale);
const x = manager.stage.x();
const y = manager.stage.y();
const width = manager.stage.width();
const height = manager.stage.height();
const stageRect = {
x1: 0,
y1: 0,
x2: width,
y2: height,
};
const gridOffset = {
x: Math.ceil(x / scale / gridSpacing) * gridSpacing,
y: Math.ceil(y / scale / gridSpacing) * gridSpacing,
};
const gridOffset = {
x: Math.ceil(x / scale / gridSpacing) * gridSpacing,
y: Math.ceil(y / scale / gridSpacing) * gridSpacing,
};
const gridRect = {
x1: -gridOffset.x,
y1: -gridOffset.y,
x2: width / scale - gridOffset.x + gridSpacing,
y2: height / scale - gridOffset.y + gridSpacing,
};
const gridRect = {
x1: -gridOffset.x,
y1: -gridOffset.y,
x2: width / scale - gridOffset.x + gridSpacing,
y2: height / scale - gridOffset.y + gridSpacing,
};
const gridFullRect = {
x1: Math.min(stageRect.x1, gridRect.x1),
y1: Math.min(stageRect.y1, gridRect.y1),
x2: Math.max(stageRect.x2, gridRect.x2),
y2: Math.max(stageRect.y2, gridRect.y2),
};
const gridFullRect = {
x1: Math.min(stageRect.x1, gridRect.x1),
y1: Math.min(stageRect.y1, gridRect.y1),
x2: Math.max(stageRect.x2, gridRect.x2),
y2: Math.max(stageRect.y2, gridRect.y2),
};
// find the x & y size of the grid
const xSize = gridFullRect.x2 - gridFullRect.x1;
const ySize = gridFullRect.y2 - gridFullRect.y1;
// compute the number of steps required on each axis.
const xSteps = Math.round(xSize / gridSpacing) + 1;
const ySteps = Math.round(ySize / gridSpacing) + 1;
// find the x & y size of the grid
const xSize = gridFullRect.x2 - gridFullRect.x1;
const ySize = gridFullRect.y2 - gridFullRect.y1;
// compute the number of steps required on each axis.
const xSteps = Math.round(xSize / gridSpacing) + 1;
const ySteps = Math.round(ySize / gridSpacing) + 1;
const strokeWidth = 1 / scale;
let _x = 0;
let _y = 0;
const strokeWidth = 1 / scale;
let _x = 0;
let _y = 0;
background.destroyChildren();
background.destroyChildren();
for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing;
background.add(
new Konva.Line({
x: _x,
y: gridFullRect.y1,
points: [0, 0, 0, ySize],
stroke: _x % 64 ? fineGridLineColor : baseGridLineColor,
strokeWidth,
listening: false,
})
);
}
for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing;
background.add(
new Konva.Line({
x: gridFullRect.x1,
y: _y,
points: [0, 0, xSize, 0],
stroke: _y % 64 ? fineGridLineColor : baseGridLineColor,
strokeWidth,
listening: false,
})
);
for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing;
background.add(
new Konva.Line({
x: _x,
y: gridFullRect.y1,
points: [0, 0, 0, ySize],
stroke: _x % 64 ? fineGridLineColor : baseGridLineColor,
strokeWidth,
listening: false,
})
);
}
for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing;
background.add(
new Konva.Line({
x: gridFullRect.x1,
y: _y,
points: [0, 0, xSize, 0],
stroke: _y % 64 ? fineGridLineColor : baseGridLineColor,
strokeWidth,
listening: false,
})
);
}
}
return renderBackground;
};

View File

@ -6,16 +6,11 @@ import {
createObjectGroup,
updateImageSource,
} from 'features/controlLayers/konva/renderers/objects';
import type { CanvasV2State, ControlAdapterEntity } from 'features/controlLayers/store/types';
import type { ControlAdapterEntity } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { isEqual } from 'lodash-es';
import { assert } from 'tsafe';
/**
* Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects
* and require some special handling to update the source and attributes as control images are swapped or processed.
*/
/**
* Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist.
* @param manager The konva node manager
@ -102,16 +97,12 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co
/**
* Gets a function to render all control adapters.
* @param manager The konva node manager
* @param getControlAdapterEntityStates A function to get all control adapter entities
* @returns A function to render all control adapters
*/
export const getRenderControlAdapters =
(arg: {
manager: KonvaNodeManager;
getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities'];
}) =>
(): void => {
const { manager, getControlAdapterEntityStates } = arg;
export const getRenderControlAdapters = (manager: KonvaNodeManager) => {
const { getControlAdapterEntityStates } = manager.stateApi;
function renderControlAdapters(): void {
const entities = getControlAdapterEntityStates();
// Destroy nonexistent layers
for (const adapters of manager.getAll('control_adapter')) {
@ -122,4 +113,7 @@ export const getRenderControlAdapters =
for (const entity of entities) {
renderControlAdapter(manager, entity);
}
};
}
return renderControlAdapters;
};

View File

@ -16,7 +16,7 @@ import {
getRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasEntity, CanvasV2State, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types';
import type { CanvasEntity, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types';
import Konva from 'konva';
/**
@ -66,26 +66,14 @@ const getInpaintMask = (
};
/**
* Gets the inpaint mask render function.
* Gets a function to render the inpaint mask.
* @param manager The konva node manager
* @param getEntityState A function to get the inpaint mask entity state
* @param getMaskOpacity A function to get the mask opacity
* @param getToolState A function to get the tool state
* @param getSelectedEntity A function to get the selected entity
* @param onPosChanged Callback for when the position changes (e.g. the entity is dragged)
* @returns The inpaint mask render function
* @returns A function to render the inpaint mask
*/
export const getRenderInpaintMask =
(arg: {
manager: KonvaNodeManager;
getInpaintMaskEntityState: () => CanvasV2State['inpaintMask'];
getMaskOpacity: () => number;
getToolState: () => CanvasV2State['tool'];
getSelectedEntity: () => CanvasEntity | null;
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
}) =>
(): void => {
const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg;
export const getRenderInpaintMask = (manager: KonvaNodeManager) => {
const { getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi;
function renderInpaintMask(): void {
const entity = getInpaintMaskEntityState();
const globalMaskLayerOpacity = getMaskOpacity();
const toolState = getToolState();
@ -228,4 +216,7 @@ export const getRenderInpaintMask =
// } else {
// bboxRect.visible(false);
// }
};
}
return renderInpaintMask;
};

View File

@ -15,13 +15,9 @@ import {
getRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasEntity, CanvasV2State, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
/**
* Logic for creating and rendering raster layers.
*/
/**
* Gets layer entity's konva nodes and entity adapter, creating them if they do not exist.
* @param manager The konva node manager
@ -137,20 +133,12 @@ export const renderLayer = async (
/**
* Gets a function to render all layers.
* @param manager The konva node manager
* @param getLayerEntityStates A function to get all layer entities
* @param getToolState A function to get the current tool state
* @param onPosChanged Callback for when the layer's position changes
* @returns A function to render all layers
*/
export const getRenderLayers =
(arg: {
manager: KonvaNodeManager;
getLayerEntityStates: () => CanvasV2State['layers']['entities'];
getToolState: () => CanvasV2State['tool'];
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
}) =>
(): void => {
const { manager, getLayerEntityStates, getToolState, onPosChanged } = arg;
export const getRenderLayers = (manager: KonvaNodeManager) => {
const { getLayerEntityStates, getToolState, onPosChanged } = manager.stateApi;
function renderLayers(): void {
const entities = getLayerEntityStates();
const tool = getToolState();
// Destroy nonexistent layers
@ -162,4 +150,7 @@ export const getRenderLayers =
for (const entity of entities) {
renderLayer(manager, entity, tool.selected, onPosChanged);
}
};
}
return renderLayers;
};

View File

@ -19,9 +19,9 @@ import {
PREVIEW_TOOL_GROUP_ID,
} from 'features/controlLayers/konva/naming';
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import type { CanvasEntity, CanvasV2State, RgbaColor } from 'features/controlLayers/store/types';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types';
import type { IRect } from 'konva/lib/types';
import { atom } from 'nanostores';
/**
@ -245,13 +245,12 @@ const NO_ANCHORS: string[] = [];
/**
* Gets the bbox render function.
* @param manager The konva node manager
* @param getBbox A function to get the bbox
* @param getToolState A function to get the tool state
* @returns The bbox render function
*/
export const getRenderBbox =
(manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) =>
(): void => {
export const getRenderBbox = (manager: KonvaNodeManager) => {
const { getBbox, getToolState } = manager.stateApi;
return (): void => {
const bbox = getBbox();
const toolState = getToolState();
manager.preview.bbox.group.listening(toolState.selected === 'bbox');
@ -270,6 +269,7 @@ export const getRenderBbox =
enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS,
});
};
};
/**
* Gets the tool preview konva nodes.
@ -328,39 +328,21 @@ export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] =>
/**
* Gets the tool preview (brush, eraser, rect) render function.
* @param arg.manager The konva node manager
* @param arg.getToolState The selected tool
* @param arg.currentFill The selected layer's color
* @param arg.selectedEntity The selected layer's type
* @param arg.globalMaskLayerOpacity The global mask layer opacity
* @param arg.cursorPos The cursor position
* @param arg.lastMouseDownPos The position of the last mouse down event - used for the rect tool
* @param arg.brushSize The brush size
* @param manager The konva node manager
* @returns The tool preview render function
*/
export const getRenderToolPreview =
(arg: {
manager: KonvaNodeManager;
getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
getSelectedEntity: () => CanvasEntity | null;
getLastCursorPos: () => Vector2d | null;
getLastMouseDownPos: () => Vector2d | null;
getIsDrawing: () => boolean;
getIsMouseDown: () => boolean;
}) =>
(): void => {
const {
manager,
getToolState,
getCurrentFill,
getSelectedEntity,
getLastCursorPos,
getLastMouseDownPos,
getIsDrawing,
getIsMouseDown,
} = arg;
export const getRenderToolPreview = (manager: KonvaNodeManager) => {
const {
getToolState,
getCurrentFill,
getSelectedEntity,
getLastCursorPos,
getLastMouseDownPos,
getIsDrawing,
getIsMouseDown,
} = manager.stateApi;
return (): void => {
const stage = manager.stage;
const layerCount = manager.adapters.size;
const toolState = getToolState();
@ -451,6 +433,7 @@ export const getRenderToolPreview =
}
}
};
};
/**
* Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview
@ -493,13 +476,13 @@ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOv
/**
* Gets the document overlay render function.
* @param arg.manager The konva node manager
* @param arg.getDocument A function to get the document state
* @param manager The konva node manager
* @returns The document overlay render function
*/
export const getRenderDocumentOverlay =
(arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => {
const { manager, getDocument } = arg;
export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => {
const { getDocument } = manager.stateApi;
function renderDocumentOverlay(): void {
const document = getDocument();
const stage = manager.stage;
@ -524,4 +507,7 @@ export const getRenderDocumentOverlay =
width: document.width,
height: document.height,
});
};
}
return renderDocumentOverlay;
};

View File

@ -19,7 +19,6 @@ import { mapId } from 'features/controlLayers/konva/util';
import type {
CanvasEntity,
CanvasEntityIdentifier,
CanvasV2State,
PosChangedArg,
RegionEntity,
Tool,
@ -230,25 +229,13 @@ export const renderRegion = (
/**
* Gets a function to render all regions.
* @param arg.manager The konva node manager
* @param arg.getRegionEntityStates A function to get all region entities
* @param arg.getMaskOpacity A function to get the mask opacity
* @param arg.getToolState A function to get the tool state
* @param arg.getSelectedEntity A function to get the selectedEntity
* @param arg.onPosChanged A callback for when the position of an entity changes
* @param manager The konva node manager
* @returns A function to render all regions
*/
export const getRenderRegions =
(arg: {
manager: KonvaNodeManager;
getRegionEntityStates: () => CanvasV2State['regions']['entities'];
getMaskOpacity: () => number;
getToolState: () => CanvasV2State['tool'];
getSelectedEntity: () => CanvasEntity | null;
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
}) =>
() => {
const { manager, getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg;
export const getRenderRegions = (manager: KonvaNodeManager) => {
const { getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi;
function renderRegions(): void {
const entities = getRegionEntityStates();
const maskOpacity = getMaskOpacity();
const toolState = getToolState();
@ -264,4 +251,7 @@ export const getRenderRegions =
for (const entity of entities) {
renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged);
}
};
}
return renderRegions;
};

View File

@ -21,7 +21,7 @@ import {
getRenderToolPreview,
} from 'features/controlLayers/konva/renderers/preview';
import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions';
import { getFitDocumentToStage } from 'features/controlLayers/konva/renderers/stage';
import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage';
import {
$stageAttrs,
bboxChanged,
@ -283,7 +283,7 @@ export const initializeRenderer = (
spaceKey = val;
};
const manager = new KonvaNodeManager(stage);
const manager = new KonvaNodeManager(stage, container);
$nodeManager.set(manager);
manager.background = { layer: createBackgroundLayer() };
@ -298,17 +298,31 @@ export const initializeRenderer = (
manager.preview.layer.add(manager.preview.tool.group);
manager.preview.layer.add(manager.preview.documentOverlay.group);
manager.stage.add(manager.preview.layer);
const cleanupListeners = setStageEventHandlers({
manager,
manager.stateApi = {
// Read-only state
getToolState,
getSelectedEntity,
getBbox,
getSettings,
getCurrentFill,
getAltKey: $alt.get,
getCtrlKey: $ctrl.get,
getMetaKey: $meta.get,
getShiftKey: $shift.get,
getControlAdapterEntityStates,
getDocument,
getLayerEntityStates,
getRegionEntityStates,
getMaskOpacity,
getInpaintMaskEntityState,
// Read-write state
setTool,
setToolBuffer,
getIsDrawing,
setIsDrawing,
getIsMouseDown,
setIsMouseDown,
getSelectedEntity,
getLastAddedPoint,
setLastAddedPoint,
getLastCursorPos,
@ -318,61 +332,37 @@ export const initializeRenderer = (
getSpaceKey,
setSpaceKey,
setStageAttrs: $stageAttrs.set,
getBbox,
getSettings,
// Callbacks
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
onBrushWidthChanged,
onEraserWidthChanged,
getCurrentFill,
});
onPosChanged,
onBboxTransformed,
};
const cleanupListeners = setStageEventHandlers(manager);
// Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction.
// TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending
// the entire state over when needed.
const debouncedUpdateBboxes = debounce(updateBboxes, 300);
manager.renderers = {
renderRegions: getRenderRegions({
manager,
getRegionEntityStates,
getMaskOpacity,
getToolState,
getSelectedEntity,
onPosChanged,
}),
renderLayers: getRenderLayers({ manager, getLayerEntityStates, getToolState, onPosChanged }),
renderControlAdapters: getRenderControlAdapters({ manager, getControlAdapterEntityStates }),
renderInpaintMask: getRenderInpaintMask({
manager,
getInpaintMaskEntityState,
getMaskOpacity,
getToolState,
getSelectedEntity,
onPosChanged,
}),
renderBbox: getRenderBbox(manager, getBbox, getToolState),
renderToolPreview: getRenderToolPreview({
manager,
getToolState,
getCurrentFill,
getSelectedEntity,
getLastCursorPos,
getLastMouseDownPos,
getIsDrawing,
getIsMouseDown,
}),
renderDocumentOverlay: getRenderDocumentOverlay({ manager, getDocument }),
renderBackground: getRenderBackground({ manager }),
fitDocumentToStage: getFitDocumentToStage({ manager, getDocument, setStageAttrs: $stageAttrs.set }),
arrangeEntities: getArrangeEntities({
manager,
getLayerEntityStates,
getControlAdapterEntityStates,
getRegionEntityStates,
}),
manager.konvaApi = {
renderRegions: getRenderRegions(manager),
renderLayers: getRenderLayers(manager),
renderControlAdapters: getRenderControlAdapters(manager),
renderInpaintMask: getRenderInpaintMask(manager),
renderBbox: getRenderBbox(manager),
renderToolPreview: getRenderToolPreview(manager),
renderDocumentOverlay: getRenderDocumentOverlay(manager),
renderBackground: getRenderBackground(manager),
arrangeEntities: getArrangeEntities(manager),
fitDocumentToStage: getFitDocumentToStage(manager),
fitStageToContainer: getFitStageToContainer(manager),
};
const renderCanvas = () => {
@ -392,7 +382,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected
) {
logIfDebugging('Rendering layers');
manager.renderers.renderLayers();
manager.konvaApi.renderLayers();
}
if (
@ -402,7 +392,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected
) {
logIfDebugging('Rendering regions');
manager.renderers.renderRegions();
manager.konvaApi.renderRegions();
}
if (
@ -412,22 +402,22 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected
) {
logIfDebugging('Rendering inpaint mask');
manager.renderers.renderInpaintMask();
manager.konvaApi.renderInpaintMask();
}
if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) {
logIfDebugging('Rendering control adapters');
manager.renderers.renderControlAdapters();
manager.konvaApi.renderControlAdapters();
}
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
logIfDebugging('Rendering document bounds overlay');
manager.renderers.renderDocumentOverlay();
manager.konvaApi.renderDocumentOverlay();
}
if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) {
logIfDebugging('Rendering generation bbox');
manager.renderers.renderBbox();
manager.konvaApi.renderBbox();
}
if (
@ -447,7 +437,7 @@ export const initializeRenderer = (
canvasV2.regions.entities !== prevCanvasV2.regions.entities
) {
logIfDebugging('Arranging entities');
manager.renderers.arrangeEntities();
manager.konvaApi.arrangeEntities();
}
prevCanvasV2 = canvasV2;
@ -461,30 +451,16 @@ export const initializeRenderer = (
// We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and
// document bounds overlay when the stage is resized.
const fitStageToContainer = () => {
stage.width(container.offsetWidth);
stage.height(container.offsetHeight);
$stageAttrs.set({
x: stage.x(),
y: stage.y(),
width: stage.width(),
height: stage.height(),
scale: stage.scaleX(),
});
manager.renderers.renderBackground();
manager.renderers.renderDocumentOverlay();
};
const resizeObserver = new ResizeObserver(fitStageToContainer);
const resizeObserver = new ResizeObserver(manager.konvaApi.fitStageToContainer);
resizeObserver.observe(container);
fitStageToContainer();
manager.konvaApi.fitStageToContainer();
const unsubscribeRenderer = subscribe(renderCanvas);
logIfDebugging('First render of konva stage');
// On first render, the document should be fit to the stage.
manager.renderers.fitDocumentToStage();
manager.renderers.renderToolPreview();
manager.konvaApi.fitDocumentToStage();
manager.konvaApi.renderToolPreview();
renderCanvas();
return () => {

View File

@ -1,23 +1,15 @@
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types';
/**
* Gets a function to fit the document to the stage, resetting the stage scale to 100%.
* If the document is smaller than the stage, the stage scale is increased to fit the document.
* @param arg.manager The konva node manager
* @param arg.getDocument A function to get the current document state
* @param arg.setStageAttrs A function to set the stage attributes
* @param manager The konva node manager
* @returns A function to fit the document to the stage
*/
export const getFitDocumentToStage =
(arg: {
manager: KonvaNodeManager;
getDocument: () => CanvasV2State['document'];
setStageAttrs: (stageAttrs: StageAttrs) => void;
}) =>
(): void => {
const { manager, getDocument, setStageAttrs } = arg;
export const getFitDocumentToStage = (manager: KonvaNodeManager) => {
function fitDocumentToStage(): void {
const { getDocument, setStageAttrs } = manager.stateApi;
const document = getDocument();
// Fit & center the document on the stage
const width = manager.stage.width();
@ -29,4 +21,32 @@ export const getFitDocumentToStage =
const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale;
manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
setStageAttrs({ x, y, width, height, scale });
};
}
return fitDocumentToStage;
};
/**
* Gets a function to fit the stage to its container element. Called during resize events.
* @param manager The konva node manager
* @returns A function to fit the stage to its container
*/
export const getFitStageToContainer = (manager: KonvaNodeManager) => {
const { stage, container } = manager;
const { setStageAttrs } = manager.stateApi;
function fitStageToContainer(): void {
stage.width(container.offsetWidth);
stage.height(container.offsetHeight);
setStageAttrs({
x: stage.x(),
y: stage.y(),
width: stage.width(),
height: stage.height(),
scale: stage.scaleX(),
});
manager.konvaApi.renderBackground();
manager.konvaApi.renderDocumentOverlay();
}
return fitStageToContainer;
};