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 type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type { import type { CanvasEntity } from 'features/controlLayers/store/types';
BrushLineAddedArg,
CanvasEntity,
CanvasV2State,
EraserLineAddedArg,
PointAddedToLineArg,
RectShapeAddedArg,
RgbaColor,
StageAttrs,
Tool,
} from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
@ -25,43 +15,16 @@ import {
} from './constants'; } from './constants';
import { PREVIEW_TOOL_GROUP_ID } from './naming'; 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 * 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. * cursor is not over the stage.
* @param stage The konva stage * @param stage The konva stage
* @param setLastCursorPos The callback to store the cursor pos * @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); const pos = getScaledFlooredCursorPosition(stage);
if (!pos) { if (!pos) {
return null; return null;
@ -93,10 +56,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => {
const maybeAddNextPoint = ( const maybeAddNextPoint = (
selectedEntity: CanvasEntity, selectedEntity: CanvasEntity,
currentPos: Vector2d, currentPos: Vector2d,
getToolState: Arg['getToolState'], getToolState: KonvaNodeManager['stateApi']['getToolState'],
getLastAddedPoint: Arg['getLastAddedPoint'], getLastAddedPoint: KonvaNodeManager['stateApi']['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'], setLastAddedPoint: KonvaNodeManager['stateApi']['setLastAddedPoint'],
onPointAddedToLine: Arg['onPointAddedToLine'] onPointAddedToLine: KonvaNodeManager['stateApi']['onPointAddedToLine']
) => { ) => {
const isDrawableEntity = const isDrawableEntity =
selectedEntity?.type === 'regional_guidance' || selectedEntity?.type === 'regional_guidance' ||
@ -132,8 +95,9 @@ const maybeAddNextPoint = (
); );
}; };
export const setStageEventHandlers = ({ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) => {
manager, const { stage, stateApi } = manager;
const {
getToolState, getToolState,
getCurrentFill, getCurrentFill,
setTool, setTool,
@ -158,16 +122,15 @@ export const setStageEventHandlers = ({
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
onBrushWidthChanged: onBrushSizeChanged, onBrushWidthChanged,
onEraserWidthChanged: onEraserSizeChanged, onEraserWidthChanged,
}: Arg): (() => void) => { } = stateApi;
const stage = manager.stage;
//#region mouseenter //#region mouseenter
stage.on('mouseenter', () => { stage.on('mouseenter', () => {
const tool = getToolState().selected; const tool = getToolState().selected;
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region mousedown //#region mousedown
@ -288,7 +251,7 @@ export const setStageEventHandlers = ({
setLastAddedPoint(pos); setLastAddedPoint(pos);
} }
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region mouseup //#region mouseup
@ -327,7 +290,7 @@ export const setStageEventHandlers = ({
setLastMouseDownPos(null); setLastMouseDownPos(null);
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region mousemove //#region mousemove
@ -433,7 +396,7 @@ export const setStageEventHandlers = ({
} }
} }
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region mouseleave //#region mouseleave
@ -462,7 +425,7 @@ export const setStageEventHandlers = ({
} }
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region wheel //#region wheel
@ -477,9 +440,9 @@ export const setStageEventHandlers = ({
} }
// Holding ctrl or meta while scrolling changes the brush size // Holding ctrl or meta while scrolling changes the brush size
if (toolState.selected === 'brush') { if (toolState.selected === 'brush') {
onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta)); onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta));
} else if (toolState.selected === 'eraser') { } else if (toolState.selected === 'eraser') {
onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta)); onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta));
} }
} else { } else {
// We need the absolute cursor position - not the scaled position // We need the absolute cursor position - not the scaled position
@ -503,11 +466,11 @@ export const setStageEventHandlers = ({
stage.scaleY(newScale); stage.scaleY(newScale);
stage.position(newPos); stage.position(newPos);
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
manager.renderers.renderBackground(); manager.konvaApi.renderBackground();
manager.renderers.renderDocumentOverlay(); manager.konvaApi.renderDocumentOverlay();
} }
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region dragmove //#region dragmove
@ -519,9 +482,9 @@ export const setStageEventHandlers = ({
height: stage.height(), height: stage.height(),
scale: stage.scaleX(), scale: stage.scaleX(),
}); });
manager.renderers.renderBackground(); manager.konvaApi.renderBackground();
manager.renderers.renderDocumentOverlay(); manager.konvaApi.renderDocumentOverlay();
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region dragend //#region dragend
@ -534,7 +497,7 @@ export const setStageEventHandlers = ({
height: stage.height(), height: stage.height(),
scale: stage.scaleX(), scale: stage.scaleX(),
}); });
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}); });
//#region key //#region key
@ -555,12 +518,12 @@ export const setStageEventHandlers = ({
setTool('view'); setTool('view');
setSpaceKey(true); setSpaceKey(true);
} else if (e.key === 'r') { } else if (e.key === 'r') {
manager.renderers.fitDocumentToStage(); manager.konvaApi.fitDocumentToStage();
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
manager.renderers.renderBackground(); manager.konvaApi.renderBackground();
manager.renderers.renderDocumentOverlay(); manager.konvaApi.renderDocumentOverlay();
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}; };
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
@ -578,7 +541,7 @@ export const setStageEventHandlers = ({
setToolBuffer(null); setToolBuffer(null);
setSpaceKey(false); setSpaceKey(false);
} }
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
}; };
window.addEventListener('keyup', onKeyUp); 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 Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
export type BrushLineObjectRecord = { export type BrushLineObjectRecord = {
@ -36,7 +53,7 @@ export type ImageObjectRecord = {
type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord;
type KonvaRenderers = { type KonvaApi = {
renderRegions: () => void; renderRegions: () => void;
renderLayers: () => void; renderLayers: () => void;
renderControlAdapters: () => void; renderControlAdapters: () => void;
@ -45,8 +62,9 @@ type KonvaRenderers = {
renderDocumentOverlay: () => void; renderDocumentOverlay: () => void;
renderBackground: () => void; renderBackground: () => void;
renderToolPreview: () => void; renderToolPreview: () => void;
fitDocumentToStage: () => void;
arrangeEntities: () => void; arrangeEntities: () => void;
fitDocumentToStage: () => void;
fitStageToContainer: () => void;
}; };
type BackgroundLayer = { 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 { export class KonvaNodeManager {
stage: Konva.Stage; stage: Konva.Stage;
container: HTMLDivElement;
adapters: Map<string, KonvaEntityAdapter>; adapters: Map<string, KonvaEntityAdapter>;
_background: BackgroundLayer | null; _background: BackgroundLayer | null;
_preview: PreviewLayer | 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.stage = stage;
this._renderers = null; this.container = container;
this._konvaApi = null;
this._preview = null; this._preview = null;
this._background = null; this._background = null;
this._stateApi = null;
this.adapters = new Map(); this.adapters = new Map();
} }
@ -121,13 +184,13 @@ export class KonvaNodeManager {
return this.adapters.delete(id); return this.adapters.delete(id);
} }
set renderers(renderers: KonvaRenderers) { set konvaApi(konvaApi: KonvaApi) {
this._renderers = renderers; this._konvaApi = konvaApi;
} }
get renderers(): KonvaRenderers { get konvaApi(): KonvaApi {
assert(this._renderers !== null, 'Konva renderers have not been set'); assert(this._konvaApi !== null, 'Konva API has not been set');
return this._renderers; return this._konvaApi;
} }
set preview(preview: PreviewLayer) { set preview(preview: PreviewLayer) {
@ -147,6 +210,15 @@ export class KonvaNodeManager {
assert(this._background !== null, 'Konva background layer has not been set'); assert(this._background !== null, 'Konva background layer has not been set');
return this._background; 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 { export class KonvaEntityAdapter {

View File

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

View File

@ -38,11 +38,11 @@ export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BA
/** /**
* Gets a render function for the background layer. * 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 * @returns A function to render the background grid
*/ */
export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): void => { export const getRenderBackground = (manager: KonvaNodeManager) => {
const { manager } = arg; function renderBackground(): void {
const background = manager.background.layer; const background = manager.background.layer;
background.zIndex(0); background.zIndex(0);
const scale = manager.stage.scaleX(); const scale = manager.stage.scaleX();
@ -116,4 +116,7 @@ export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): v
}) })
); );
} }
}
return renderBackground;
}; };

View File

@ -6,16 +6,11 @@ import {
createObjectGroup, createObjectGroup,
updateImageSource, updateImageSource,
} from 'features/controlLayers/konva/renderers/objects'; } 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 Konva from 'konva';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { assert } from 'tsafe'; 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. * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist.
* @param manager The konva node manager * @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. * Gets a function to render all control adapters.
* @param manager The konva node manager * @param manager The konva node manager
* @param getControlAdapterEntityStates A function to get all control adapter entities
* @returns A function to render all control adapters * @returns A function to render all control adapters
*/ */
export const getRenderControlAdapters = export const getRenderControlAdapters = (manager: KonvaNodeManager) => {
(arg: { const { getControlAdapterEntityStates } = manager.stateApi;
manager: KonvaNodeManager;
getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; function renderControlAdapters(): void {
}) =>
(): void => {
const { manager, getControlAdapterEntityStates } = arg;
const entities = getControlAdapterEntityStates(); const entities = getControlAdapterEntityStates();
// Destroy nonexistent layers // Destroy nonexistent layers
for (const adapters of manager.getAll('control_adapter')) { for (const adapters of manager.getAll('control_adapter')) {
@ -122,4 +113,7 @@ export const getRenderControlAdapters =
for (const entity of entities) { for (const entity of entities) {
renderControlAdapter(manager, entity); renderControlAdapter(manager, entity);
} }
}
return renderControlAdapters;
}; };

View File

@ -16,7 +16,7 @@ import {
getRectShape, getRectShape,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import { mapId } from 'features/controlLayers/konva/util'; 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'; 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 manager The konva node manager
* @param getEntityState A function to get the inpaint mask entity state * @returns A function to render the inpaint mask
* @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
*/ */
export const getRenderInpaintMask = export const getRenderInpaintMask = (manager: KonvaNodeManager) => {
(arg: { const { getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi;
manager: KonvaNodeManager;
getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; function renderInpaintMask(): void {
getMaskOpacity: () => number;
getToolState: () => CanvasV2State['tool'];
getSelectedEntity: () => CanvasEntity | null;
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
}) =>
(): void => {
const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg;
const entity = getInpaintMaskEntityState(); const entity = getInpaintMaskEntityState();
const globalMaskLayerOpacity = getMaskOpacity(); const globalMaskLayerOpacity = getMaskOpacity();
const toolState = getToolState(); const toolState = getToolState();
@ -228,4 +216,7 @@ export const getRenderInpaintMask =
// } else { // } else {
// bboxRect.visible(false); // bboxRect.visible(false);
// } // }
}
return renderInpaintMask;
}; };

View File

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

View File

@ -19,9 +19,9 @@ import {
PREVIEW_TOOL_GROUP_ID, PREVIEW_TOOL_GROUP_ID,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; 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 Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
/** /**
@ -245,13 +245,12 @@ const NO_ANCHORS: string[] = [];
/** /**
* Gets the bbox render function. * Gets the bbox render function.
* @param manager The konva node manager * @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 * @returns The bbox render function
*/ */
export const getRenderBbox = export const getRenderBbox = (manager: KonvaNodeManager) => {
(manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) => const { getBbox, getToolState } = manager.stateApi;
(): void => {
return (): void => {
const bbox = getBbox(); const bbox = getBbox();
const toolState = getToolState(); const toolState = getToolState();
manager.preview.bbox.group.listening(toolState.selected === 'bbox'); manager.preview.bbox.group.listening(toolState.selected === 'bbox');
@ -270,6 +269,7 @@ export const getRenderBbox =
enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS,
}); });
}; };
};
/** /**
* Gets the tool preview konva nodes. * Gets the tool preview konva nodes.
@ -328,30 +328,11 @@ export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] =>
/** /**
* Gets the tool preview (brush, eraser, rect) render function. * Gets the tool preview (brush, eraser, rect) render function.
* @param arg.manager The konva node manager * @param 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
* @returns The tool preview render function * @returns The tool preview render function
*/ */
export const getRenderToolPreview = export const getRenderToolPreview = (manager: KonvaNodeManager) => {
(arg: {
manager: KonvaNodeManager;
getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
getSelectedEntity: () => CanvasEntity | null;
getLastCursorPos: () => Vector2d | null;
getLastMouseDownPos: () => Vector2d | null;
getIsDrawing: () => boolean;
getIsMouseDown: () => boolean;
}) =>
(): void => {
const { const {
manager,
getToolState, getToolState,
getCurrentFill, getCurrentFill,
getSelectedEntity, getSelectedEntity,
@ -359,8 +340,9 @@ export const getRenderToolPreview =
getLastMouseDownPos, getLastMouseDownPos,
getIsDrawing, getIsDrawing,
getIsMouseDown, getIsMouseDown,
} = arg; } = manager.stateApi;
return (): void => {
const stage = manager.stage; const stage = manager.stage;
const layerCount = manager.adapters.size; const layerCount = manager.adapters.size;
const toolState = getToolState(); 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 * 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. * Gets the document overlay render function.
* @param arg.manager The konva node manager * @param manager The konva node manager
* @param arg.getDocument A function to get the document state
* @returns The document overlay render function * @returns The document overlay render function
*/ */
export const getRenderDocumentOverlay = export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => {
(arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => { const { getDocument } = manager.stateApi;
const { manager, getDocument } = arg;
function renderDocumentOverlay(): void {
const document = getDocument(); const document = getDocument();
const stage = manager.stage; const stage = manager.stage;
@ -524,4 +507,7 @@ export const getRenderDocumentOverlay =
width: document.width, width: document.width,
height: document.height, height: document.height,
}); });
}
return renderDocumentOverlay;
}; };

View File

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

View File

@ -21,7 +21,7 @@ import {
getRenderToolPreview, getRenderToolPreview,
} from 'features/controlLayers/konva/renderers/preview'; } from 'features/controlLayers/konva/renderers/preview';
import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; 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 { import {
$stageAttrs, $stageAttrs,
bboxChanged, bboxChanged,
@ -283,7 +283,7 @@ export const initializeRenderer = (
spaceKey = val; spaceKey = val;
}; };
const manager = new KonvaNodeManager(stage); const manager = new KonvaNodeManager(stage, container);
$nodeManager.set(manager); $nodeManager.set(manager);
manager.background = { layer: createBackgroundLayer() }; 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.tool.group);
manager.preview.layer.add(manager.preview.documentOverlay.group); manager.preview.layer.add(manager.preview.documentOverlay.group);
manager.stage.add(manager.preview.layer); manager.stage.add(manager.preview.layer);
manager.stateApi = {
const cleanupListeners = setStageEventHandlers({ // Read-only state
manager,
getToolState, 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, setTool,
setToolBuffer, setToolBuffer,
getIsDrawing, getIsDrawing,
setIsDrawing, setIsDrawing,
getIsMouseDown, getIsMouseDown,
setIsMouseDown, setIsMouseDown,
getSelectedEntity,
getLastAddedPoint, getLastAddedPoint,
setLastAddedPoint, setLastAddedPoint,
getLastCursorPos, getLastCursorPos,
@ -318,61 +332,37 @@ export const initializeRenderer = (
getSpaceKey, getSpaceKey,
setSpaceKey, setSpaceKey,
setStageAttrs: $stageAttrs.set, setStageAttrs: $stageAttrs.set,
getBbox,
getSettings, // Callbacks
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
onBrushWidthChanged, onBrushWidthChanged,
onEraserWidthChanged, 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. // 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 // 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. // the entire state over when needed.
const debouncedUpdateBboxes = debounce(updateBboxes, 300); const debouncedUpdateBboxes = debounce(updateBboxes, 300);
manager.renderers = { manager.konvaApi = {
renderRegions: getRenderRegions({ renderRegions: getRenderRegions(manager),
manager, renderLayers: getRenderLayers(manager),
getRegionEntityStates, renderControlAdapters: getRenderControlAdapters(manager),
getMaskOpacity, renderInpaintMask: getRenderInpaintMask(manager),
getToolState, renderBbox: getRenderBbox(manager),
getSelectedEntity, renderToolPreview: getRenderToolPreview(manager),
onPosChanged, renderDocumentOverlay: getRenderDocumentOverlay(manager),
}), renderBackground: getRenderBackground(manager),
renderLayers: getRenderLayers({ manager, getLayerEntityStates, getToolState, onPosChanged }), arrangeEntities: getArrangeEntities(manager),
renderControlAdapters: getRenderControlAdapters({ manager, getControlAdapterEntityStates }), fitDocumentToStage: getFitDocumentToStage(manager),
renderInpaintMask: getRenderInpaintMask({ fitStageToContainer: getFitStageToContainer(manager),
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,
}),
}; };
const renderCanvas = () => { const renderCanvas = () => {
@ -392,7 +382,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering layers'); logIfDebugging('Rendering layers');
manager.renderers.renderLayers(); manager.konvaApi.renderLayers();
} }
if ( if (
@ -402,7 +392,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering regions'); logIfDebugging('Rendering regions');
manager.renderers.renderRegions(); manager.konvaApi.renderRegions();
} }
if ( if (
@ -412,22 +402,22 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering inpaint mask'); logIfDebugging('Rendering inpaint mask');
manager.renderers.renderInpaintMask(); manager.konvaApi.renderInpaintMask();
} }
if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) {
logIfDebugging('Rendering control adapters'); logIfDebugging('Rendering control adapters');
manager.renderers.renderControlAdapters(); manager.konvaApi.renderControlAdapters();
} }
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
logIfDebugging('Rendering document bounds overlay'); logIfDebugging('Rendering document bounds overlay');
manager.renderers.renderDocumentOverlay(); manager.konvaApi.renderDocumentOverlay();
} }
if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) {
logIfDebugging('Rendering generation bbox'); logIfDebugging('Rendering generation bbox');
manager.renderers.renderBbox(); manager.konvaApi.renderBbox();
} }
if ( if (
@ -447,7 +437,7 @@ export const initializeRenderer = (
canvasV2.regions.entities !== prevCanvasV2.regions.entities canvasV2.regions.entities !== prevCanvasV2.regions.entities
) { ) {
logIfDebugging('Arranging entities'); logIfDebugging('Arranging entities');
manager.renderers.arrangeEntities(); manager.konvaApi.arrangeEntities();
} }
prevCanvasV2 = canvasV2; 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 // 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. // document bounds overlay when the stage is resized.
const fitStageToContainer = () => { const resizeObserver = new ResizeObserver(manager.konvaApi.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);
resizeObserver.observe(container); resizeObserver.observe(container);
fitStageToContainer(); manager.konvaApi.fitStageToContainer();
const unsubscribeRenderer = subscribe(renderCanvas); const unsubscribeRenderer = subscribe(renderCanvas);
logIfDebugging('First render of konva stage'); logIfDebugging('First render of konva stage');
// On first render, the document should be fit to the stage. // On first render, the document should be fit to the stage.
manager.renderers.fitDocumentToStage(); manager.konvaApi.fitDocumentToStage();
manager.renderers.renderToolPreview(); manager.konvaApi.renderToolPreview();
renderCanvas(); renderCanvas();
return () => { return () => {

View File

@ -1,23 +1,15 @@
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; 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%. * 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. * 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 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
* @returns A function to fit the document to the stage * @returns A function to fit the document to the stage
*/ */
export const getFitDocumentToStage = export const getFitDocumentToStage = (manager: KonvaNodeManager) => {
(arg: { function fitDocumentToStage(): void {
manager: KonvaNodeManager; const { getDocument, setStageAttrs } = manager.stateApi;
getDocument: () => CanvasV2State['document'];
setStageAttrs: (stageAttrs: StageAttrs) => void;
}) =>
(): void => {
const { manager, getDocument, setStageAttrs } = arg;
const document = getDocument(); const document = getDocument();
// Fit & center the document on the stage // Fit & center the document on the stage
const width = manager.stage.width(); const width = manager.stage.width();
@ -29,4 +21,32 @@ export const getFitDocumentToStage =
const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale;
manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
setStageAttrs({ x, y, width, height, 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;
}; };