feat(ui): revised docstrings for renderers & simplified api

This commit is contained in:
psychedelicious 2024-06-21 14:14:09 +10:00
parent 196779ff19
commit a2ef8d9d47
11 changed files with 760 additions and 644 deletions

View File

@ -1,11 +1,4 @@
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background';
import {
renderDocumentBoundsOverlay,
renderToolPreview,
scaleToolPreview,
} from 'features/controlLayers/konva/renderers/preview';
import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type { import type {
BrushLineAddedArg, BrushLineAddedArg,
@ -52,7 +45,6 @@ type Arg = {
getSelectedEntity: () => CanvasEntity | null; getSelectedEntity: () => CanvasEntity | null;
getSpaceKey: () => boolean; getSpaceKey: () => boolean;
setSpaceKey: (val: boolean) => void; setSpaceKey: (val: boolean) => void;
getDocument: () => CanvasV2State['document'];
getBbox: () => CanvasV2State['bbox']; getBbox: () => CanvasV2State['bbox'];
getSettings: () => CanvasV2State['settings']; getSettings: () => CanvasV2State['settings'];
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
@ -160,7 +152,6 @@ export const setStageEventHandlers = ({
getSelectedEntity, getSelectedEntity,
getSpaceKey, getSpaceKey,
setSpaceKey, setSpaceKey,
getDocument,
getBbox, getBbox,
getSettings, getSettings,
onBrushLineAdded, onBrushLineAdded,
@ -176,16 +167,7 @@ export const setStageEventHandlers = ({
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');
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region mousedown //#region mousedown
@ -306,16 +288,7 @@ export const setStageEventHandlers = ({
setLastAddedPoint(pos); setLastAddedPoint(pos);
} }
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region mouseup //#region mouseup
@ -354,16 +327,7 @@ export const setStageEventHandlers = ({
setLastMouseDownPos(null); setLastMouseDownPos(null);
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region mousemove //#region mousemove
@ -469,17 +433,7 @@ export const setStageEventHandlers = ({
} }
} }
} }
manager.renderers.renderToolPreview();
renderToolPreview(
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region mouseleave //#region mouseleave
@ -508,16 +462,7 @@ export const setStageEventHandlers = ({
} }
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region wheel //#region wheel
@ -558,21 +503,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 });
renderBackgroundLayer(manager); manager.renderers.renderBackground();
scaleToolPreview(manager, getToolState()); manager.renderers.renderDocumentOverlay();
renderDocumentBoundsOverlay(manager, getDocument);
} }
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region dragmove //#region dragmove
@ -584,18 +519,9 @@ export const setStageEventHandlers = ({
height: stage.height(), height: stage.height(),
scale: stage.scaleX(), scale: stage.scaleX(),
}); });
renderBackgroundLayer(manager); manager.renderers.renderBackground();
renderDocumentBoundsOverlay(manager, getDocument); manager.renderers.renderDocumentOverlay();
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region dragend //#region dragend
@ -608,16 +534,7 @@ export const setStageEventHandlers = ({
height: stage.height(), height: stage.height(),
scale: stage.scaleX(), scale: stage.scaleX(),
}); });
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}); });
//#region key //#region key
@ -638,22 +555,12 @@ export const setStageEventHandlers = ({
setTool('view'); setTool('view');
setSpaceKey(true); setSpaceKey(true);
} else if (e.key === 'r') { } else if (e.key === 'r') {
const stageAttrs = fitDocumentToStage(stage, getDocument()); manager.renderers.fitDocumentToStage();
setStageAttrs(stageAttrs); manager.renderers.renderToolPreview();
scaleToolPreview(manager, getToolState()); manager.renderers.renderBackground();
renderBackgroundLayer(manager); manager.renderers.renderDocumentOverlay();
renderDocumentBoundsOverlay(manager, getDocument);
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}; };
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
@ -671,16 +578,7 @@ export const setStageEventHandlers = ({
setToolBuffer(null); setToolBuffer(null);
setSpaceKey(false); setSpaceKey(false);
} }
renderToolPreview( manager.renderers.renderToolPreview();
manager,
getToolState(),
getCurrentFill(),
getSelectedEntity(),
getLastCursorPos(),
getLastMouseDownPos(),
getIsDrawing(),
getIsMouseDown()
);
}; };
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', onKeyUp);

View File

@ -1,20 +1,6 @@
import { createBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types';
import {
createBboxPreview,
createDocumentOverlay,
createPreviewLayer,
createToolPreview,
} from 'features/controlLayers/konva/renderers/preview';
import type {
BrushLine,
CanvasEntity,
CanvasV2State,
EraserLine,
ImageObject,
Rect,
RectShape,
} from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import { assert } from 'tsafe';
export type BrushLineObjectRecord = { export type BrushLineObjectRecord = {
id: string; id: string;
@ -50,61 +36,62 @@ export type ImageObjectRecord = {
type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord;
type KonvaRenderers = {
renderRegions: () => void;
renderLayers: () => void;
renderControlAdapters: () => void;
renderInpaintMask: () => void;
renderBbox: () => void;
renderDocumentOverlay: () => void;
renderBackground: () => void;
renderToolPreview: () => void;
fitDocumentToStage: () => void;
arrangeEntities: () => void;
};
type BackgroundLayer = {
layer: Konva.Layer;
};
type PreviewLayer = {
layer: Konva.Layer;
bbox: {
group: Konva.Group;
rect: Konva.Rect;
transformer: Konva.Transformer;
};
tool: {
group: Konva.Group;
brush: {
group: Konva.Group;
fill: Konva.Circle;
innerBorder: Konva.Circle;
outerBorder: Konva.Circle;
};
rect: {
rect: Konva.Rect;
};
};
documentOverlay: {
group: Konva.Group;
innerRect: Konva.Rect;
outerRect: Konva.Rect;
};
};
export class KonvaNodeManager { export class KonvaNodeManager {
stage: Konva.Stage; stage: Konva.Stage;
adapters: Map<string, KonvaEntityAdapter>; adapters: Map<string, KonvaEntityAdapter>;
background: { layer: Konva.Layer }; _background: BackgroundLayer | null;
preview: { _preview: PreviewLayer | null;
layer: Konva.Layer; _renderers: KonvaRenderers | null;
bbox: {
group: Konva.Group;
rect: Konva.Rect;
transformer: Konva.Transformer;
};
tool: {
group: Konva.Group;
brush: {
group: Konva.Group;
fill: Konva.Circle;
innerBorder: Konva.Circle;
outerBorder: Konva.Circle;
};
rect: {
rect: Konva.Rect;
};
};
documentOverlay: {
group: Konva.Group;
innerRect: Konva.Rect;
outerRect: Konva.Rect;
};
};
constructor( constructor(stage: Konva.Stage) {
stage: Konva.Stage,
getBbox: () => CanvasV2State['bbox'],
onBboxTransformed: (bbox: Rect) => void,
getShiftKey: () => boolean,
getCtrlKey: () => boolean,
getMetaKey: () => boolean,
getAltKey: () => boolean
) {
this.stage = stage; this.stage = stage;
this._renderers = null;
this._preview = null;
this._background = null;
this.adapters = new Map(); this.adapters = new Map();
this.background = { layer: createBackgroundLayer() };
this.stage.add(this.background.layer);
this.preview = {
layer: createPreviewLayer(),
bbox: createBboxPreview(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey),
tool: createToolPreview(stage),
documentOverlay: createDocumentOverlay(),
};
this.preview.layer.add(this.preview.bbox.group);
this.preview.layer.add(this.preview.tool.group);
this.preview.layer.add(this.preview.documentOverlay.group);
this.stage.add(this.preview.layer);
} }
add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter { add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter {
@ -133,6 +120,33 @@ export class KonvaNodeManager {
adapter.konvaLayer.destroy(); adapter.konvaLayer.destroy();
return this.adapters.delete(id); return this.adapters.delete(id);
} }
set renderers(renderers: KonvaRenderers) {
this._renderers = renderers;
}
get renderers(): KonvaRenderers {
assert(this._renderers !== null, 'Konva renderers have not been set');
return this._renderers;
}
set preview(preview: PreviewLayer) {
this._preview = preview;
}
get preview(): PreviewLayer {
assert(this._preview !== null, 'Konva preview layer has not been set');
return this._preview;
}
set background(background: BackgroundLayer) {
this._background = background;
}
get background(): BackgroundLayer {
assert(this._background !== null, 'Konva background layer has not been set');
return this._background;
}
} }
export class KonvaEntityAdapter { export class KonvaEntityAdapter {

View File

@ -1,23 +1,37 @@
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; import type { CanvasV2State } from 'features/controlLayers/store/types';
export const arrangeEntities = ( /**
manager: KonvaNodeManager, * Gets a function to arrange the entities in the konva stage.
layers: LayerEntity[], * @param manager The konva node manager
controlAdapters: ControlAdapterEntity[], * @param getLayerEntityStates A function to get all layer entity states
regions: RegionEntity[] * @param getControlAdapterEntityStates A function to get all control adapter entity states
): void => { * @param getRegionEntityStates A function to get all region entity states
let zIndex = 0; * @returns An arrange entities function
manager.background.layer.zIndex(++zIndex); */
for (const layer of layers) { export const getArrangeEntities =
manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); (arg: {
} manager: KonvaNodeManager;
for (const ca of controlAdapters) { getLayerEntityStates: () => CanvasV2State['layers']['entities'];
manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities'];
} getRegionEntityStates: () => CanvasV2State['regions']['entities'];
for (const rg of regions) { }) =>
manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); (): void => {
} const { manager, getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = arg;
manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); const layers = getLayerEntityStates();
manager.preview.layer.zIndex(++zIndex); const controlAdapters = getControlAdapterEntityStates();
}; const regions = getRegionEntityStates();
let zIndex = 0;
manager.background.layer.zIndex(++zIndex);
for (const layer of layers) {
manager.get(layer.id)?.konvaLayer.zIndex(++zIndex);
}
for (const ca of controlAdapters) {
manager.get(ca.id)?.konvaLayer.zIndex(++zIndex);
}
for (const rg of regions) {
manager.get(rg.id)?.konvaLayer.zIndex(++zIndex);
}
manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex);
manager.preview.layer.zIndex(++zIndex);
};

View File

@ -6,6 +6,11 @@ import Konva from 'konva';
const baseGridLineColor = getArbitraryBaseColor(27); const baseGridLineColor = getArbitraryBaseColor(27);
const fineGridLineColor = getArbitraryBaseColor(18); const fineGridLineColor = getArbitraryBaseColor(18);
/**
* Gets the grid spacing. 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
*/
const getGridSpacing = (scale: number): number => { const getGridSpacing = (scale: number): number => {
if (scale >= 2) { if (scale >= 2) {
return 8; return 8;
@ -25,9 +30,19 @@ const getGridSpacing = (scale: number): number => {
return 256; return 256;
}; };
/**
* Creates the background konva layer.
* @returns The background konva layer
*/
export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false }); export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false });
export const renderBackgroundLayer = (manager: KonvaNodeManager): void => { /**
* Gets a render function for the background layer.
* @param arg.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; const background = manager.background.layer;
background.zIndex(0); background.zIndex(0);
const scale = manager.stage.scaleX(); const scale = manager.stage.scaleX();

View File

@ -6,7 +6,7 @@ import {
createObjectGroup, createObjectGroup,
updateImageSource, updateImageSource,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import type { CanvasV2State, 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';
@ -17,8 +17,8 @@ import { assert } from 'tsafe';
*/ */
/** /**
* Creates a control adapter layer. * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist.
* @param stage The konva stage * @param manager The konva node manager
* @param entity The control adapter layer state * @param entity The control adapter layer state
*/ */
const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => { const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => {
@ -37,11 +37,9 @@ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEnti
}; };
/** /**
* Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated * Renders a control adapter.
* with the current image source and attributes. * @param manager The konva node manager
* @param stage The konva stage * @param entity The control adapter entity state
* @param entity The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/ */
export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise<void> => { export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise<void> => {
const adapter = getControlAdapter(manager, entity); const adapter = getControlAdapter(manager, entity);
@ -101,14 +99,27 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co
} }
}; };
export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { /**
// Destroy nonexistent layers * Gets a function to render all control adapters.
for (const adapters of manager.getAll('control_adapter')) { * @param manager The konva node manager
if (!entities.find((ca) => ca.id === adapters.id)) { * @param getControlAdapterEntityStates A function to get all control adapter entities
manager.destroy(adapters.id); * @returns A function to render all control adapters
*/
export const getRenderControlAdapters =
(arg: {
manager: KonvaNodeManager;
getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities'];
}) =>
(): void => {
const { manager, getControlAdapterEntityStates } = arg;
const entities = getControlAdapterEntityStates();
// Destroy nonexistent layers
for (const adapters of manager.getAll('control_adapter')) {
if (!entities.find((ca) => ca.id === adapters.id)) {
manager.destroy(adapters.id);
}
} }
} for (const entity of entities) {
for (const entity of entities) { renderControlAdapter(manager, entity);
renderControlAdapter(manager, entity); }
} };
};

View File

@ -16,24 +16,11 @@ 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 { import type { CanvasEntity, CanvasV2State, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types';
CanvasEntity,
CanvasEntityIdentifier,
InpaintMaskEntity,
PosChangedArg,
Tool,
} from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
/** /**
* Logic for creating and rendering regional guidance layers. * Creates the "compositing rect" for the inpaint mask.
*
* Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments
* in `renderRGLayer`.
*/
/**
* Creates the "compositing rect" for a regional guidance layer.
* @param konvaLayer The konva layer * @param konvaLayer The konva layer
*/ */
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
@ -43,23 +30,24 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
}; };
/** /**
* Creates a regional guidance layer. * Gets the singleton inpaint mask entity's konva nodes and entity adapter, creating them if they do not exist.
* @param stage The konva stage * @param manager The konva node manager
* @param entity The regional guidance layer state * @param entityState The inpaint mask entity state
* @param onLayerPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged)
* @returns The konva entity adapter for the inpaint mask
*/ */
const getInpaintMask = ( const getInpaintMask = (
manager: KonvaNodeManager, manager: KonvaNodeManager,
entity: InpaintMaskEntity, entityState: InpaintMaskEntity,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): KonvaEntityAdapter => { ): KonvaEntityAdapter => {
const adapter = manager.get(entity.id); const adapter = manager.get(entityState.id);
if (adapter) { if (adapter) {
return adapter; return adapter;
} }
// This layer hasn't been added to the konva state yet // This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: entity.id, id: entityState.id,
name: INPAINT_MASK_LAYER_NAME, name: INPAINT_MASK_LAYER_NAME,
draggable: true, draggable: true,
dragDistance: 0, dragDistance: 0,
@ -69,166 +57,175 @@ const getInpaintMask = (
// the position - we do not need to call this on the `dragmove` event. // the position - we do not need to call this on the `dragmove` event.
if (onPosChanged) { if (onPosChanged) {
konvaLayer.on('dragend', function (e) { konvaLayer.on('dragend', function (e) {
onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); onPosChanged({ id: entityState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask');
}); });
} }
const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME); const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME);
return manager.add(entity, konvaLayer, konvaObjectGroup); return manager.add(entityState, konvaLayer, konvaObjectGroup);
}; };
/** /**
* Renders a raster layer. * Gets the inpaint mask render function.
* @param stage The konva stage * @param manager The konva node manager
* @param entity The regional guidance layer state * @param getEntityState A function to get the inpaint mask entity state
* @param globalMaskLayerOpacity The global mask layer opacity * @param getMaskOpacity A function to get the mask opacity
* @param tool The current tool * @param getToolState A function to get the tool state
* @param onPosChanged Callback for when the layer's position changes * @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 renderInpaintMask = ( export const getRenderInpaintMask =
manager: KonvaNodeManager, (arg: {
entity: InpaintMaskEntity, manager: KonvaNodeManager;
globalMaskLayerOpacity: number, getInpaintMaskEntityState: () => CanvasV2State['inpaintMask'];
tool: Tool, getMaskOpacity: () => number;
selectedEntityIdentifier: CanvasEntityIdentifier | null, getToolState: () => CanvasV2State['tool'];
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void getSelectedEntity: () => CanvasEntity | null;
): void => { onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
const adapter = getInpaintMask(manager, entity, onPosChanged); }) =>
(): void => {
const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg;
const entity = getInpaintMaskEntityState();
const globalMaskLayerOpacity = getMaskOpacity();
const toolState = getToolState();
const selectedEntity = getSelectedEntity();
const adapter = getInpaintMask(manager, entity, onPosChanged);
// Update the layer's position and listening state // Update the layer's position and listening state
adapter.konvaLayer.setAttrs({ adapter.konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events listening: toolState.selected === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(entity.x), x: Math.floor(entity.x),
y: Math.floor(entity.y), y: Math.floor(entity.y),
}); });
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(entity.fill); const rgbColor = rgbColorToString(entity.fill);
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
let groupNeedsCache = false; let groupNeedsCache = false;
const objectIds = entity.objects.map(mapId); const objectIds = entity.objects.map(mapId);
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const objectRecord of adapter.getAll()) { for (const objectRecord of adapter.getAll()) {
if (!objectIds.includes(objectRecord.id)) { if (!objectIds.includes(objectRecord.id)) {
adapter.destroy(objectRecord.id); adapter.destroy(objectRecord.id);
groupNeedsCache = true;
}
}
for (const obj of entity.objects) {
if (obj.type === 'brush_line') {
const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (objectRecord.konvaLine.points().length !== obj.points.length) {
objectRecord.konvaLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (objectRecord.konvaLine.stroke() !== rgbColor) {
objectRecord.konvaLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (obj.type === 'eraser_line') {
const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (objectRecord.konvaLine.points().length !== obj.points.length) {
objectRecord.konvaLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (objectRecord.konvaLine.stroke() !== rgbColor) {
objectRecord.konvaLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (obj.type === 'rect_shape') {
const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME);
// Only update the color if it has changed.
if (objectRecord.konvaRect.fill() !== rgbColor) {
objectRecord.konvaRect.fill(rgbColor);
groupNeedsCache = true;
}
}
}
// Only update layer visibility if it has changed.
if (adapter.konvaLayer.visible() !== entity.isEnabled) {
adapter.konvaLayer.visible(entity.isEnabled);
groupNeedsCache = true; groupNeedsCache = true;
} }
}
for (const obj of entity.objects) { if (adapter.konvaObjectGroup.getChildren().length === 0) {
if (obj.type === 'brush_line') { // No objects - clear the cache to reset the previous pixel data
const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (objectRecord.konvaLine.points().length !== obj.points.length) {
objectRecord.konvaLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (objectRecord.konvaLine.stroke() !== rgbColor) {
objectRecord.konvaLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (obj.type === 'eraser_line') {
const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (objectRecord.konvaLine.points().length !== obj.points.length) {
objectRecord.konvaLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (objectRecord.konvaLine.stroke() !== rgbColor) {
objectRecord.konvaLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (obj.type === 'rect_shape') {
const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME);
// Only update the color if it has changed.
if (objectRecord.konvaRect.fill() !== rgbColor) {
objectRecord.konvaRect.fill(rgbColor);
groupNeedsCache = true;
}
}
}
// Only update layer visibility if it has changed.
if (adapter.konvaLayer.visible() !== entity.isEnabled) {
adapter.konvaLayer.visible(entity.isEnabled);
groupNeedsCache = true;
}
if (adapter.konvaObjectGroup.getChildren().length === 0) {
// No objects - clear the cache to reset the previous pixel data
adapter.konvaObjectGroup.clearCache();
return;
}
const compositingRect =
adapter.konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer);
const isSelected = selectedEntityIdentifier?.id === entity.id;
/**
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
* shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
*
* Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
* effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
* Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
*
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
* a single raster image, and _then_ applied the 50% opacity.
*/
if (isSelected && tool !== 'move') {
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
if (adapter.konvaObjectGroup.isCached()) {
adapter.konvaObjectGroup.clearCache(); adapter.konvaObjectGroup.clearCache();
return;
} }
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
adapter.konvaObjectGroup.opacity(1);
compositingRect.setAttrs({ const compositingRect =
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already adapter.konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer);
...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), const isSelected = selectedEntity?.id === entity.id;
fill: rgbColor,
opacity: globalMaskLayerOpacity, /**
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
globalCompositeOperation: 'source-in', * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
visible: true, *
// This rect must always be on top of all other shapes * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
zIndex: adapter.konvaObjectGroup.getChildren().length, * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
}); * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
} else { *
// The compositing rect should only be shown when the layer is selected. * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
compositingRect.visible(false); * a single raster image, and _then_ applied the 50% opacity.
// Cache only if needed - or if we are on this code path and _don't_ have a cache */
if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { if (isSelected && toolState.selected !== 'move') {
adapter.konvaObjectGroup.cache(); // We must clear the cache first so Konva will re-draw the group with the new compositing rect
if (adapter.konvaObjectGroup.isCached()) {
adapter.konvaObjectGroup.clearCache();
}
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
adapter.konvaObjectGroup.opacity(1);
compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)),
fill: rgbColor,
opacity: globalMaskLayerOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
globalCompositeOperation: 'source-in',
visible: true,
// This rect must always be on top of all other shapes
zIndex: adapter.konvaObjectGroup.getChildren().length,
});
} else {
// The compositing rect should only be shown when the layer is selected.
compositingRect.visible(false);
// Cache only if needed - or if we are on this code path and _don't_ have a cache
if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) {
adapter.konvaObjectGroup.cache();
}
// Updating group opacity does not require re-caching
adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity);
} }
// Updating group opacity does not require re-caching
adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity);
}
// const bboxRect = // const bboxRect =
// regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); // regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer);
// if (rg.bbox) { // if (rg.bbox) {
// const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move';
// bboxRect.setAttrs({ // bboxRect.setAttrs({
// visible: active, // visible: active,
// listening: active, // listening: active,
// x: rg.bbox.x, // x: rg.bbox.x,
// y: rg.bbox.y, // y: rg.bbox.y,
// width: rg.bbox.width, // width: rg.bbox.width,
// height: rg.bbox.height, // height: rg.bbox.height,
// stroke: isSelected ? BBOX_SELECTED_STROKE : '', // stroke: isSelected ? BBOX_SELECTED_STROKE : '',
// }); // });
// } else { // } else {
// bboxRect.visible(false); // bboxRect.visible(false);
// } // }
}; };

View File

@ -15,7 +15,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, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, CanvasV2State, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
/** /**
@ -23,10 +23,11 @@ import Konva from 'konva';
*/ */
/** /**
* Creates a raster layer. * Gets layer entity's konva nodes and entity adapter, creating them if they do not exist.
* @param stage The konva stage * @param manager The konva node manager
* @param entity The raster layer state * @param entity The layer entity state
* @param onPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
* @returns The konva entity adapter for the layer
*/ */
const getLayer = ( const getLayer = (
manager: KonvaNodeManager, manager: KonvaNodeManager,
@ -58,9 +59,9 @@ const getLayer = (
}; };
/** /**
* Renders a regional guidance layer. * Renders a layer.
* @param stage The konva stage * @param manager The konva node manager
* @param entity The regional guidance layer state * @param entity The layer entity state
* @param tool The current tool * @param tool The current tool
* @param onPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
*/ */
@ -133,19 +134,32 @@ export const renderLayer = async (
adapter.konvaObjectGroup.opacity(entity.opacity); adapter.konvaObjectGroup.opacity(entity.opacity);
}; };
export const renderLayers = ( /**
manager: KonvaNodeManager, * Gets a function to render all layers.
entities: LayerEntity[], * @param manager The konva node manager
tool: Tool, * @param getLayerEntityStates A function to get all layer entities
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void * @param getToolState A function to get the current tool state
): void => { * @param onPosChanged Callback for when the layer's position changes
// Destroy nonexistent layers * @returns A function to render all layers
for (const adapter of manager.getAll('layer')) { */
if (!entities.find((l) => l.id === adapter.id)) { export const getRenderLayers =
manager.destroy(adapter.id); (arg: {
manager: KonvaNodeManager;
getLayerEntityStates: () => CanvasV2State['layers']['entities'];
getToolState: () => CanvasV2State['tool'];
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
}) =>
(): void => {
const { manager, getLayerEntityStates, getToolState, onPosChanged } = arg;
const entities = getLayerEntityStates();
const tool = getToolState();
// Destroy nonexistent layers
for (const adapter of manager.getAll('layer')) {
if (!entities.find((l) => l.id === adapter.id)) {
manager.destroy(adapter.id);
}
} }
} for (const entity of entities) {
for (const entity of entities) { renderLayer(manager, entity, tool.selected, onPosChanged);
renderLayer(manager, entity, tool, onPosChanged); }
} };
};

View File

@ -19,14 +19,29 @@ 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, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, CanvasV2State, RgbaColor } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
/**
* Creates the konva preview layer.
* @returns The konva preview layer
*/
export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true });
export const createBboxPreview = ( /**
* Creates the bbox konva nodes.
* @param stage The konva stage
* @param getBbox A function to get the bbox
* @param onBboxTransformed A callback for when the bbox is transformed
* @param getShiftKey A function to get the shift key state
* @param getCtrlKey A function to get the ctrl key state
* @param getMetaKey A function to get the meta key state
* @param getAltKey A function to get the alt key state
* @returns The bbox nodes
*/
export const createBboxNodes = (
stage: Konva.Stage, stage: Konva.Stage,
getBbox: () => IRect, getBbox: () => IRect,
onBboxTransformed: (bbox: IRect) => void, onBboxTransformed: (bbox: IRect) => void,
@ -227,18 +242,40 @@ const ALL_ANCHORS: string[] = [
const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
const NO_ANCHORS: string[] = []; const NO_ANCHORS: string[] = [];
export const renderBboxPreview = (manager: KonvaNodeManager, bbox: IRect, tool: Tool): void => { /**
manager.preview.bbox.group.listening(tool === 'bbox'); * Gets the bbox render function.
// This updates the bbox during transformation * @param manager The konva node manager
manager.preview.bbox.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); * @param getBbox A function to get the bbox
manager.preview.bbox.transformer.setAttrs({ * @param getToolState A function to get the tool state
listening: tool === 'bbox', * @returns The bbox render function
enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, */
}); export const getRenderBbox =
}; (manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) =>
(): void => {
const bbox = getBbox();
const toolState = getToolState();
manager.preview.bbox.group.listening(toolState.selected === 'bbox');
// This updates the bbox during transformation
manager.preview.bbox.rect.setAttrs({
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
scaleX: 1,
scaleY: 1,
listening: toolState.selected === 'bbox',
});
manager.preview.bbox.transformer.setAttrs({
listening: toolState.selected === 'bbox',
enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS,
});
};
export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview']['tool'] => { /**
const scale = stage.scaleX(); * Gets the tool preview konva nodes.
* @returns The tool preview konva nodes
*/
export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] => {
const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID });
// Create the brush preview group & circles // Create the brush preview group & circles
@ -253,7 +290,7 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview
id: PREVIEW_BRUSH_BORDER_INNER_ID, id: PREVIEW_BRUSH_BORDER_INNER_ID,
listening: false, listening: false,
stroke: BRUSH_BORDER_INNER_COLOR, stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true, strokeEnabled: true,
}); });
brushGroup.add(brushBorderInner); brushGroup.add(brushBorderInner);
@ -261,7 +298,7 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview
id: PREVIEW_BRUSH_BORDER_OUTER_ID, id: PREVIEW_BRUSH_BORDER_OUTER_ID,
listening: false, listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR, stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true, strokeEnabled: true,
}); });
brushGroup.add(brushBorderOuter); brushGroup.add(brushBorderOuter);
@ -290,111 +327,138 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview
}; };
/** /**
* Renders the preview layer. * Gets the tool preview (brush, eraser, rect) render function.
* @param stage The konva stage * @param arg.manager The konva node manager
* @param tool The selected tool * @param arg.getToolState The selected tool
* @param currentFill The selected layer's color * @param arg.currentFill The selected layer's color
* @param selectedEntity The selected layer's type * @param arg.selectedEntity The selected layer's type
* @param globalMaskLayerOpacity The global mask layer opacity * @param arg.globalMaskLayerOpacity The global mask layer opacity
* @param cursorPos The cursor position * @param arg.cursorPos The cursor position
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool * @param arg.lastMouseDownPos The position of the last mouse down event - used for the rect tool
* @param brushSize The brush size * @param arg.brushSize The brush size
* @returns The tool preview render function
*/ */
export const renderToolPreview = ( export const getRenderToolPreview =
manager: KonvaNodeManager, (arg: {
toolState: CanvasV2State['tool'], manager: KonvaNodeManager;
currentFill: RgbaColor, getToolState: () => CanvasV2State['tool'];
selectedEntity: CanvasEntity | null, getCurrentFill: () => RgbaColor;
cursorPos: Vector2d | null, getSelectedEntity: () => CanvasEntity | null;
lastMouseDownPos: Vector2d | null, getLastCursorPos: () => Vector2d | null;
isDrawing: boolean, getLastMouseDownPos: () => Vector2d | null;
isMouseDown: boolean getIsDrawing: () => boolean;
): void => { getIsMouseDown: () => boolean;
const stage = manager.stage; }) =>
const layerCount = manager.adapters.size; (): void => {
const tool = toolState.selected; const {
const isDrawableEntity = manager,
selectedEntity?.type === 'regional_guidance' || getToolState,
selectedEntity?.type === 'layer' || getCurrentFill,
selectedEntity?.type === 'inpaint_mask'; getSelectedEntity,
getLastCursorPos,
getLastMouseDownPos,
getIsDrawing,
getIsMouseDown,
} = arg;
// Update the stage's pointer style const stage = manager.stage;
if (tool === 'view') { const layerCount = manager.adapters.size;
// View gets a hand const toolState = getToolState();
stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; const currentFill = getCurrentFill();
} else if (layerCount === 0) { const selectedEntity = getSelectedEntity();
// We have no layers, so we should not render any tool const cursorPos = getLastCursorPos();
stage.container().style.cursor = 'default'; const lastMouseDownPos = getLastMouseDownPos();
} else if (!isDrawableEntity) { const isDrawing = getIsDrawing();
// Non-drawable layers don't have tools const isMouseDown = getIsMouseDown();
stage.container().style.cursor = 'not-allowed'; const tool = toolState.selected;
} else if (tool === 'move') { const isDrawableEntity =
// Move tool gets a pointer selectedEntity?.type === 'regional_guidance' ||
stage.container().style.cursor = 'default'; selectedEntity?.type === 'layer' ||
} else if (tool === 'rect') { selectedEntity?.type === 'inpaint_mask';
// Rect gets a crosshair
stage.container().style.cursor = 'crosshair';
} else if (tool === 'brush' || tool === 'eraser') {
// Hide the native cursor and use the konva-rendered brush preview
stage.container().style.cursor = 'none';
} else if (tool === 'bbox') {
stage.container().style.cursor = 'default';
}
stage.draggable(tool === 'view'); // Update the stage's pointer style
if (tool === 'view') {
if (!cursorPos || layerCount === 0 || !isDrawableEntity) { // View gets a hand
// We can bail early if the mouse isn't over the stage or there are no layers stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab';
manager.preview.tool.group.visible(false); } else if (layerCount === 0) {
} else { // We have no layers, so we should not render any tool
manager.preview.tool.group.visible(true); stage.container().style.cursor = 'default';
} else if (!isDrawableEntity) {
// No need to render the brush preview if the cursor position or color is missing // Non-drawable layers don't have tools
if (cursorPos && (tool === 'brush' || tool === 'eraser')) { stage.container().style.cursor = 'not-allowed';
const scale = stage.scaleX(); } else if (tool === 'move') {
// Update the fill circle // Move tool gets a pointer
const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; stage.container().style.cursor = 'default';
manager.preview.tool.brush.fill.setAttrs({ } else if (tool === 'rect') {
x: cursorPos.x, // Rect gets a crosshair
y: cursorPos.y, stage.container().style.cursor = 'crosshair';
radius, } else if (tool === 'brush' || tool === 'eraser') {
fill: isDrawing ? '' : rgbaColorToString(currentFill), // Hide the native cursor and use the konva-rendered brush preview
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', stage.container().style.cursor = 'none';
}); } else if (tool === 'bbox') {
stage.container().style.cursor = 'default';
// Update the inner border of the brush preview
manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the brush preview
manager.preview.tool.brush.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
});
scaleToolPreview(manager, toolState);
manager.preview.tool.brush.group.visible(true);
} else {
manager.preview.tool.brush.group.visible(false);
} }
if (cursorPos && lastMouseDownPos && tool === 'rect') { stage.draggable(tool === 'view');
manager.preview.tool.rect.rect.setAttrs({
x: Math.min(cursorPos.x, lastMouseDownPos.x),
y: Math.min(cursorPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y),
fill: rgbaColorToString(currentFill),
visible: true,
});
} else {
manager.preview.tool.rect.rect.visible(false);
}
}
};
export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { if (!cursorPos || layerCount === 0 || !isDrawableEntity) {
// We can bail early if the mouse isn't over the stage or there are no layers
manager.preview.tool.group.visible(false);
} else {
manager.preview.tool.group.visible(true);
// No need to render the brush preview if the cursor position or color is missing
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
const scale = stage.scaleX();
// Update the fill circle
const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2;
manager.preview.tool.brush.fill.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius,
fill: isDrawing ? '' : rgbaColorToString(currentFill),
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
});
// Update the inner border of the brush preview
manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the brush preview
manager.preview.tool.brush.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
});
scaleToolPreview(manager, toolState);
manager.preview.tool.brush.group.visible(true);
} else {
manager.preview.tool.brush.group.visible(false);
}
if (cursorPos && lastMouseDownPos && tool === 'rect') {
manager.preview.tool.rect.rect.setAttrs({
x: Math.min(cursorPos.x, lastMouseDownPos.x),
y: Math.min(cursorPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y),
fill: rgbaColorToString(currentFill),
visible: true,
});
} else {
manager.preview.tool.rect.rect.visible(false);
}
}
};
/**
* Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview
* need to be adjusted.
* @param manager The konva node manager
* @param toolState The tool state
*/
const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => {
const scale = manager.stage.scaleX(); const scale = manager.stage.scaleX();
const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2;
manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
@ -404,6 +468,10 @@ export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2S
}); });
}; };
/**
* Creates the document overlay konva nodes.
* @returns The document overlay konva nodes
*/
export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => { export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => {
const group = new Konva.Group({ id: 'document_overlay_group', listening: false }); const group = new Konva.Group({ id: 'document_overlay_group', listening: false });
const outerRect = new Konva.Rect({ const outerRect = new Konva.Rect({
@ -423,32 +491,37 @@ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOv
return { group, innerRect, outerRect }; return { group, innerRect, outerRect };
}; };
export const renderDocumentBoundsOverlay = ( /**
manager: KonvaNodeManager, * Gets the document overlay render function.
getDocument: () => CanvasV2State['document'] * @param arg.manager The konva node manager
): void => { * @param arg.getDocument A function to get the document state
const document = getDocument(); * @returns The document overlay render function
const stage = manager.stage; */
export const getRenderDocumentOverlay =
(arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => {
const { manager, getDocument } = arg;
const document = getDocument();
const stage = manager.stage;
manager.preview.documentOverlay.group.zIndex(0); manager.preview.documentOverlay.group.zIndex(0);
const x = stage.x(); const x = stage.x();
const y = stage.y(); const y = stage.y();
const width = stage.width(); const width = stage.width();
const height = stage.height(); const height = stage.height();
const scale = stage.scaleX(); const scale = stage.scaleX();
manager.preview.documentOverlay.outerRect.setAttrs({ manager.preview.documentOverlay.outerRect.setAttrs({
offsetX: x / scale, offsetX: x / scale,
offsetY: y / scale, offsetY: y / scale,
width: width / scale, width: width / scale,
height: height / scale, height: height / scale,
}); });
manager.preview.documentOverlay.innerRect.setAttrs({ manager.preview.documentOverlay.innerRect.setAttrs({
x: 0, x: 0,
y: 0, y: 0,
width: document.width, width: document.width,
height: document.height, height: document.height,
}); });
}; };

View File

@ -19,19 +19,13 @@ import { mapId } from 'features/controlLayers/konva/util';
import type { import type {
CanvasEntity, CanvasEntity,
CanvasEntityIdentifier, CanvasEntityIdentifier,
CanvasV2State,
PosChangedArg, PosChangedArg,
RegionEntity, RegionEntity,
Tool, Tool,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
/**
* Logic for creating and rendering regional guidance layers.
*
* Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments
* in `renderRGLayer`.
*/
/** /**
* Creates the "compositing rect" for a regional guidance layer. * Creates the "compositing rect" for a regional guidance layer.
* @param konvaLayer The konva layer * @param konvaLayer The konva layer
@ -43,10 +37,11 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
}; };
/** /**
* Creates a regional guidance layer. * Gets a region's konva nodes and entity adapter, creating them if they do not exist.
* @param stage The konva stage * @param stage The konva stage
* @param entity The regional guidance layer state * @param entity The regional guidance layer state
* @param onLayerPosChanged Callback for when the layer's position changes * @param onLayerPosChanged Callback for when the layer's position changes
* @returns The konva entity adapter for the region
*/ */
const getRegion = ( const getRegion = (
manager: KonvaNodeManager, manager: KonvaNodeManager,
@ -78,7 +73,7 @@ const getRegion = (
}; };
/** /**
* Renders a raster layer. * Renders a region.
* @param stage The konva stage * @param stage The konva stage
* @param entity The regional guidance layer state * @param entity The regional guidance layer state
* @param globalMaskLayerOpacity The global mask layer opacity * @param globalMaskLayerOpacity The global mask layer opacity
@ -233,21 +228,40 @@ export const renderRegion = (
// } // }
}; };
export const renderRegions = ( /**
manager: KonvaNodeManager, * Gets a function to render all regions.
entities: RegionEntity[], * @param arg.manager The konva node manager
maskOpacity: number, * @param arg.getRegionEntityStates A function to get all region entities
tool: Tool, * @param arg.getMaskOpacity A function to get the mask opacity
selectedEntityIdentifier: CanvasEntityIdentifier | null, * @param arg.getToolState A function to get the tool state
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void * @param arg.getSelectedEntity A function to get the selectedEntity
): void => { * @param arg.onPosChanged A callback for when the position of an entity changes
// Destroy nonexistent layers * @returns A function to render all regions
for (const adapter of manager.getAll('regional_guidance')) { */
if (!entities.find((rg) => rg.id === adapter.id)) { export const getRenderRegions =
manager.destroy(adapter.id); (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;
const entities = getRegionEntityStates();
const maskOpacity = getMaskOpacity();
const toolState = getToolState();
const selectedEntity = getSelectedEntity();
// Destroy the konva nodes for nonexistent entities
for (const adapter of manager.getAll('regional_guidance')) {
if (!entities.find((rg) => rg.id === adapter.id)) {
manager.destroy(adapter.id);
}
} }
}
for (const entity of entities) { for (const entity of entities) {
renderRegion(manager, entity, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged);
} }
}; };

View File

@ -5,19 +5,23 @@ import { $isDebugging } from 'app/store/nanostores/isDebugging';
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { getArrangeEntities } from 'features/controlLayers/konva/renderers/arrange';
import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { createBackgroundLayer, getRenderBackground } from 'features/controlLayers/konva/renderers/background';
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
import { renderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; import { getRenderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters';
import { renderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask'; import { getRenderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask';
import { renderLayers } from 'features/controlLayers/konva/renderers/layers'; import { getRenderLayers } from 'features/controlLayers/konva/renderers/layers';
import { import {
renderBboxPreview, createBboxNodes,
renderDocumentBoundsOverlay, createDocumentOverlay,
scaleToolPreview, createPreviewLayer,
createToolPreviewNodes,
getRenderBbox,
getRenderDocumentOverlay,
getRenderToolPreview,
} from 'features/controlLayers/konva/renderers/preview'; } from 'features/controlLayers/konva/renderers/preview';
import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions';
import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { getFitDocumentToStage } from 'features/controlLayers/konva/renderers/stage';
import { import {
$stageAttrs, $stageAttrs,
bboxChanged, bboxChanged,
@ -67,8 +71,8 @@ export const $nodeManager = atom<KonvaNodeManager | null>(null);
/** /**
* Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the
* react rendering cycle entirely, improving canvas performance. * react rendering cycle entirely, improving canvas performance.
* @param store The Redux store * @param store The redux store
* @param stage The Konva stage * @param stage The konva stage
* @param container The stage's target container element * @param container The stage's target container element
* @returns A cleanup function * @returns A cleanup function
*/ */
@ -180,7 +184,7 @@ export const initializeRenderer = (
dispatch(toolBufferChanged(toolBuffer)); dispatch(toolBufferChanged(toolBuffer));
}; };
const _getSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => { const selectSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => {
const identifier = canvasV2.selectedEntityIdentifier; const identifier = canvasV2.selectedEntityIdentifier;
let selectedEntity: CanvasEntity | null = null; let selectedEntity: CanvasEntity | null = null;
if (!identifier) { if (!identifier) {
@ -202,10 +206,14 @@ export const initializeRenderer = (
return selectedEntity; return selectedEntity;
}; };
const _getCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => { const selectCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => {
let currentFill: RgbaColor = canvasV2.tool.fill; let currentFill: RgbaColor = canvasV2.tool.fill;
if (selectedEntity && selectedEntity.type === 'regional_guidance') { if (selectedEntity) {
currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity }; if (selectedEntity.type === 'regional_guidance') {
currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity };
} else if (selectedEntity.type === 'inpaint_mask') {
currentFill = { ...canvasV2.inpaintMask.fill, a: canvasV2.settings.maskOpacity };
}
} else { } else {
currentFill = canvasV2.tool.fill; currentFill = canvasV2.tool.fill;
} }
@ -223,14 +231,20 @@ export const initializeRenderer = (
// Read-only state, derived from redux // Read-only state, derived from redux
let prevCanvasV2 = getState().canvasV2; let prevCanvasV2 = getState().canvasV2;
let prevSelectedEntity: CanvasEntity | null = _getSelectedEntity(prevCanvasV2); let canvasV2 = getState().canvasV2;
let prevCurrentFill: RgbaColor = _getCurrentFill(prevCanvasV2, prevSelectedEntity); let prevSelectedEntity: CanvasEntity | null = selectSelectedEntity(prevCanvasV2);
let prevCurrentFill: RgbaColor = selectCurrentFill(prevCanvasV2, prevSelectedEntity);
const getSelectedEntity = () => prevSelectedEntity; const getSelectedEntity = () => prevSelectedEntity;
const getCurrentFill = () => prevCurrentFill; const getCurrentFill = () => prevCurrentFill;
const getBbox = () => getState().canvasV2.bbox; const getBbox = () => canvasV2.bbox;
const getDocument = () => getState().canvasV2.document; const getDocument = () => canvasV2.document;
const getToolState = () => getState().canvasV2.tool; const getToolState = () => canvasV2.tool;
const getSettings = () => getState().canvasV2.settings; const getSettings = () => canvasV2.settings;
const getRegionEntityStates = () => canvasV2.regions.entities;
const getLayerEntityStates = () => canvasV2.layers.entities;
const getControlAdapterEntityStates = () => canvasV2.controlAdapters.entities;
const getMaskOpacity = () => canvasV2.settings.maskOpacity;
const getInpaintMaskEntityState = () => canvasV2.inpaintMask;
// Read-write state, ephemeral interaction state // Read-write state, ephemeral interaction state
let isDrawing = false; let isDrawing = false;
@ -269,10 +283,22 @@ export const initializeRenderer = (
spaceKey = val; spaceKey = val;
}; };
const manager = new KonvaNodeManager(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get); const manager = new KonvaNodeManager(stage);
console.log(manager);
$nodeManager.set(manager); $nodeManager.set(manager);
manager.background = { layer: createBackgroundLayer() };
manager.stage.add(manager.background.layer);
manager.preview = {
layer: createPreviewLayer(),
bbox: createBboxNodes(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get),
tool: createToolPreviewNodes(),
documentOverlay: createDocumentOverlay(),
};
manager.preview.layer.add(manager.preview.bbox.group);
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({ const cleanupListeners = setStageEventHandlers({
manager, manager,
getToolState, getToolState,
@ -292,7 +318,6 @@ export const initializeRenderer = (
getSpaceKey, getSpaceKey,
setSpaceKey, setSpaceKey,
setStageAttrs: $stageAttrs.set, setStageAttrs: $stageAttrs.set,
getDocument,
getBbox, getBbox,
getSettings, getSettings,
onBrushLineAdded, onBrushLineAdded,
@ -309,16 +334,57 @@ export const initializeRenderer = (
// the entire state over when needed. // the entire state over when needed.
const debouncedUpdateBboxes = debounce(updateBboxes, 300); 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,
}),
};
const renderCanvas = () => { const renderCanvas = () => {
const { canvasV2 } = store.getState(); canvasV2 = store.getState().canvasV2;
if (prevCanvasV2 === canvasV2 && !isFirstRender) { if (prevCanvasV2 === canvasV2 && !isFirstRender) {
logIfDebugging('No changes detected, skipping render'); logIfDebugging('No changes detected, skipping render');
return; return;
} }
const selectedEntity = _getSelectedEntity(canvasV2); const selectedEntity = selectSelectedEntity(canvasV2);
const currentFill = _getCurrentFill(canvasV2, selectedEntity); const currentFill = selectCurrentFill(canvasV2, selectedEntity);
if ( if (
isFirstRender || isFirstRender ||
@ -326,7 +392,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering layers'); logIfDebugging('Rendering layers');
renderLayers(manager, canvasV2.layers.entities, canvasV2.tool.selected, onPosChanged); manager.renderers.renderLayers();
} }
if ( if (
@ -336,14 +402,7 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering regions'); logIfDebugging('Rendering regions');
renderRegions( manager.renderers.renderRegions();
manager,
canvasV2.regions.entities,
canvasV2.settings.maskOpacity,
canvasV2.tool.selected,
canvasV2.selectedEntityIdentifier,
onPosChanged
);
} }
if ( if (
@ -353,29 +412,22 @@ export const initializeRenderer = (
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering inpaint mask'); logIfDebugging('Rendering inpaint mask');
renderInpaintMask( manager.renderers.renderInpaintMask();
manager,
canvasV2.inpaintMask,
canvasV2.settings.maskOpacity,
canvasV2.tool.selected,
canvasV2.selectedEntityIdentifier,
onPosChanged
);
} }
if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) {
logIfDebugging('Rendering control adapters'); logIfDebugging('Rendering control adapters');
renderControlAdapters(manager, canvasV2.controlAdapters.entities); manager.renderers.renderControlAdapters();
} }
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
logIfDebugging('Rendering document bounds overlay'); logIfDebugging('Rendering document bounds overlay');
renderDocumentBoundsOverlay(manager, getDocument); manager.renderers.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');
renderBboxPreview(manager, canvasV2.bbox, canvasV2.tool.selected); manager.renderers.renderBbox();
} }
if ( if (
@ -395,7 +447,7 @@ export const initializeRenderer = (
canvasV2.regions.entities !== prevCanvasV2.regions.entities canvasV2.regions.entities !== prevCanvasV2.regions.entities
) { ) {
logIfDebugging('Arranging entities'); logIfDebugging('Arranging entities');
arrangeEntities(manager, canvasV2.layers.entities, canvasV2.controlAdapters.entities, canvasV2.regions.entities); manager.renderers.arrangeEntities();
} }
prevCanvasV2 = canvasV2; prevCanvasV2 = canvasV2;
@ -419,8 +471,8 @@ export const initializeRenderer = (
height: stage.height(), height: stage.height(),
scale: stage.scaleX(), scale: stage.scaleX(),
}); });
renderBackgroundLayer(manager); manager.renderers.renderBackground();
renderDocumentBoundsOverlay(manager, getDocument); manager.renderers.renderDocumentOverlay();
}; };
const resizeObserver = new ResizeObserver(fitStageToContainer); const resizeObserver = new ResizeObserver(fitStageToContainer);
@ -431,10 +483,8 @@ export const initializeRenderer = (
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.
const stageAttrs = fitDocumentToStage(stage, prevCanvasV2.document); manager.renderers.fitDocumentToStage();
// The HUD displays some of the stage attributes, so we need to update it here. manager.renderers.renderToolPreview();
$stageAttrs.set(stageAttrs);
scaleToolPreview(manager, getToolState());
renderCanvas(); renderCanvas();
return () => { return () => {

View File

@ -1,16 +1,32 @@
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 { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types';
import type Konva from 'konva';
export const fitDocumentToStage = (stage: Konva.Stage, document: CanvasV2State['document']): StageAttrs => { /**
// Fit & center the document on the stage * Gets a function to fit the document to the stage, resetting the stage scale to 100%.
const width = stage.width(); * If the document is smaller than the stage, the stage scale is increased to fit the document.
const height = stage.height(); * @param arg.manager The konva node manager
const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; * @param arg.getDocument A function to get the current document state
const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; * @param arg.setStageAttrs A function to set the stage attributes
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); * @returns A function to fit the document to the stage
const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; */
const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; export const getFitDocumentToStage =
stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); (arg: {
return { x, y, width, height, scale }; manager: KonvaNodeManager;
}; getDocument: () => CanvasV2State['document'];
setStageAttrs: (stageAttrs: StageAttrs) => void;
}) =>
(): void => {
const { manager, getDocument, setStageAttrs } = arg;
const document = getDocument();
// Fit & center the document on the stage
const width = manager.stage.width();
const height = manager.stage.height();
const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2;
const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2;
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1);
const x = (width - docWidthWithBuffer * 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 });
setStageAttrs({ x, y, width, height, scale });
};