mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): consolidate konva API
This commit is contained in:
parent
d497da0e61
commit
57c257d10d
@ -130,7 +130,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
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.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mousedown
|
//#region mousedown
|
||||||
@ -251,7 +251,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setLastAddedPoint(pos);
|
setLastAddedPoint(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mouseup
|
//#region mouseup
|
||||||
@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setLastMouseDownPos(null);
|
setLastMouseDownPos(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mousemove
|
//#region mousemove
|
||||||
@ -396,7 +396,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mouseleave
|
//#region mouseleave
|
||||||
@ -425,7 +425,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region wheel
|
//#region wheel
|
||||||
@ -466,11 +466,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
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.konvaApi.renderBackground();
|
manager.renderBackground();
|
||||||
manager.konvaApi.renderDocumentOverlay();
|
manager.renderDocumentOverlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region dragmove
|
//#region dragmove
|
||||||
@ -482,9 +482,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.konvaApi.renderBackground();
|
manager.renderBackground();
|
||||||
manager.konvaApi.renderDocumentOverlay();
|
manager.renderDocumentOverlay();
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region dragend
|
//#region dragend
|
||||||
@ -497,7 +497,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region key
|
//#region key
|
||||||
@ -518,12 +518,12 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setTool('view');
|
setTool('view');
|
||||||
setSpaceKey(true);
|
setSpaceKey(true);
|
||||||
} else if (e.key === 'r') {
|
} else if (e.key === 'r') {
|
||||||
manager.konvaApi.fitDocumentToStage();
|
manager.fitDocumentToStage();
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
manager.konvaApi.renderBackground();
|
manager.renderBackground();
|
||||||
manager.konvaApi.renderDocumentOverlay();
|
manager.renderDocumentOverlay();
|
||||||
}
|
}
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
@ -541,7 +541,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setToolBuffer(null);
|
setToolBuffer(null);
|
||||||
setSpaceKey(false);
|
setSpaceKey(false);
|
||||||
}
|
}
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
};
|
};
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||||
|
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
|
||||||
|
import { KonvaBackground } from 'features/controlLayers/konva/renderers/background';
|
||||||
|
import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview';
|
||||||
import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
BrushLine,
|
|
||||||
BrushLineAddedArg,
|
BrushLineAddedArg,
|
||||||
CanvasEntity,
|
CanvasEntity,
|
||||||
CanvasV2State,
|
CanvasV2State,
|
||||||
EraserLine,
|
|
||||||
EraserLineAddedArg,
|
EraserLineAddedArg,
|
||||||
GenerationMode,
|
GenerationMode,
|
||||||
ImageObject,
|
|
||||||
PointAddedToLineArg,
|
PointAddedToLineArg,
|
||||||
PosChangedArg,
|
PosChangedArg,
|
||||||
Rect,
|
Rect,
|
||||||
RectShape,
|
|
||||||
RectShapeAddedArg,
|
RectShapeAddedArg,
|
||||||
RgbaColor,
|
RgbaColor,
|
||||||
StageAttrs,
|
StageAttrs,
|
||||||
@ -21,96 +20,16 @@ import type {
|
|||||||
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
||||||
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 { atom } from 'nanostores';
|
|
||||||
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
||||||
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export type BrushLineObjectRecord = {
|
import { KonvaControlAdapter } from './renderers/controlAdapters';
|
||||||
id: string;
|
import { KonvaInpaintMask } from './renderers/inpaintMask';
|
||||||
type: BrushLine['type'];
|
import { KonvaLayerAdapter } from './renderers/layers';
|
||||||
konvaLine: Konva.Line;
|
import { KonvaRegion } from './renderers/regions';
|
||||||
konvaLineGroup: Konva.Group;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EraserLineObjectRecord = {
|
export type StateApi = {
|
||||||
id: string;
|
|
||||||
type: EraserLine['type'];
|
|
||||||
konvaLine: Konva.Line;
|
|
||||||
konvaLineGroup: Konva.Group;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RectShapeObjectRecord = {
|
|
||||||
id: string;
|
|
||||||
type: RectShape['type'];
|
|
||||||
konvaRect: Konva.Rect;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ImageObjectRecord = {
|
|
||||||
id: string;
|
|
||||||
type: ImageObject['type'];
|
|
||||||
konvaImageGroup: Konva.Group;
|
|
||||||
konvaPlaceholderGroup: Konva.Group;
|
|
||||||
konvaPlaceholderRect: Konva.Rect;
|
|
||||||
konvaPlaceholderText: Konva.Text;
|
|
||||||
imageName: string | null;
|
|
||||||
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
|
|
||||||
isLoading: boolean;
|
|
||||||
isError: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord;
|
|
||||||
|
|
||||||
type KonvaApi = {
|
|
||||||
renderRegions: () => void;
|
|
||||||
renderLayers: () => void;
|
|
||||||
renderControlAdapters: () => void;
|
|
||||||
renderInpaintMask: () => void;
|
|
||||||
renderBbox: () => void;
|
|
||||||
renderDocumentOverlay: () => void;
|
|
||||||
renderBackground: () => void;
|
|
||||||
renderToolPreview: () => void;
|
|
||||||
renderStagingArea: () => void;
|
|
||||||
arrangeEntities: () => void;
|
|
||||||
fitDocumentToStage: () => void;
|
|
||||||
fitStageToContainer: () => 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;
|
|
||||||
};
|
|
||||||
stagingArea: {
|
|
||||||
group: Konva.Group;
|
|
||||||
image: ImageObjectRecord | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateApi = {
|
|
||||||
getToolState: () => CanvasV2State['tool'];
|
getToolState: () => CanvasV2State['tool'];
|
||||||
getCurrentFill: () => RgbaColor;
|
getCurrentFill: () => RgbaColor;
|
||||||
setTool: (tool: Tool) => void;
|
setTool: (tool: Tool) => void;
|
||||||
@ -174,21 +93,25 @@ type Util = {
|
|||||||
export class KonvaNodeManager {
|
export class KonvaNodeManager {
|
||||||
stage: Konva.Stage;
|
stage: Konva.Stage;
|
||||||
container: HTMLDivElement;
|
container: HTMLDivElement;
|
||||||
adapters: Map<string, KonvaEntityAdapter>;
|
controlAdapters: Map<string, KonvaControlAdapter>;
|
||||||
|
layers: Map<string, KonvaLayerAdapter>;
|
||||||
|
regions: Map<string, KonvaRegion>;
|
||||||
|
inpaintMask: KonvaInpaintMask | null;
|
||||||
util: Util;
|
util: Util;
|
||||||
_background: BackgroundLayer | null;
|
stateApi: StateApi;
|
||||||
_preview: PreviewLayer | null;
|
preview: KonvaPreview;
|
||||||
_konvaApi: KonvaApi | null;
|
background: KonvaBackground;
|
||||||
_stateApi: StateApi | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
container: HTMLDivElement,
|
container: HTMLDivElement,
|
||||||
|
stateApi: StateApi,
|
||||||
getImageDTO: Util['getImageDTO'] = defaultGetImageDTO,
|
getImageDTO: Util['getImageDTO'] = defaultGetImageDTO,
|
||||||
uploadImage: Util['uploadImage'] = defaultUploadImage
|
uploadImage: Util['uploadImage'] = defaultUploadImage
|
||||||
) {
|
) {
|
||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
this.stateApi = stateApi;
|
||||||
this.util = {
|
this.util = {
|
||||||
getImageDTO,
|
getImageDTO,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
@ -199,83 +122,183 @@ export class KonvaNodeManager {
|
|||||||
getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this),
|
getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this),
|
||||||
getGenerationMode: this._getGenerationMode.bind(this),
|
getGenerationMode: this._getGenerationMode.bind(this),
|
||||||
};
|
};
|
||||||
this._konvaApi = null;
|
this.preview = new KonvaPreview(
|
||||||
this._preview = null;
|
this.stage,
|
||||||
this._background = null;
|
this.stateApi.getBbox,
|
||||||
this._stateApi = null;
|
this.stateApi.onBboxTransformed,
|
||||||
this.adapters = new Map();
|
this.stateApi.getShiftKey,
|
||||||
|
this.stateApi.getCtrlKey,
|
||||||
|
this.stateApi.getMetaKey,
|
||||||
|
this.stateApi.getAltKey
|
||||||
|
);
|
||||||
|
this.background = new KonvaBackground();
|
||||||
|
this.layers = new Map();
|
||||||
|
this.regions = new Map();
|
||||||
|
this.controlAdapters = new Map();
|
||||||
|
this.inpaintMask = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter {
|
renderLayers() {
|
||||||
const adapter = new KonvaEntityAdapter(entity, konvaLayer, konvaObjectGroup, this);
|
const { entities } = this.stateApi.getLayersState();
|
||||||
this.adapters.set(adapter.id, adapter);
|
const toolState = this.stateApi.getToolState();
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id: string): KonvaEntityAdapter | undefined {
|
for (const adapter of this.layers.values()) {
|
||||||
return this.adapters.get(id);
|
if (!entities.find((l) => l.id === adapter.id)) {
|
||||||
}
|
adapter.destroy();
|
||||||
|
this.layers.delete(adapter.id);
|
||||||
getAll(type?: CanvasEntity['type']): KonvaEntityAdapter[] {
|
|
||||||
if (type) {
|
|
||||||
return Array.from(this.adapters.values()).filter((adapter) => adapter.entityType === type);
|
|
||||||
} else {
|
|
||||||
return Array.from(this.adapters.values());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(id: string): boolean {
|
for (const entity of entities) {
|
||||||
const adapter = this.get(id);
|
let adapter = this.layers.get(entity.id);
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
return false;
|
adapter = new KonvaLayerAdapter(entity, this.stateApi.onPosChanged);
|
||||||
|
this.layers.set(adapter.id, adapter);
|
||||||
|
this.stage.add(adapter.konvaLayer);
|
||||||
|
}
|
||||||
|
adapter.render(entity, toolState.selected);
|
||||||
}
|
}
|
||||||
adapter.konvaLayer.destroy();
|
|
||||||
return this.adapters.delete(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set konvaApi(konvaApi: KonvaApi) {
|
renderRegions() {
|
||||||
this._konvaApi = konvaApi;
|
const { entities } = this.stateApi.getRegionsState();
|
||||||
|
const maskOpacity = this.stateApi.getMaskOpacity();
|
||||||
|
const toolState = this.stateApi.getToolState();
|
||||||
|
const selectedEntity = this.stateApi.getSelectedEntity();
|
||||||
|
|
||||||
|
// Destroy the konva nodes for nonexistent entities
|
||||||
|
for (const adapter of this.regions.values()) {
|
||||||
|
if (!entities.find((rg) => rg.id === adapter.id)) {
|
||||||
|
adapter.destroy();
|
||||||
|
this.regions.delete(adapter.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get konvaApi(): KonvaApi {
|
for (const entity of entities) {
|
||||||
assert(this._konvaApi !== null, 'Konva API has not been set');
|
let adapter = this.regions.get(entity.id);
|
||||||
return this._konvaApi;
|
if (!adapter) {
|
||||||
|
adapter = new KonvaRegion(entity, this.stateApi.onPosChanged);
|
||||||
|
this.regions.set(adapter.id, adapter);
|
||||||
|
this.stage.add(adapter.konvaLayer);
|
||||||
|
}
|
||||||
|
adapter.render(entity, toolState.selected, selectedEntity, maskOpacity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set preview(preview: PreviewLayer) {
|
renderInpaintMask() {
|
||||||
this._preview = preview;
|
const inpaintMaskState = this.stateApi.getInpaintMaskState();
|
||||||
|
if (!this.inpaintMask) {
|
||||||
|
this.inpaintMask = new KonvaInpaintMask(inpaintMaskState, this.stateApi.onPosChanged);
|
||||||
|
this.stage.add(this.inpaintMask.konvaLayer);
|
||||||
|
}
|
||||||
|
const toolState = this.stateApi.getToolState();
|
||||||
|
const selectedEntity = this.stateApi.getSelectedEntity();
|
||||||
|
const maskOpacity = this.stateApi.getMaskOpacity();
|
||||||
|
|
||||||
|
this.inpaintMask.render(inpaintMaskState, toolState.selected, selectedEntity, maskOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
get preview(): PreviewLayer {
|
renderControlAdapters() {
|
||||||
assert(this._preview !== null, 'Konva preview layer has not been set');
|
const { entities } = this.stateApi.getControlAdaptersState();
|
||||||
return this._preview;
|
|
||||||
|
for (const adapter of this.controlAdapters.values()) {
|
||||||
|
if (!entities.find((ca) => ca.id === adapter.id)) {
|
||||||
|
adapter.destroy();
|
||||||
|
this.controlAdapters.delete(adapter.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set background(background: BackgroundLayer) {
|
for (const entity of entities) {
|
||||||
this._background = background;
|
let adapter = this.controlAdapters.get(entity.id);
|
||||||
|
if (!adapter) {
|
||||||
|
adapter = new KonvaControlAdapter(entity);
|
||||||
|
this.controlAdapters.set(adapter.id, adapter);
|
||||||
|
this.stage.add(adapter.konvaLayer);
|
||||||
|
}
|
||||||
|
adapter.render(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get background(): BackgroundLayer {
|
arrangeEntities() {
|
||||||
assert(this._background !== null, 'Konva background layer has not been set');
|
const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi;
|
||||||
return this._background;
|
const layers = getLayersState().entities;
|
||||||
|
const controlAdapters = getControlAdaptersState().entities;
|
||||||
|
const regions = getRegionsState().entities;
|
||||||
|
let zIndex = 0;
|
||||||
|
this.background.konvaLayer.zIndex(++zIndex);
|
||||||
|
for (const layer of layers) {
|
||||||
|
this.layers.get(layer.id)?.konvaLayer.zIndex(++zIndex);
|
||||||
|
}
|
||||||
|
for (const ca of controlAdapters) {
|
||||||
|
this.controlAdapters.get(ca.id)?.konvaLayer.zIndex(++zIndex);
|
||||||
|
}
|
||||||
|
for (const rg of regions) {
|
||||||
|
this.regions.get(rg.id)?.konvaLayer.zIndex(++zIndex);
|
||||||
|
}
|
||||||
|
this.inpaintMask?.konvaLayer.zIndex(++zIndex);
|
||||||
|
this.preview.konvaLayer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
set stateApi(stateApi: StateApi) {
|
renderDocumentOverlay() {
|
||||||
this._stateApi = stateApi;
|
this.preview.renderDocumentOverlay(this.stage, this.stateApi.getDocument());
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateApi(): StateApi {
|
renderBbox() {
|
||||||
assert(this._stateApi !== null, 'State API has not been set');
|
this.preview.renderBbox(this.stateApi.getBbox(), this.stateApi.getToolState());
|
||||||
return this._stateApi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMaskLayerClone(arg: { id: string }): Konva.Layer {
|
renderToolPreview() {
|
||||||
const { id } = arg;
|
this.preview.renderToolPreview(
|
||||||
const adapter = this.get(id);
|
this.stage,
|
||||||
assert(adapter, `Adapter for entity ${id} not found`);
|
1,
|
||||||
|
this.stateApi.getToolState(),
|
||||||
|
this.stateApi.getCurrentFill(),
|
||||||
|
this.stateApi.getSelectedEntity(),
|
||||||
|
this.stateApi.getLastCursorPos(),
|
||||||
|
this.stateApi.getLastMouseDownPos(),
|
||||||
|
this.stateApi.getIsDrawing(),
|
||||||
|
this.stateApi.getIsMouseDown()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const layerClone = adapter.konvaLayer.clone();
|
fitDocumentToStage(): void {
|
||||||
const objectGroupClone = adapter.konvaObjectGroup.clone();
|
const { getDocument, setStageAttrs } = this.stateApi;
|
||||||
|
const document = getDocument();
|
||||||
|
// Fit & center the document on the stage
|
||||||
|
const width = this.stage.width();
|
||||||
|
const height = this.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;
|
||||||
|
this.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
|
||||||
|
setStageAttrs({ x, y, width, height, scale });
|
||||||
|
}
|
||||||
|
|
||||||
|
fitStageToContainer(): void {
|
||||||
|
this.stage.width(this.container.offsetWidth);
|
||||||
|
this.stage.height(this.container.offsetHeight);
|
||||||
|
this.stateApi.setStageAttrs({
|
||||||
|
x: this.stage.x(),
|
||||||
|
y: this.stage.y(),
|
||||||
|
width: this.stage.width(),
|
||||||
|
height: this.stage.height(),
|
||||||
|
scale: this.stage.scaleX(),
|
||||||
|
});
|
||||||
|
this.renderBackground();
|
||||||
|
this.renderDocumentOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBackground() {
|
||||||
|
this.background.renderBackground(this.stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMaskLayerClone(): Konva.Layer {
|
||||||
|
assert(this.inpaintMask, 'Inpaint mask layer has not been set');
|
||||||
|
|
||||||
|
const layerClone = this.inpaintMask.konvaLayer.clone();
|
||||||
|
const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone();
|
||||||
|
|
||||||
layerClone.destroyChildren();
|
layerClone.destroyChildren();
|
||||||
layerClone.add(objectGroupClone);
|
layerClone.add(objectGroupClone);
|
||||||
@ -392,7 +415,7 @@ export class KonvaNodeManager {
|
|||||||
|
|
||||||
async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> {
|
async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> {
|
||||||
const { bbox, preview = false } = arg;
|
const { bbox, preview = false } = arg;
|
||||||
const { imageCache } = this.stateApi.getLayersState();
|
// const { imageCache } = this.stateApi.getLayersState();
|
||||||
|
|
||||||
// if (imageCache) {
|
// if (imageCache) {
|
||||||
// const imageDTO = await this.util.getImageDTO(imageCache.name);
|
// const imageDTO = await this.util.getImageDTO(imageCache.name);
|
||||||
@ -416,72 +439,3 @@ export class KonvaNodeManager {
|
|||||||
return imageDTO;
|
return imageDTO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KonvaEntityAdapter {
|
|
||||||
id: string;
|
|
||||||
entityType: CanvasEntity['type'];
|
|
||||||
konvaLayer: Konva.Layer; // Every entity is associated with a konva layer
|
|
||||||
konvaObjectGroup: Konva.Group; // Every entity's nodes are part of an object group
|
|
||||||
objectRecords: Map<string, ObjectRecord>;
|
|
||||||
manager: KonvaNodeManager;
|
|
||||||
|
|
||||||
constructor(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) {
|
|
||||||
this.id = entity.id;
|
|
||||||
this.entityType = entity.type;
|
|
||||||
this.konvaLayer = konvaLayer;
|
|
||||||
this.konvaObjectGroup = konvaObjectGroup;
|
|
||||||
this.objectRecords = new Map();
|
|
||||||
this.manager = manager;
|
|
||||||
this.konvaLayer.add(this.konvaObjectGroup);
|
|
||||||
this.manager.stage.add(this.konvaLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
add<T extends ObjectRecord>(objectRecord: T): T {
|
|
||||||
this.objectRecords.set(objectRecord.id, objectRecord);
|
|
||||||
if (objectRecord.type === 'brush_line' || objectRecord.type === 'eraser_line') {
|
|
||||||
objectRecord.konvaLineGroup.add(objectRecord.konvaLine);
|
|
||||||
this.konvaObjectGroup.add(objectRecord.konvaLineGroup);
|
|
||||||
} else if (objectRecord.type === 'rect_shape') {
|
|
||||||
this.konvaObjectGroup.add(objectRecord.konvaRect);
|
|
||||||
} else if (objectRecord.type === 'image') {
|
|
||||||
objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderRect);
|
|
||||||
objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderText);
|
|
||||||
objectRecord.konvaImageGroup.add(objectRecord.konvaPlaceholderGroup);
|
|
||||||
this.konvaObjectGroup.add(objectRecord.konvaImageGroup);
|
|
||||||
}
|
|
||||||
return objectRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
get<T extends ObjectRecord>(id: string): T | undefined {
|
|
||||||
return this.objectRecords.get(id) as T | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll<T extends ObjectRecord>(): T[] {
|
|
||||||
return Array.from(this.objectRecords.values()) as T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(id: string): boolean {
|
|
||||||
const record = this.get(id);
|
|
||||||
if (!record) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (record.type === 'brush_line' || record.type === 'eraser_line') {
|
|
||||||
record.konvaLineGroup.destroy();
|
|
||||||
} else if (record.type === 'rect_shape') {
|
|
||||||
record.konvaRect.destroy();
|
|
||||||
} else if (record.type === 'image') {
|
|
||||||
record.konvaImageGroup.destroy();
|
|
||||||
}
|
|
||||||
return this.objectRecords.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $nodeManager = atom<KonvaNodeManager | null>(null);
|
|
||||||
export const setNodeManager = (manager: KonvaNodeManager) => {
|
|
||||||
$nodeManager.set(manager);
|
|
||||||
};
|
|
||||||
export const getNodeManager = () => {
|
|
||||||
const nodeManager = $nodeManager.get();
|
|
||||||
assert(nodeManager, 'Konva node manager not initialized');
|
|
||||||
return nodeManager;
|
|
||||||
};
|
|
||||||
|
@ -24,7 +24,7 @@ export const getArrangeEntities = (manager: KonvaNodeManager) => {
|
|||||||
manager.get(rg.id)?.konvaLayer.zIndex(++zIndex);
|
manager.get(rg.id)?.konvaLayer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex);
|
manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex);
|
||||||
manager.preview.layer.zIndex(++zIndex);
|
manager.preview.konvaLayer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return arrangeEntities;
|
return arrangeEntities;
|
||||||
|
@ -120,3 +120,86 @@ export const getRenderBackground = (manager: KonvaNodeManager) => {
|
|||||||
|
|
||||||
return renderBackground;
|
return renderBackground;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class KonvaBackground {
|
||||||
|
konvaLayer: Konva.Layer;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.konvaLayer = new Konva.Layer({ listening: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBackground(stage: Konva.Stage): void {
|
||||||
|
this.konvaLayer.zIndex(0);
|
||||||
|
const scale = stage.scaleX();
|
||||||
|
const gridSpacing = getGridSpacing(scale);
|
||||||
|
const x = stage.x();
|
||||||
|
const y = stage.y();
|
||||||
|
const width = stage.width();
|
||||||
|
const height = stage.height();
|
||||||
|
const stageRect = {
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
x2: width,
|
||||||
|
y2: height,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOffset = {
|
||||||
|
x: Math.ceil(x / scale / gridSpacing) * gridSpacing,
|
||||||
|
y: Math.ceil(y / scale / gridSpacing) * gridSpacing,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridRect = {
|
||||||
|
x1: -gridOffset.x,
|
||||||
|
y1: -gridOffset.y,
|
||||||
|
x2: width / scale - gridOffset.x + gridSpacing,
|
||||||
|
y2: height / scale - gridOffset.y + gridSpacing,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridFullRect = {
|
||||||
|
x1: Math.min(stageRect.x1, gridRect.x1),
|
||||||
|
y1: Math.min(stageRect.y1, gridRect.y1),
|
||||||
|
x2: Math.max(stageRect.x2, gridRect.x2),
|
||||||
|
y2: Math.max(stageRect.y2, gridRect.y2),
|
||||||
|
};
|
||||||
|
|
||||||
|
// find the x & y size of the grid
|
||||||
|
const xSize = gridFullRect.x2 - gridFullRect.x1;
|
||||||
|
const ySize = gridFullRect.y2 - gridFullRect.y1;
|
||||||
|
// compute the number of steps required on each axis.
|
||||||
|
const xSteps = Math.round(xSize / gridSpacing) + 1;
|
||||||
|
const ySteps = Math.round(ySize / gridSpacing) + 1;
|
||||||
|
|
||||||
|
const strokeWidth = 1 / scale;
|
||||||
|
let _x = 0;
|
||||||
|
let _y = 0;
|
||||||
|
|
||||||
|
this.konvaLayer.destroyChildren();
|
||||||
|
|
||||||
|
for (let i = 0; i < xSteps; i++) {
|
||||||
|
_x = gridFullRect.x1 + i * gridSpacing;
|
||||||
|
this.konvaLayer.add(
|
||||||
|
new Konva.Line({
|
||||||
|
x: _x,
|
||||||
|
y: gridFullRect.y1,
|
||||||
|
points: [0, 0, 0, ySize],
|
||||||
|
stroke: _x % 64 ? fineGridLineColor : baseGridLineColor,
|
||||||
|
strokeWidth,
|
||||||
|
listening: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ySteps; i++) {
|
||||||
|
_y = gridFullRect.y1 + i * gridSpacing;
|
||||||
|
this.konvaLayer.add(
|
||||||
|
new Konva.Line({
|
||||||
|
x: gridFullRect.x1,
|
||||||
|
y: _y,
|
||||||
|
points: [0, 0, xSize, 0],
|
||||||
|
stroke: _y % 64 ? fineGridLineColor : baseGridLineColor,
|
||||||
|
strokeWidth,
|
||||||
|
listening: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,63 +1,50 @@
|
|||||||
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
|
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
|
||||||
import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { ImageObjectRecord, KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import {
|
|
||||||
createImageObjectGroup,
|
|
||||||
createObjectGroup,
|
|
||||||
updateImageSource,
|
|
||||||
} from 'features/controlLayers/konva/renderers/objects';
|
|
||||||
import type { 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 { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
/**
|
import { KonvaImage } from './objects';
|
||||||
* Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist.
|
|
||||||
* @param manager The konva node manager
|
export class KonvaControlAdapter {
|
||||||
* @param entity The control adapter layer state
|
id: string;
|
||||||
*/
|
konvaLayer: Konva.Layer;
|
||||||
const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => {
|
konvaObjectGroup: Konva.Group;
|
||||||
const adapter = manager.get(entity.id);
|
konvaImageObject: KonvaImage | null;
|
||||||
if (adapter) {
|
|
||||||
return adapter;
|
constructor(entity: ControlAdapterEntity) {
|
||||||
}
|
const { id } = entity;
|
||||||
const konvaLayer = new Konva.Layer({
|
this.id = id;
|
||||||
id: entity.id,
|
this.konvaLayer = new Konva.Layer({
|
||||||
name: CA_LAYER_NAME,
|
id,
|
||||||
imageSmoothingEnabled: false,
|
imageSmoothingEnabled: false,
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME);
|
this.konvaObjectGroup = new Konva.Group({
|
||||||
return manager.add(entity, konvaLayer, konvaObjectGroup);
|
id: getObjectGroupId(this.konvaLayer.id(), uuidv4()),
|
||||||
};
|
listening: false,
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a control adapter.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @param entity The control adapter entity state
|
|
||||||
*/
|
|
||||||
export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise<void> => {
|
|
||||||
const adapter = getControlAdapter(manager, entity);
|
|
||||||
const imageObject = entity.processedImageObject ?? entity.imageObject;
|
|
||||||
|
|
||||||
if (!imageObject) {
|
|
||||||
// The user has deleted/reset the image
|
|
||||||
adapter.getAll().forEach((entry) => {
|
|
||||||
adapter.destroy(entry.id);
|
|
||||||
});
|
});
|
||||||
|
this.konvaLayer.add(this.konvaObjectGroup);
|
||||||
|
this.konvaImageObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(entity: ControlAdapterEntity) {
|
||||||
|
const imageObject = entity.processedImageObject ?? entity.imageObject;
|
||||||
|
if (!imageObject) {
|
||||||
|
if (this.konvaImageObject) {
|
||||||
|
this.konvaImageObject.destroy();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = adapter.getAll<ImageObjectRecord>()[0];
|
|
||||||
const opacity = entity.opacity;
|
const opacity = entity.opacity;
|
||||||
const visible = entity.isEnabled;
|
const visible = entity.isEnabled;
|
||||||
const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [];
|
const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [];
|
||||||
|
|
||||||
if (!entry) {
|
if (!this.konvaImageObject) {
|
||||||
entry = await createImageObjectGroup({
|
this.konvaImageObject = await new KonvaImage({
|
||||||
adapter: adapter,
|
imageObject,
|
||||||
obj: imageObject,
|
|
||||||
name: CA_LAYER_IMAGE_NAME,
|
|
||||||
onLoad: (konvaImage) => {
|
onLoad: (konvaImage) => {
|
||||||
konvaImage.filters(filters);
|
konvaImage.filters(filters);
|
||||||
konvaImage.cache();
|
konvaImage.cache();
|
||||||
@ -65,55 +52,25 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co
|
|||||||
konvaImage.visible(visible);
|
konvaImage.visible(visible);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
this.konvaObjectGroup.add(this.konvaImageObject.konvaImageGroup);
|
||||||
if (entry.isLoading || entry.isError) {
|
}
|
||||||
|
if (this.konvaImageObject.isLoading || this.konvaImageObject.isError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(entry.konvaImage, `Image entry ${entry.id} must have a konva image if it is not loading or in error state`);
|
if (this.konvaImageObject.imageName !== imageObject.image.name) {
|
||||||
const imageSource = entry.konvaImage.image();
|
this.konvaImageObject.updateImageSource(imageObject.image.name);
|
||||||
assert(imageSource instanceof HTMLImageElement, `Image source must be an HTMLImageElement`);
|
|
||||||
if (imageSource.id !== imageObject.image.name) {
|
|
||||||
updateImageSource({
|
|
||||||
objectRecord: entry,
|
|
||||||
image: imageObject.image,
|
|
||||||
onLoad: (konvaImage) => {
|
|
||||||
konvaImage.filters(filters);
|
|
||||||
konvaImage.cache();
|
|
||||||
konvaImage.opacity(opacity);
|
|
||||||
konvaImage.visible(visible);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!isEqual(entry.konvaImage.filters(), filters)) {
|
|
||||||
entry.konvaImage.filters(filters);
|
|
||||||
entry.konvaImage.cache();
|
|
||||||
}
|
}
|
||||||
entry.konvaImage.opacity(opacity);
|
if (this.konvaImageObject.konvaImage) {
|
||||||
entry.konvaImage.visible(visible);
|
if (!isEqual(this.konvaImageObject.konvaImage.filters(), filters)) {
|
||||||
|
this.konvaImageObject.konvaImage.filters(filters);
|
||||||
|
this.konvaImageObject.konvaImage.cache();
|
||||||
}
|
}
|
||||||
}
|
this.konvaImageObject.konvaImage.opacity(opacity);
|
||||||
};
|
this.konvaImageObject.konvaImage.visible(visible);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a function to render all control adapters.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @returns A function to render all control adapters
|
|
||||||
*/
|
|
||||||
export const getRenderControlAdapters = (manager: KonvaNodeManager) => {
|
|
||||||
const { getControlAdaptersState } = manager.stateApi;
|
|
||||||
|
|
||||||
function renderControlAdapters(): void {
|
|
||||||
const { entities } = getControlAdaptersState();
|
|
||||||
// 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) {
|
|
||||||
renderControlAdapter(manager, entity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderControlAdapters;
|
destroy(): void {
|
||||||
};
|
this.konvaLayer.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,162 +1,134 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import {
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
COMPOSITING_RECT_NAME,
|
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
||||||
INPAINT_MASK_LAYER_BRUSH_LINE_NAME,
|
|
||||||
INPAINT_MASK_LAYER_ERASER_LINE_NAME,
|
|
||||||
INPAINT_MASK_LAYER_NAME,
|
|
||||||
INPAINT_MASK_LAYER_OBJECT_GROUP_NAME,
|
|
||||||
INPAINT_MASK_LAYER_RECT_SHAPE_NAME,
|
|
||||||
} from 'features/controlLayers/konva/naming';
|
|
||||||
import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
|
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
|
||||||
import {
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||||
createObjectGroup,
|
|
||||||
getBrushLine,
|
|
||||||
getEraserLine,
|
|
||||||
getRectShape,
|
|
||||||
} from 'features/controlLayers/konva/renderers/objects';
|
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type { CanvasEntity, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier, InpaintMaskEntity, Tool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
/**
|
export class KonvaInpaintMask {
|
||||||
* Creates the "compositing rect" for the inpaint mask.
|
id: string;
|
||||||
* @param konvaLayer The konva layer
|
konvaLayer: Konva.Layer;
|
||||||
*/
|
konvaObjectGroup: Konva.Group;
|
||||||
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
|
compositingRect: Konva.Rect;
|
||||||
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
|
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
||||||
konvaLayer.add(compositingRect);
|
|
||||||
return compositingRect;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
constructor(entity: InpaintMaskEntity, onPosChanged: StateApi['onPosChanged']) {
|
||||||
* Gets the singleton inpaint mask entity's konva nodes and entity adapter, creating them if they do not exist.
|
this.id = entity.id;
|
||||||
* @param manager The konva node manager
|
|
||||||
* @param entityState The inpaint mask entity state
|
this.konvaLayer = new Konva.Layer({
|
||||||
* @param onPosChanged Callback for when the position changes (e.g. the entity is dragged)
|
id: entity.id,
|
||||||
* @returns The konva entity adapter for the inpaint mask
|
|
||||||
*/
|
|
||||||
const getInpaintMask = (
|
|
||||||
manager: KonvaNodeManager,
|
|
||||||
entityState: InpaintMaskEntity,
|
|
||||||
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
|
|
||||||
): KonvaEntityAdapter => {
|
|
||||||
const adapter = manager.get(entityState.id);
|
|
||||||
if (adapter) {
|
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
// This layer hasn't been added to the konva state yet
|
|
||||||
const konvaLayer = new Konva.Layer({
|
|
||||||
id: entityState.id,
|
|
||||||
name: INPAINT_MASK_LAYER_NAME,
|
|
||||||
draggable: true,
|
draggable: true,
|
||||||
dragDistance: 0,
|
dragDistance: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||||
// the position - we do not need to call this on the `dragmove` event.
|
// the position - we do not need to call this on the `dragmove` event.
|
||||||
if (onPosChanged) {
|
this.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');
|
|
||||||
});
|
});
|
||||||
|
this.konvaObjectGroup = new Konva.Group({
|
||||||
|
id: getObjectGroupId(this.konvaLayer.id(), uuidv4()),
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
this.konvaLayer.add(this.konvaObjectGroup);
|
||||||
|
this.compositingRect = new Konva.Rect({ listening: false });
|
||||||
|
this.konvaLayer.add(this.compositingRect);
|
||||||
|
this.objects = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME);
|
destroy(): void {
|
||||||
return manager.add(entityState, konvaLayer, konvaObjectGroup);
|
this.konvaLayer.destroy();
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a function to render the inpaint mask.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @returns A function to render the inpaint mask
|
|
||||||
*/
|
|
||||||
export const getRenderInpaintMask = (manager: KonvaNodeManager) => {
|
|
||||||
const { getInpaintMaskState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi;
|
|
||||||
|
|
||||||
function renderInpaintMask(): void {
|
|
||||||
const entity = getInpaintMaskState();
|
|
||||||
const globalMaskLayerOpacity = getMaskOpacity();
|
|
||||||
const toolState = getToolState();
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
const adapter = getInpaintMask(manager, entity, onPosChanged);
|
|
||||||
|
|
||||||
|
async render(
|
||||||
|
inpaintMaskState: InpaintMaskEntity,
|
||||||
|
selectedTool: Tool,
|
||||||
|
selectedEntityIdentifier: CanvasEntityIdentifier | null,
|
||||||
|
maskOpacity: number
|
||||||
|
) {
|
||||||
// Update the layer's position and listening state
|
// Update the layer's position and listening state
|
||||||
adapter.konvaLayer.setAttrs({
|
this.konvaLayer.setAttrs({
|
||||||
listening: toolState.selected === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
listening: selectedTool === '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(inpaintMaskState.x),
|
||||||
y: Math.floor(entity.y),
|
y: Math.floor(inpaintMaskState.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(inpaintMaskState.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 = inpaintMaskState.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 object of this.objects.values()) {
|
||||||
if (!objectIds.includes(objectRecord.id)) {
|
if (!objectIds.includes(object.id)) {
|
||||||
adapter.destroy(objectRecord.id);
|
object.destroy();
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const obj of entity.objects) {
|
for (const obj of inpaintMaskState.objects) {
|
||||||
if (obj.type === 'brush_line') {
|
if (obj.type === 'brush_line') {
|
||||||
const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME);
|
let brushLine = this.objects.get(obj.id);
|
||||||
|
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||||
|
|
||||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
if (!brushLine) {
|
||||||
// array, so checking the length is sufficient to determine if we need to re-cache.
|
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
this.objects.set(brushLine.id, brushLine);
|
||||||
objectRecord.konvaLine.points(obj.points);
|
this.konvaLayer.add(brushLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
// Only update the color if it has changed.
|
|
||||||
if (objectRecord.konvaLine.stroke() !== rgbColor) {
|
if (obj.points.length !== brushLine.konvaLine.points().length) {
|
||||||
objectRecord.konvaLine.stroke(rgbColor);
|
brushLine.konvaLine.points(obj.points);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'eraser_line') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME);
|
let eraserLine = this.objects.get(obj.id);
|
||||||
|
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||||
|
|
||||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
if (!eraserLine) {
|
||||||
// array, so checking the length is sufficient to determine if we need to re-cache.
|
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
this.objects.set(eraserLine.id, eraserLine);
|
||||||
objectRecord.konvaLine.points(obj.points);
|
this.konvaLayer.add(eraserLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
// Only update the color if it has changed.
|
|
||||||
if (objectRecord.konvaLine.stroke() !== rgbColor) {
|
if (obj.points.length !== eraserLine.konvaLine.points().length) {
|
||||||
objectRecord.konvaLine.stroke(rgbColor);
|
eraserLine.konvaLine.points(obj.points);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'rect_shape') {
|
} else if (obj.type === 'rect_shape') {
|
||||||
const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME);
|
let rect = this.objects.get(obj.id);
|
||||||
|
assert(rect instanceof KonvaRect || rect === undefined);
|
||||||
|
|
||||||
// Only update the color if it has changed.
|
if (!rect) {
|
||||||
if (objectRecord.konvaRect.fill() !== rgbColor) {
|
rect = new KonvaRect({ rectShape: obj });
|
||||||
objectRecord.konvaRect.fill(rgbColor);
|
this.objects.set(rect.id, rect);
|
||||||
|
this.konvaLayer.add(rect.konvaRect);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update layer visibility if it has changed.
|
// Only update layer visibility if it has changed.
|
||||||
if (adapter.konvaLayer.visible() !== entity.isEnabled) {
|
if (this.konvaLayer.visible() !== inpaintMaskState.isEnabled) {
|
||||||
adapter.konvaLayer.visible(entity.isEnabled);
|
this.konvaLayer.visible(inpaintMaskState.isEnabled);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapter.konvaObjectGroup.getChildren().length === 0) {
|
if (this.objects.size === 0) {
|
||||||
// No objects - clear the cache to reset the previous pixel data
|
// No objects - clear the cache to reset the previous pixel data
|
||||||
adapter.konvaObjectGroup.clearCache();
|
this.konvaObjectGroup.clearCache();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compositingRect =
|
const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id;
|
||||||
adapter.konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer);
|
|
||||||
const isSelected = selectedEntity?.id === entity.id;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
|
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
|
||||||
@ -169,39 +141,40 @@ export const getRenderInpaintMask = (manager: KonvaNodeManager) => {
|
|||||||
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
|
* 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.
|
* a single raster image, and _then_ applied the 50% opacity.
|
||||||
*/
|
*/
|
||||||
if (isSelected && toolState.selected !== 'move') {
|
if (isSelected && selectedTool !== 'move') {
|
||||||
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
||||||
if (adapter.konvaObjectGroup.isCached()) {
|
if (this.konvaObjectGroup.isCached()) {
|
||||||
adapter.konvaObjectGroup.clearCache();
|
this.konvaObjectGroup.clearCache();
|
||||||
}
|
}
|
||||||
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
||||||
adapter.konvaObjectGroup.opacity(1);
|
this.konvaObjectGroup.opacity(1);
|
||||||
|
|
||||||
compositingRect.setAttrs({
|
this.compositingRect.setAttrs({
|
||||||
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
||||||
...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)),
|
...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox
|
||||||
|
? inpaintMaskState.bbox
|
||||||
|
: getLayerBboxFast(this.konvaLayer)),
|
||||||
fill: rgbColor,
|
fill: rgbColor,
|
||||||
opacity: globalMaskLayerOpacity,
|
opacity: maskOpacity,
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
globalCompositeOperation: 'source-in',
|
globalCompositeOperation: 'source-in',
|
||||||
visible: true,
|
visible: true,
|
||||||
// This rect must always be on top of all other shapes
|
// This rect must always be on top of all other shapes
|
||||||
zIndex: adapter.konvaObjectGroup.getChildren().length,
|
zIndex: this.objects.size + 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// The compositing rect should only be shown when the layer is selected.
|
// The compositing rect should only be shown when the layer is selected.
|
||||||
compositingRect.visible(false);
|
this.compositingRect.visible(false);
|
||||||
// Cache only if needed - or if we are on this code path and _don't_ have a cache
|
// Cache only if needed - or if we are on this code path and _don't_ have a cache
|
||||||
if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) {
|
if (groupNeedsCache || !this.konvaObjectGroup.isCached()) {
|
||||||
adapter.konvaObjectGroup.cache();
|
this.konvaObjectGroup.cache();
|
||||||
}
|
}
|
||||||
// Updating group opacity does not require re-caching
|
// Updating group opacity does not require re-caching
|
||||||
adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity);
|
this.konvaObjectGroup.opacity(maskOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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({
|
||||||
@ -217,6 +190,4 @@ export const getRenderInpaintMask = (manager: KonvaNodeManager) => {
|
|||||||
// bboxRect.visible(false);
|
// bboxRect.visible(false);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return renderInpaintMask;
|
|
||||||
};
|
|
||||||
|
@ -1,116 +1,116 @@
|
|||||||
import {
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
RASTER_LAYER_BRUSH_LINE_NAME,
|
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
||||||
RASTER_LAYER_ERASER_LINE_NAME,
|
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||||
RASTER_LAYER_IMAGE_NAME,
|
|
||||||
RASTER_LAYER_NAME,
|
|
||||||
RASTER_LAYER_OBJECT_GROUP_NAME,
|
|
||||||
RASTER_LAYER_RECT_SHAPE_NAME,
|
|
||||||
} from 'features/controlLayers/konva/naming';
|
|
||||||
import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import {
|
|
||||||
createImageObjectGroup,
|
|
||||||
createObjectGroup,
|
|
||||||
getBrushLine,
|
|
||||||
getEraserLine,
|
|
||||||
getRectShape,
|
|
||||||
} 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 { LayerEntity, Tool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
/**
|
export class KonvaLayerAdapter {
|
||||||
* Gets layer entity's konva nodes and entity adapter, creating them if they do not exist.
|
id: string;
|
||||||
* @param manager The konva node manager
|
konvaLayer: Konva.Layer;
|
||||||
* @param entity The layer entity state
|
konvaObjectGroup: Konva.Group;
|
||||||
* @param onPosChanged Callback for when the layer's position changes
|
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect | KonvaImage>;
|
||||||
* @returns The konva entity adapter for the layer
|
|
||||||
*/
|
constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) {
|
||||||
const getLayer = (
|
this.id = entity.id;
|
||||||
manager: KonvaNodeManager,
|
|
||||||
entity: LayerEntity,
|
this.konvaLayer = new Konva.Layer({
|
||||||
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
|
|
||||||
): KonvaEntityAdapter => {
|
|
||||||
const adapter = manager.get(entity.id);
|
|
||||||
if (adapter) {
|
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
// This layer hasn't been added to the konva state yet
|
|
||||||
const konvaLayer = new Konva.Layer({
|
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: RASTER_LAYER_NAME,
|
|
||||||
draggable: true,
|
draggable: true,
|
||||||
dragDistance: 0,
|
dragDistance: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||||
// the position - we do not need to call this on the `dragmove` event.
|
// the position - we do not need to call this on the `dragmove` event.
|
||||||
if (onPosChanged) {
|
this.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()) }, 'layer');
|
onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer');
|
||||||
});
|
});
|
||||||
|
const konvaObjectGroup = new Konva.Group({
|
||||||
|
id: getObjectGroupId(this.konvaLayer.id(), uuidv4()),
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
this.konvaObjectGroup = konvaObjectGroup;
|
||||||
|
this.konvaLayer.add(this.konvaObjectGroup);
|
||||||
|
this.objects = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME);
|
destroy(): void {
|
||||||
return manager.add(entity, konvaLayer, konvaObjectGroup);
|
this.konvaLayer.destroy();
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a layer.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @param entity The layer entity state
|
|
||||||
* @param tool The current tool
|
|
||||||
* @param onPosChanged Callback for when the layer's position changes
|
|
||||||
*/
|
|
||||||
export const renderLayer = async (
|
|
||||||
manager: KonvaNodeManager,
|
|
||||||
entity: LayerEntity,
|
|
||||||
tool: Tool,
|
|
||||||
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
|
|
||||||
) => {
|
|
||||||
const adapter = getLayer(manager, entity, onPosChanged);
|
|
||||||
|
|
||||||
|
async render(layerState: LayerEntity, selectedTool: Tool) {
|
||||||
// Update the layer's position and listening state
|
// Update the layer's position and listening state
|
||||||
adapter.konvaLayer.setAttrs({
|
this.konvaLayer.setAttrs({
|
||||||
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
listening: selectedTool === '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(layerState.x),
|
||||||
y: Math.floor(entity.y),
|
y: Math.floor(layerState.y),
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectIds = entity.objects.map(mapId);
|
const objectIds = layerState.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 object of this.objects.values()) {
|
||||||
if (!objectIds.includes(objectRecord.id)) {
|
if (!objectIds.includes(object.id)) {
|
||||||
adapter.destroy(objectRecord.id);
|
object.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const obj of entity.objects) {
|
for (const obj of layerState.objects) {
|
||||||
if (obj.type === 'brush_line') {
|
if (obj.type === 'brush_line') {
|
||||||
const objectRecord = getBrushLine(adapter, obj, RASTER_LAYER_BRUSH_LINE_NAME);
|
let brushLine = this.objects.get(obj.id);
|
||||||
// Only update the points if they have changed.
|
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
|
||||||
objectRecord.konvaLine.points(obj.points);
|
if (!brushLine) {
|
||||||
|
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||||
|
this.objects.set(brushLine.id, brushLine);
|
||||||
|
this.konvaLayer.add(brushLine.konvaLineGroup);
|
||||||
|
}
|
||||||
|
if (obj.points.length !== brushLine.konvaLine.points().length) {
|
||||||
|
brushLine.konvaLine.points(obj.points);
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'eraser_line') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
const objectRecord = getEraserLine(adapter, obj, RASTER_LAYER_ERASER_LINE_NAME);
|
let eraserLine = this.objects.get(obj.id);
|
||||||
// Only update the points if they have changed.
|
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
|
||||||
objectRecord.konvaLine.points(obj.points);
|
if (!eraserLine) {
|
||||||
|
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||||
|
this.objects.set(eraserLine.id, eraserLine);
|
||||||
|
this.konvaLayer.add(eraserLine.konvaLineGroup);
|
||||||
|
}
|
||||||
|
if (obj.points.length !== eraserLine.konvaLine.points().length) {
|
||||||
|
eraserLine.konvaLine.points(obj.points);
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'rect_shape') {
|
} else if (obj.type === 'rect_shape') {
|
||||||
getRectShape(adapter, obj, RASTER_LAYER_RECT_SHAPE_NAME);
|
let rect = this.objects.get(obj.id);
|
||||||
|
assert(rect instanceof KonvaRect || rect === undefined);
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
rect = new KonvaRect({ rectShape: obj });
|
||||||
|
this.objects.set(rect.id, rect);
|
||||||
|
this.konvaLayer.add(rect.konvaRect);
|
||||||
|
}
|
||||||
} else if (obj.type === 'image') {
|
} else if (obj.type === 'image') {
|
||||||
createImageObjectGroup({ adapter, obj, name: RASTER_LAYER_IMAGE_NAME });
|
let image = this.objects.get(obj.id);
|
||||||
|
assert(image instanceof KonvaImage || image === undefined);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
image = await new KonvaImage({ imageObject: obj });
|
||||||
|
this.objects.set(image.id, image);
|
||||||
|
this.konvaLayer.add(image.konvaImageGroup);
|
||||||
|
}
|
||||||
|
if (image.imageName !== obj.image.name) {
|
||||||
|
image.updateImageSource(obj.image.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update layer visibility if it has changed.
|
// Only update layer visibility if it has changed.
|
||||||
if (adapter.konvaLayer.visible() !== entity.isEnabled) {
|
if (this.konvaLayer.visible() !== layerState.isEnabled) {
|
||||||
adapter.konvaLayer.visible(entity.isEnabled);
|
this.konvaLayer.visible(layerState.isEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
|
// const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
|
||||||
|
|
||||||
// if (layerState.bbox) {
|
// if (layerState.bbox) {
|
||||||
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
|
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
|
||||||
// bboxRect.setAttrs({
|
// bboxRect.setAttrs({
|
||||||
@ -126,31 +126,6 @@ export const renderLayer = async (
|
|||||||
// } else {
|
// } else {
|
||||||
// bboxRect.visible(false);
|
// bboxRect.visible(false);
|
||||||
// }
|
// }
|
||||||
|
this.konvaObjectGroup.opacity(layerState.opacity);
|
||||||
adapter.konvaObjectGroup.opacity(entity.opacity);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a function to render all layers.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @returns A function to render all layers
|
|
||||||
*/
|
|
||||||
export const getRenderLayers = (manager: KonvaNodeManager) => {
|
|
||||||
const { getLayersState, getToolState, onPosChanged } = manager.stateApi;
|
|
||||||
|
|
||||||
function renderLayers(): void {
|
|
||||||
const { entities } = getLayersState();
|
|
||||||
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) {
|
|
||||||
renderLayer(manager, entity, tool.selected, onPosChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLayers;
|
|
||||||
};
|
|
||||||
|
@ -1,262 +1,11 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import {
|
import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming';
|
||||||
getLayerBboxId,
|
import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types';
|
||||||
getObjectGroupId,
|
|
||||||
IMAGE_PLACEHOLDER_NAME,
|
|
||||||
LAYER_BBOX_NAME,
|
|
||||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
|
||||||
} from 'features/controlLayers/konva/naming';
|
|
||||||
import type {
|
|
||||||
BrushLineObjectRecord,
|
|
||||||
EraserLineObjectRecord,
|
|
||||||
ImageObjectRecord,
|
|
||||||
KonvaEntityAdapter,
|
|
||||||
RectShapeObjectRecord,
|
|
||||||
} from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import type {
|
|
||||||
BrushLine,
|
|
||||||
CanvasEntity,
|
|
||||||
EraserLine,
|
|
||||||
ImageObject,
|
|
||||||
ImageWithDims,
|
|
||||||
RectShape,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images';
|
import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities to create various konva objects from layer state. These are used by both the raster and regional guidance
|
|
||||||
* layers types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a konva line for a brush line.
|
|
||||||
* @param brushLine The brush line state
|
|
||||||
* @param layerObjectGroup The konva layer's object group to add the line to
|
|
||||||
* @param name The konva name for the line
|
|
||||||
*/
|
|
||||||
export const getBrushLine = (
|
|
||||||
adapter: KonvaEntityAdapter,
|
|
||||||
brushLine: BrushLine,
|
|
||||||
name: string
|
|
||||||
): BrushLineObjectRecord => {
|
|
||||||
const objectRecord = adapter.get<BrushLineObjectRecord>(brushLine.id);
|
|
||||||
if (objectRecord) {
|
|
||||||
return objectRecord;
|
|
||||||
}
|
|
||||||
const { id, strokeWidth, clip, color } = brushLine;
|
|
||||||
const konvaLineGroup = new Konva.Group({
|
|
||||||
clip,
|
|
||||||
listening: false,
|
|
||||||
});
|
|
||||||
const konvaLine = new Konva.Line({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
strokeWidth,
|
|
||||||
tension: 0,
|
|
||||||
lineCap: 'round',
|
|
||||||
lineJoin: 'round',
|
|
||||||
shadowForStrokeEnabled: false,
|
|
||||||
globalCompositeOperation: 'source-over',
|
|
||||||
listening: false,
|
|
||||||
stroke: rgbaColorToString(color),
|
|
||||||
});
|
|
||||||
return adapter.add({ id, type: 'brush_line', konvaLine, konvaLineGroup });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a konva line for a eraser line.
|
|
||||||
* @param eraserLine The eraser line state
|
|
||||||
* @param layerObjectGroup The konva layer's object group to add the line to
|
|
||||||
* @param name The konva name for the line
|
|
||||||
*/
|
|
||||||
export const getEraserLine = (
|
|
||||||
adapter: KonvaEntityAdapter,
|
|
||||||
eraserLine: EraserLine,
|
|
||||||
name: string
|
|
||||||
): EraserLineObjectRecord => {
|
|
||||||
const objectRecord = adapter.get<EraserLineObjectRecord>(eraserLine.id);
|
|
||||||
if (objectRecord) {
|
|
||||||
return objectRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, strokeWidth, clip } = eraserLine;
|
|
||||||
const konvaLineGroup = new Konva.Group({
|
|
||||||
clip,
|
|
||||||
listening: false,
|
|
||||||
});
|
|
||||||
const konvaLine = new Konva.Line({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
strokeWidth,
|
|
||||||
tension: 0,
|
|
||||||
lineCap: 'round',
|
|
||||||
lineJoin: 'round',
|
|
||||||
shadowForStrokeEnabled: false,
|
|
||||||
globalCompositeOperation: 'destination-out',
|
|
||||||
listening: false,
|
|
||||||
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
|
|
||||||
});
|
|
||||||
return adapter.add({ id, type: 'eraser_line', konvaLine, konvaLineGroup });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a konva rect for a rect shape.
|
|
||||||
* @param rectShape The rect shape state
|
|
||||||
* @param layerObjectGroup The konva layer's object group to add the rect to
|
|
||||||
* @param name The konva name for the rect
|
|
||||||
*/
|
|
||||||
export const getRectShape = (
|
|
||||||
adapter: KonvaEntityAdapter,
|
|
||||||
rectShape: RectShape,
|
|
||||||
name: string
|
|
||||||
): RectShapeObjectRecord => {
|
|
||||||
const objectRecord = adapter.get<RectShapeObjectRecord>(rectShape.id);
|
|
||||||
if (objectRecord) {
|
|
||||||
return objectRecord;
|
|
||||||
}
|
|
||||||
const { id, x, y, width, height } = rectShape;
|
|
||||||
const konvaRect = new Konva.Rect({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
listening: false,
|
|
||||||
fill: rgbaColorToString(rectShape.color),
|
|
||||||
});
|
|
||||||
return adapter.add({ id: rectShape.id, type: 'rect_shape', konvaRect });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateImageSource = async (arg: {
|
|
||||||
objectRecord: ImageObjectRecord;
|
|
||||||
image: ImageWithDims;
|
|
||||||
getImageDTO?: (imageName: string) => Promise<ImageDTO | null>;
|
|
||||||
onLoading?: () => void;
|
|
||||||
onLoad?: (konvaImage: Konva.Image) => void;
|
|
||||||
onError?: () => void;
|
|
||||||
}) => {
|
|
||||||
const { objectRecord, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg;
|
|
||||||
|
|
||||||
try {
|
|
||||||
objectRecord.isLoading = true;
|
|
||||||
if (!objectRecord.konvaImage) {
|
|
||||||
objectRecord.konvaPlaceholderGroup.visible(true);
|
|
||||||
objectRecord.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image'));
|
|
||||||
}
|
|
||||||
onLoading?.();
|
|
||||||
|
|
||||||
const imageDTO = await getImageDTO(image.name);
|
|
||||||
if (!imageDTO) {
|
|
||||||
objectRecord.imageName = null;
|
|
||||||
objectRecord.isLoading = false;
|
|
||||||
objectRecord.isError = true;
|
|
||||||
objectRecord.konvaPlaceholderGroup.visible(true);
|
|
||||||
objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
|
|
||||||
onError?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const imageEl = new Image();
|
|
||||||
imageEl.onload = () => {
|
|
||||||
if (objectRecord.konvaImage) {
|
|
||||||
objectRecord.konvaImage.setAttrs({
|
|
||||||
image: imageEl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
objectRecord.konvaImage = new Konva.Image({
|
|
||||||
id: objectRecord.id,
|
|
||||||
listening: false,
|
|
||||||
image: imageEl,
|
|
||||||
});
|
|
||||||
objectRecord.konvaImageGroup.add(objectRecord.konvaImage);
|
|
||||||
objectRecord.imageName = image.name;
|
|
||||||
}
|
|
||||||
objectRecord.isLoading = false;
|
|
||||||
objectRecord.isError = false;
|
|
||||||
objectRecord.konvaPlaceholderGroup.visible(false);
|
|
||||||
onLoad?.(objectRecord.konvaImage);
|
|
||||||
};
|
|
||||||
imageEl.onerror = () => {
|
|
||||||
objectRecord.imageName = null;
|
|
||||||
objectRecord.isLoading = false;
|
|
||||||
objectRecord.isError = true;
|
|
||||||
objectRecord.konvaPlaceholderGroup.visible(true);
|
|
||||||
objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
|
|
||||||
onError?.();
|
|
||||||
};
|
|
||||||
imageEl.id = image.name;
|
|
||||||
imageEl.src = imageDTO.image_url;
|
|
||||||
} catch {
|
|
||||||
objectRecord.imageName = null;
|
|
||||||
objectRecord.isLoading = false;
|
|
||||||
objectRecord.isError = true;
|
|
||||||
objectRecord.konvaPlaceholderGroup.visible(true);
|
|
||||||
objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
|
|
||||||
onError?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an image placeholder group for an image object.
|
|
||||||
* @param image The image object state
|
|
||||||
* @returns The konva group for the image placeholder, and callbacks to handle loading and error states
|
|
||||||
*/
|
|
||||||
export const createImageObjectGroup = (arg: {
|
|
||||||
adapter: KonvaEntityAdapter;
|
|
||||||
obj: ImageObject;
|
|
||||||
name: string;
|
|
||||||
getImageDTO?: (imageName: string) => Promise<ImageDTO | null>;
|
|
||||||
onLoad?: (konvaImage: Konva.Image) => void;
|
|
||||||
onLoading?: () => void;
|
|
||||||
onError?: () => void;
|
|
||||||
}): ImageObjectRecord => {
|
|
||||||
const { adapter, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg;
|
|
||||||
let objectRecord = adapter.get<ImageObjectRecord>(obj.id);
|
|
||||||
if (objectRecord) {
|
|
||||||
return objectRecord;
|
|
||||||
}
|
|
||||||
const { id, image } = obj;
|
|
||||||
const { width, height } = obj;
|
|
||||||
const konvaImageGroup = new Konva.Group({ id, name, listening: false, x: obj.x, y: obj.y });
|
|
||||||
const konvaPlaceholderGroup = new Konva.Group({ name: IMAGE_PLACEHOLDER_NAME, listening: false });
|
|
||||||
const konvaPlaceholderRect = new Konva.Rect({
|
|
||||||
fill: 'hsl(220 12% 45% / 1)', // 'base.500'
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
listening: false,
|
|
||||||
});
|
|
||||||
const konvaPlaceholderText = new Konva.Text({
|
|
||||||
fill: 'hsl(220 12% 10% / 1)', // 'base.900'
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
align: 'center',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
fontFamily: '"Inter Variable", sans-serif',
|
|
||||||
fontSize: width / 16,
|
|
||||||
fontStyle: '600',
|
|
||||||
text: t('common.loadingImage', 'Loading Image'),
|
|
||||||
listening: false,
|
|
||||||
});
|
|
||||||
objectRecord = adapter.add({
|
|
||||||
id,
|
|
||||||
type: 'image',
|
|
||||||
konvaImageGroup,
|
|
||||||
konvaPlaceholderGroup,
|
|
||||||
konvaPlaceholderRect,
|
|
||||||
konvaPlaceholderText,
|
|
||||||
konvaImage: null,
|
|
||||||
imageName: null,
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
});
|
|
||||||
updateImageSource({ objectRecord, image, getImageDTO, onLoad, onLoading, onError });
|
|
||||||
return objectRecord;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a bounding box rect for a layer.
|
* Creates a bounding box rect for a layer.
|
||||||
@ -274,33 +23,217 @@ export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): K
|
|||||||
return rect;
|
return rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export class KonvaBrushLine {
|
||||||
* Creates a konva group for a layer's objects.
|
id: string;
|
||||||
* @param konvaLayer The konva layer to add the object group to
|
konvaLineGroup: Konva.Group;
|
||||||
* @param name The konva name for the group
|
konvaLine: Konva.Line;
|
||||||
* @returns
|
|
||||||
*/
|
constructor(arg: { brushLine: BrushLine }) {
|
||||||
export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva.Group => {
|
const { brushLine } = arg;
|
||||||
const konvaObjectGroup = new Konva.Group({
|
const { id, strokeWidth, clip, color } = brushLine;
|
||||||
id: getObjectGroupId(konvaLayer.id(), uuidv4()),
|
this.id = id;
|
||||||
name,
|
this.konvaLineGroup = new Konva.Group({
|
||||||
|
clip,
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
konvaLayer.add(konvaObjectGroup);
|
this.konvaLine = new Konva.Line({
|
||||||
return konvaObjectGroup;
|
id,
|
||||||
};
|
listening: false,
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
strokeWidth,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
globalCompositeOperation: 'source-over',
|
||||||
|
stroke: rgbaColorToString(color),
|
||||||
|
});
|
||||||
|
this.konvaLineGroup.add(this.konvaLine);
|
||||||
|
}
|
||||||
|
|
||||||
export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => {
|
destroy() {
|
||||||
const imageDimsPreview = new Konva.Rect({
|
this.konvaLineGroup.destroy();
|
||||||
id: PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
}
|
||||||
x: 0,
|
}
|
||||||
y: 0,
|
|
||||||
|
export class KonvaEraserLine {
|
||||||
|
id: string;
|
||||||
|
konvaLineGroup: Konva.Group;
|
||||||
|
konvaLine: Konva.Line;
|
||||||
|
|
||||||
|
constructor(arg: { eraserLine: EraserLine }) {
|
||||||
|
const { eraserLine } = arg;
|
||||||
|
const { id, strokeWidth, clip } = eraserLine;
|
||||||
|
this.id = id;
|
||||||
|
this.konvaLineGroup = new Konva.Group({
|
||||||
|
clip,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
this.konvaLine = new Konva.Line({
|
||||||
|
id,
|
||||||
|
listening: false,
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
strokeWidth,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
globalCompositeOperation: 'destination-out',
|
||||||
|
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
|
||||||
|
});
|
||||||
|
this.konvaLineGroup.add(this.konvaLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.konvaLineGroup.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KonvaRect {
|
||||||
|
id: string;
|
||||||
|
konvaRect: Konva.Rect;
|
||||||
|
|
||||||
|
constructor(arg: { rectShape: RectShape }) {
|
||||||
|
const { rectShape } = arg;
|
||||||
|
const { id, x, y, width, height } = rectShape;
|
||||||
|
this.id = id;
|
||||||
|
const konvaRect = new Konva.Rect({
|
||||||
|
id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
listening: false,
|
||||||
|
fill: rgbaColorToString(rectShape.color),
|
||||||
|
});
|
||||||
|
this.konvaRect = konvaRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.konvaRect.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KonvaImage {
|
||||||
|
id: string;
|
||||||
|
konvaImageGroup: Konva.Group;
|
||||||
|
konvaPlaceholderGroup: Konva.Group;
|
||||||
|
konvaPlaceholderRect: Konva.Rect;
|
||||||
|
konvaPlaceholderText: Konva.Text;
|
||||||
|
imageName: string | null;
|
||||||
|
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>;
|
||||||
|
onLoading: () => void;
|
||||||
|
onLoad: (imageName: string, imageEl: HTMLImageElement) => void;
|
||||||
|
onError: () => void;
|
||||||
|
|
||||||
|
constructor(arg: {
|
||||||
|
imageObject: ImageObject;
|
||||||
|
getImageDTO?: (imageName: string) => Promise<ImageDTO | null>;
|
||||||
|
onLoading?: () => void;
|
||||||
|
onLoad?: (konvaImage: Konva.Image) => void;
|
||||||
|
onError?: () => void;
|
||||||
|
}) {
|
||||||
|
const { imageObject, getImageDTO, onLoading, onLoad, onError } = arg;
|
||||||
|
const { id, width, height, x, y } = imageObject;
|
||||||
|
this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y });
|
||||||
|
this.konvaPlaceholderGroup = new Konva.Group({ listening: false });
|
||||||
|
this.konvaPlaceholderRect = new Konva.Rect({
|
||||||
|
fill: 'hsl(220 12% 45% / 1)', // 'base.500'
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
stroke: 'rgb(255,0,255)',
|
|
||||||
strokeWidth: 1 / konvaLayer.getStage().scaleX(),
|
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
konvaLayer.add(imageDimsPreview);
|
this.konvaPlaceholderText = new Konva.Text({
|
||||||
return imageDimsPreview;
|
fill: 'hsl(220 12% 10% / 1)', // 'base.900'
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
fontFamily: '"Inter Variable", sans-serif',
|
||||||
|
fontSize: width / 16,
|
||||||
|
fontStyle: '600',
|
||||||
|
text: t('common.loadingImage', 'Loading Image'),
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect);
|
||||||
|
this.konvaPlaceholderGroup.add(this.konvaPlaceholderText);
|
||||||
|
this.konvaImageGroup.add(this.konvaPlaceholderGroup);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.imageName = null;
|
||||||
|
this.konvaImage = null;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isError = false;
|
||||||
|
this.getImageDTO = getImageDTO ?? defaultGetImageDTO;
|
||||||
|
this.onLoading = function () {
|
||||||
|
this.isLoading = true;
|
||||||
|
if (!this.konvaImage) {
|
||||||
|
this.konvaPlaceholderGroup.visible(true);
|
||||||
|
this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image'));
|
||||||
|
}
|
||||||
|
if (onLoading) {
|
||||||
|
onLoading();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
this.onLoad = function (imageName: string, imageEl: HTMLImageElement) {
|
||||||
|
if (this.konvaImage) {
|
||||||
|
this.konvaImage.setAttrs({
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.konvaImage = new Konva.Image({
|
||||||
|
id: this.id,
|
||||||
|
listening: false,
|
||||||
|
image: imageEl,
|
||||||
|
});
|
||||||
|
this.konvaImageGroup.add(this.konvaImage);
|
||||||
|
this.imageName = imageName;
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isError = false;
|
||||||
|
this.konvaPlaceholderGroup.visible(false);
|
||||||
|
if (onLoad) {
|
||||||
|
onLoad(this.konvaImage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.onError = function () {
|
||||||
|
this.imageName = null;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isError = true;
|
||||||
|
this.konvaPlaceholderGroup.visible(true);
|
||||||
|
this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
|
||||||
|
if (onError) {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateImageSource(imageName: string) {
|
||||||
|
try {
|
||||||
|
this.onLoading();
|
||||||
|
|
||||||
|
const imageDTO = await this.getImageDTO(imageName);
|
||||||
|
if (!imageDTO) {
|
||||||
|
this.onError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imageEl = new Image();
|
||||||
|
imageEl.onload = () => {
|
||||||
|
this.onLoad(imageName, imageEl);
|
||||||
|
};
|
||||||
|
imageEl.onerror = () => {
|
||||||
|
this.onError();
|
||||||
|
};
|
||||||
|
imageEl.id = imageName;
|
||||||
|
imageEl.src = imageDTO.image_url;
|
||||||
|
} catch {
|
||||||
|
this.onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.konvaImageGroup.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,10 +19,13 @@ 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 { CanvasV2State } from 'features/controlLayers/store/types';
|
import { createImageObjectGroup, updateImageSource } from 'features/controlLayers/konva/renderers/objects';
|
||||||
|
import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types';
|
||||||
|
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the konva preview layer.
|
* Creates the konva preview layer.
|
||||||
@ -511,3 +514,230 @@ export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => {
|
|||||||
|
|
||||||
return renderDocumentOverlay;
|
return renderDocumentOverlay;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createStagingArea = (): KonvaNodeManager['preview']['stagingArea'] => {
|
||||||
|
const group = new Konva.Group({ id: 'staging_area_group', listening: false });
|
||||||
|
return { group, image: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRenderStagingArea = async (manager: KonvaNodeManager) => {
|
||||||
|
const { getStagingAreaState } = manager.stateApi;
|
||||||
|
const stagingArea = getStagingAreaState();
|
||||||
|
|
||||||
|
if (!stagingArea || stagingArea.selectedImageIndex === null) {
|
||||||
|
if (manager.preview.stagingArea.image) {
|
||||||
|
manager.preview.stagingArea.image.konvaImageGroup.visible(false);
|
||||||
|
manager.preview.stagingArea.image = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stagingArea.selectedImageIndex) {
|
||||||
|
const imageDTO = stagingArea.images[stagingArea.selectedImageIndex];
|
||||||
|
assert(imageDTO, 'Image must exist');
|
||||||
|
if (manager.preview.stagingArea.image) {
|
||||||
|
if (manager.preview.stagingArea.image.imageName !== imageDTO.image_name) {
|
||||||
|
await updateImageSource({
|
||||||
|
objectRecord: manager.preview.stagingArea.image,
|
||||||
|
image: imageDTOToImageWithDims(imageDTO),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manager.preview.stagingArea.image = await createImageObjectGroup({
|
||||||
|
obj: imageDTOToImageObject(imageDTO),
|
||||||
|
name: imageDTO.image_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class KonvaPreview {
|
||||||
|
konvaLayer: 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;
|
||||||
|
};
|
||||||
|
stagingArea: {
|
||||||
|
group: Konva.Group;
|
||||||
|
// image: KonvaImage | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
stage: Konva.Stage,
|
||||||
|
getBbox: () => IRect,
|
||||||
|
onBboxTransformed: (bbox: IRect) => void,
|
||||||
|
getShiftKey: () => boolean,
|
||||||
|
getCtrlKey: () => boolean,
|
||||||
|
getMetaKey: () => boolean,
|
||||||
|
getAltKey: () => boolean
|
||||||
|
) {
|
||||||
|
this.konvaLayer = createPreviewLayer();
|
||||||
|
this.bbox = createBboxNodes(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey);
|
||||||
|
this.tool = createToolPreviewNodes();
|
||||||
|
this.documentOverlay = createDocumentOverlay();
|
||||||
|
this.stagingArea = createStagingArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBbox(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) {
|
||||||
|
this.bbox.group.listening(toolState.selected === 'bbox');
|
||||||
|
// This updates the bbox during transformation
|
||||||
|
this.bbox.rect.setAttrs({
|
||||||
|
x: bbox.x,
|
||||||
|
y: bbox.y,
|
||||||
|
width: bbox.width,
|
||||||
|
height: bbox.height,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
listening: toolState.selected === 'bbox',
|
||||||
|
});
|
||||||
|
this.bbox.transformer.setAttrs({
|
||||||
|
listening: toolState.selected === 'bbox',
|
||||||
|
enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleToolPreview(stage: Konva.Stage, toolState: CanvasV2State['tool']) {
|
||||||
|
const scale = stage.scaleX();
|
||||||
|
const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2;
|
||||||
|
this.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
|
||||||
|
this.tool.brush.outerBorder.setAttrs({
|
||||||
|
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
|
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolPreview(
|
||||||
|
stage: Konva.Stage,
|
||||||
|
renderedEntityCount: number,
|
||||||
|
toolState: CanvasV2State['tool'],
|
||||||
|
currentFill: RgbaColor,
|
||||||
|
selectedEntity: CanvasEntity | null,
|
||||||
|
cursorPos: Position | null,
|
||||||
|
lastMouseDownPos: Position | null,
|
||||||
|
isDrawing: boolean,
|
||||||
|
isMouseDown: boolean
|
||||||
|
) {
|
||||||
|
const tool = toolState.selected;
|
||||||
|
const isDrawableEntity =
|
||||||
|
selectedEntity?.type === 'regional_guidance' ||
|
||||||
|
selectedEntity?.type === 'layer' ||
|
||||||
|
selectedEntity?.type === 'inpaint_mask';
|
||||||
|
|
||||||
|
// Update the stage's pointer style
|
||||||
|
if (tool === 'view') {
|
||||||
|
// View gets a hand
|
||||||
|
stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab';
|
||||||
|
} else if (renderedEntityCount === 0) {
|
||||||
|
// We have no layers, so we should not render any tool
|
||||||
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (!isDrawableEntity) {
|
||||||
|
// Non-drawable layers don't have tools
|
||||||
|
stage.container().style.cursor = 'not-allowed';
|
||||||
|
} else if (tool === 'move') {
|
||||||
|
// Move tool gets a pointer
|
||||||
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (tool === 'rect') {
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) {
|
||||||
|
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||||
|
this.tool.group.visible(false);
|
||||||
|
} else {
|
||||||
|
this.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;
|
||||||
|
this.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
|
||||||
|
this.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
|
||||||
|
|
||||||
|
// Update the outer border of the brush preview
|
||||||
|
this.tool.brush.outerBorder.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scaleToolPreview(stage, toolState);
|
||||||
|
|
||||||
|
this.tool.brush.group.visible(true);
|
||||||
|
} else {
|
||||||
|
this.tool.brush.group.visible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
||||||
|
this.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 {
|
||||||
|
this.tool.rect.rect.visible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDocumentOverlay(stage: Konva.Stage, document: CanvasV2State['document']) {
|
||||||
|
this.documentOverlay.group.zIndex(0);
|
||||||
|
|
||||||
|
const x = stage.x();
|
||||||
|
const y = stage.y();
|
||||||
|
const width = stage.width();
|
||||||
|
const height = stage.height();
|
||||||
|
const scale = stage.scaleX();
|
||||||
|
|
||||||
|
this.documentOverlay.outerRect.setAttrs({
|
||||||
|
offsetX: x / scale,
|
||||||
|
offsetY: y / scale,
|
||||||
|
width: width / scale,
|
||||||
|
height: height / scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.documentOverlay.innerRect.setAttrs({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: document.width,
|
||||||
|
height: document.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,171 +1,134 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import {
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
COMPOSITING_RECT_NAME,
|
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
||||||
RG_LAYER_BRUSH_LINE_NAME,
|
|
||||||
RG_LAYER_ERASER_LINE_NAME,
|
|
||||||
RG_LAYER_NAME,
|
|
||||||
RG_LAYER_OBJECT_GROUP_NAME,
|
|
||||||
RG_LAYER_RECT_SHAPE_NAME,
|
|
||||||
} from 'features/controlLayers/konva/naming';
|
|
||||||
import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
|
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
|
||||||
import {
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||||
createObjectGroup,
|
|
||||||
getBrushLine,
|
|
||||||
getEraserLine,
|
|
||||||
getRectShape,
|
|
||||||
} from 'features/controlLayers/konva/renderers/objects';
|
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types';
|
||||||
CanvasEntity,
|
|
||||||
CanvasEntityIdentifier,
|
|
||||||
PosChangedArg,
|
|
||||||
RegionEntity,
|
|
||||||
Tool,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
/**
|
export class KonvaRegion {
|
||||||
* Creates the "compositing rect" for a regional guidance layer.
|
id: string;
|
||||||
* @param konvaLayer The konva layer
|
konvaLayer: Konva.Layer;
|
||||||
*/
|
konvaObjectGroup: Konva.Group;
|
||||||
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
|
compositingRect: Konva.Rect;
|
||||||
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
|
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
||||||
konvaLayer.add(compositingRect);
|
|
||||||
return compositingRect;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
constructor(entity: RegionEntity, onPosChanged: StateApi['onPosChanged']) {
|
||||||
* Gets a region's konva nodes and entity adapter, creating them if they do not exist.
|
this.id = entity.id;
|
||||||
* @param stage The konva stage
|
|
||||||
* @param entity The regional guidance layer state
|
this.konvaLayer = new Konva.Layer({
|
||||||
* @param onLayerPosChanged Callback for when the layer's position changes
|
|
||||||
* @returns The konva entity adapter for the region
|
|
||||||
*/
|
|
||||||
const getRegion = (
|
|
||||||
manager: KonvaNodeManager,
|
|
||||||
entity: RegionEntity,
|
|
||||||
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
|
|
||||||
): KonvaEntityAdapter => {
|
|
||||||
const adapter = manager.get(entity.id);
|
|
||||||
if (adapter) {
|
|
||||||
return adapter;
|
|
||||||
}
|
|
||||||
// This layer hasn't been added to the konva state yet
|
|
||||||
const konvaLayer = new Konva.Layer({
|
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: RG_LAYER_NAME,
|
|
||||||
draggable: true,
|
draggable: true,
|
||||||
dragDistance: 0,
|
dragDistance: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||||
// the position - we do not need to call this on the `dragmove` event.
|
// the position - we do not need to call this on the `dragmove` event.
|
||||||
if (onPosChanged) {
|
this.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()) }, 'regional_guidance');
|
onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance');
|
||||||
});
|
});
|
||||||
|
this.konvaObjectGroup = new Konva.Group({
|
||||||
|
id: getObjectGroupId(this.konvaLayer.id(), uuidv4()),
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
this.konvaLayer.add(this.konvaObjectGroup);
|
||||||
|
this.compositingRect = new Konva.Rect({ listening: false });
|
||||||
|
this.konvaLayer.add(this.compositingRect);
|
||||||
|
this.objects = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME);
|
destroy(): void {
|
||||||
return manager.add(entity, konvaLayer, konvaObjectGroup);
|
this.konvaLayer.destroy();
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
async render(
|
||||||
* Renders a region.
|
regionState: RegionEntity,
|
||||||
* @param stage The konva stage
|
selectedTool: Tool,
|
||||||
* @param entity The regional guidance layer state
|
|
||||||
* @param globalMaskLayerOpacity The global mask layer opacity
|
|
||||||
* @param tool The current tool
|
|
||||||
* @param onPosChanged Callback for when the layer's position changes
|
|
||||||
*/
|
|
||||||
export const renderRegion = (
|
|
||||||
manager: KonvaNodeManager,
|
|
||||||
entity: RegionEntity,
|
|
||||||
globalMaskLayerOpacity: number,
|
|
||||||
tool: Tool,
|
|
||||||
selectedEntityIdentifier: CanvasEntityIdentifier | null,
|
selectedEntityIdentifier: CanvasEntityIdentifier | null,
|
||||||
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
|
maskOpacity: number
|
||||||
): void => {
|
) {
|
||||||
const adapter = getRegion(manager, entity, onPosChanged);
|
|
||||||
|
|
||||||
// Update the layer's position and listening state
|
// Update the layer's position and listening state
|
||||||
adapter.konvaLayer.setAttrs({
|
this.konvaLayer.setAttrs({
|
||||||
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
listening: selectedTool === '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(regionState.x),
|
||||||
y: Math.floor(entity.y),
|
y: Math.floor(regionState.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(regionState.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 = regionState.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 object of this.objects.values()) {
|
||||||
if (!objectIds.includes(objectRecord.id)) {
|
if (!objectIds.includes(object.id)) {
|
||||||
adapter.destroy(objectRecord.id);
|
object.destroy();
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const obj of entity.objects) {
|
for (const obj of regionState.objects) {
|
||||||
if (obj.type === 'brush_line') {
|
if (obj.type === 'brush_line') {
|
||||||
const objectRecord = getBrushLine(adapter, obj, RG_LAYER_BRUSH_LINE_NAME);
|
let brushLine = this.objects.get(obj.id);
|
||||||
|
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||||
|
|
||||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
if (!brushLine) {
|
||||||
// array, so checking the length is sufficient to determine if we need to re-cache.
|
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
this.objects.set(brushLine.id, brushLine);
|
||||||
objectRecord.konvaLine.points(obj.points);
|
this.konvaLayer.add(brushLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
// Only update the color if it has changed.
|
|
||||||
if (objectRecord.konvaLine.stroke() !== rgbColor) {
|
if (obj.points.length !== brushLine.konvaLine.points().length) {
|
||||||
objectRecord.konvaLine.stroke(rgbColor);
|
brushLine.konvaLine.points(obj.points);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'eraser_line') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
const objectRecord = getEraserLine(adapter, obj, RG_LAYER_ERASER_LINE_NAME);
|
let eraserLine = this.objects.get(obj.id);
|
||||||
|
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||||
|
|
||||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
if (!eraserLine) {
|
||||||
// array, so checking the length is sufficient to determine if we need to re-cache.
|
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||||
if (objectRecord.konvaLine.points().length !== obj.points.length) {
|
this.objects.set(eraserLine.id, eraserLine);
|
||||||
objectRecord.konvaLine.points(obj.points);
|
this.konvaLayer.add(eraserLine.konvaLineGroup);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
// Only update the color if it has changed.
|
|
||||||
if (objectRecord.konvaLine.stroke() !== rgbColor) {
|
if (obj.points.length !== eraserLine.konvaLine.points().length) {
|
||||||
objectRecord.konvaLine.stroke(rgbColor);
|
eraserLine.konvaLine.points(obj.points);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'rect_shape') {
|
} else if (obj.type === 'rect_shape') {
|
||||||
const objectRecord = getRectShape(adapter, obj, RG_LAYER_RECT_SHAPE_NAME);
|
let rect = this.objects.get(obj.id);
|
||||||
|
assert(rect instanceof KonvaRect || rect === undefined);
|
||||||
|
|
||||||
// Only update the color if it has changed.
|
if (!rect) {
|
||||||
if (objectRecord.konvaRect.fill() !== rgbColor) {
|
rect = new KonvaRect({ rectShape: obj });
|
||||||
objectRecord.konvaRect.fill(rgbColor);
|
this.objects.set(rect.id, rect);
|
||||||
|
this.konvaLayer.add(rect.konvaRect);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update layer visibility if it has changed.
|
// Only update layer visibility if it has changed.
|
||||||
if (adapter.konvaLayer.visible() !== entity.isEnabled) {
|
if (this.konvaLayer.visible() !== regionState.isEnabled) {
|
||||||
adapter.konvaLayer.visible(entity.isEnabled);
|
this.konvaLayer.visible(regionState.isEnabled);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapter.konvaObjectGroup.getChildren().length === 0) {
|
if (this.objects.size === 0) {
|
||||||
// No objects - clear the cache to reset the previous pixel data
|
// No objects - clear the cache to reset the previous pixel data
|
||||||
adapter.konvaObjectGroup.clearCache();
|
this.konvaObjectGroup.clearCache();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compositingRect =
|
const isSelected = selectedEntityIdentifier?.id === regionState.id;
|
||||||
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
|
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
|
||||||
@ -178,39 +141,38 @@ export const renderRegion = (
|
|||||||
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
|
* 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.
|
* a single raster image, and _then_ applied the 50% opacity.
|
||||||
*/
|
*/
|
||||||
if (isSelected && tool !== 'move') {
|
if (isSelected && selectedTool !== 'move') {
|
||||||
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
||||||
if (adapter.konvaObjectGroup.isCached()) {
|
if (this.konvaObjectGroup.isCached()) {
|
||||||
adapter.konvaObjectGroup.clearCache();
|
this.konvaObjectGroup.clearCache();
|
||||||
}
|
}
|
||||||
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
||||||
adapter.konvaObjectGroup.opacity(1);
|
this.konvaObjectGroup.opacity(1);
|
||||||
|
|
||||||
compositingRect.setAttrs({
|
this.compositingRect.setAttrs({
|
||||||
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
||||||
...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)),
|
...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)),
|
||||||
fill: rgbColor,
|
fill: rgbColor,
|
||||||
opacity: globalMaskLayerOpacity,
|
opacity: maskOpacity,
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
globalCompositeOperation: 'source-in',
|
globalCompositeOperation: 'source-in',
|
||||||
visible: true,
|
visible: true,
|
||||||
// This rect must always be on top of all other shapes
|
// This rect must always be on top of all other shapes
|
||||||
zIndex: adapter.konvaObjectGroup.getChildren().length,
|
zIndex: this.objects.size + 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// The compositing rect should only be shown when the layer is selected.
|
// The compositing rect should only be shown when the layer is selected.
|
||||||
compositingRect.visible(false);
|
this.compositingRect.visible(false);
|
||||||
// Cache only if needed - or if we are on this code path and _don't_ have a cache
|
// Cache only if needed - or if we are on this code path and _don't_ have a cache
|
||||||
if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) {
|
if (groupNeedsCache || !this.konvaObjectGroup.isCached()) {
|
||||||
adapter.konvaObjectGroup.cache();
|
this.konvaObjectGroup.cache();
|
||||||
}
|
}
|
||||||
// Updating group opacity does not require re-caching
|
// Updating group opacity does not require re-caching
|
||||||
adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity);
|
this.konvaObjectGroup.opacity(maskOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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({
|
||||||
@ -225,33 +187,5 @@ export const renderRegion = (
|
|||||||
// } else {
|
// } else {
|
||||||
// bboxRect.visible(false);
|
// bboxRect.visible(false);
|
||||||
// }
|
// }
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a function to render all regions.
|
|
||||||
* @param manager The konva node manager
|
|
||||||
* @returns A function to render all regions
|
|
||||||
*/
|
|
||||||
export const getRenderRegions = (manager: KonvaNodeManager) => {
|
|
||||||
const { getRegionsState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi;
|
|
||||||
|
|
||||||
function renderRegions(): void {
|
|
||||||
const { entities } = getRegionsState();
|
|
||||||
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) {
|
|
||||||
renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderRegions;
|
|
||||||
};
|
|
||||||
|
@ -5,24 +5,9 @@ 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, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { getArrangeEntities } from 'features/controlLayers/konva/renderers/arrange';
|
import { KonvaBackground } 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 { getRenderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters';
|
import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview';
|
||||||
import { getRenderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask';
|
|
||||||
import { getRenderLayers } from 'features/controlLayers/konva/renderers/layers';
|
|
||||||
import {
|
|
||||||
createBboxNodes,
|
|
||||||
createDocumentOverlay,
|
|
||||||
createPreviewLayer,
|
|
||||||
createToolPreviewNodes,
|
|
||||||
getRenderBbox,
|
|
||||||
getRenderDocumentOverlay,
|
|
||||||
getRenderToolPreview,
|
|
||||||
} from 'features/controlLayers/konva/renderers/preview';
|
|
||||||
import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions';
|
|
||||||
import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage';
|
|
||||||
import { createStagingArea, getRenderStagingArea } from 'features/controlLayers/konva/renderers/stagingArea';
|
|
||||||
import {
|
import {
|
||||||
$stageAttrs,
|
$stageAttrs,
|
||||||
bboxChanged,
|
bboxChanged,
|
||||||
@ -299,23 +284,7 @@ export const initializeRenderer = (
|
|||||||
spaceKey = val;
|
spaceKey = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
const manager = new KonvaNodeManager(stage, container);
|
const stateApi: KonvaNodeManager['stateApi'] = {
|
||||||
setNodeManager(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(),
|
|
||||||
stagingArea: createStagingArea(),
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
manager.stateApi = {
|
|
||||||
// Read-only state
|
// Read-only state
|
||||||
getToolState,
|
getToolState,
|
||||||
getSelectedEntity,
|
getSelectedEntity,
|
||||||
@ -365,6 +334,26 @@ export const initializeRenderer = (
|
|||||||
onLayerImageCached,
|
onLayerImageCached,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const manager = new KonvaNodeManager(stage, container, stateApi);
|
||||||
|
setNodeManager(manager);
|
||||||
|
console.log(manager);
|
||||||
|
|
||||||
|
manager.background = new KonvaBackground();
|
||||||
|
manager.stage.add(manager.background.konvaLayer);
|
||||||
|
manager.preview = new KonvaPreview({
|
||||||
|
stage,
|
||||||
|
getBbox,
|
||||||
|
onBboxTransformed,
|
||||||
|
getShiftKey: $shift.get,
|
||||||
|
getCtrlKey: $ctrl.get,
|
||||||
|
getMetaKey: $meta.get,
|
||||||
|
getAltKey: $alt.get,
|
||||||
|
});
|
||||||
|
manager.preview.konvaLayer.add(manager.preview.bbox.group);
|
||||||
|
manager.preview.konvaLayer.add(manager.preview.tool.group);
|
||||||
|
manager.preview.konvaLayer.add(manager.preview.documentOverlay.group);
|
||||||
|
manager.stage.add(manager.preview.konvaLayer);
|
||||||
|
|
||||||
const cleanupListeners = setStageEventHandlers(manager);
|
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.
|
||||||
@ -372,21 +361,6 @@ 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.konvaApi = {
|
|
||||||
renderRegions: getRenderRegions(manager),
|
|
||||||
renderLayers: getRenderLayers(manager),
|
|
||||||
renderControlAdapters: getRenderControlAdapters(manager),
|
|
||||||
renderInpaintMask: getRenderInpaintMask(manager),
|
|
||||||
renderBbox: getRenderBbox(manager),
|
|
||||||
renderToolPreview: getRenderToolPreview(manager),
|
|
||||||
renderDocumentOverlay: getRenderDocumentOverlay(manager),
|
|
||||||
renderStagingArea: getRenderStagingArea(manager),
|
|
||||||
renderBackground: getRenderBackground(manager),
|
|
||||||
arrangeEntities: getArrangeEntities(manager),
|
|
||||||
fitDocumentToStage: getFitDocumentToStage(manager),
|
|
||||||
fitStageToContainer: getFitStageToContainer(manager),
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCanvas = () => {
|
const renderCanvas = () => {
|
||||||
canvasV2 = store.getState().canvasV2;
|
canvasV2 = store.getState().canvasV2;
|
||||||
|
|
||||||
@ -404,7 +378,7 @@ export const initializeRenderer = (
|
|||||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
||||||
) {
|
) {
|
||||||
logIfDebugging('Rendering layers');
|
logIfDebugging('Rendering layers');
|
||||||
manager.konvaApi.renderLayers();
|
manager.renderLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -414,7 +388,7 @@ export const initializeRenderer = (
|
|||||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
||||||
) {
|
) {
|
||||||
logIfDebugging('Rendering regions');
|
logIfDebugging('Rendering regions');
|
||||||
manager.konvaApi.renderRegions();
|
manager.renderRegions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -424,22 +398,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.konvaApi.renderInpaintMask();
|
manager.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.konvaApi.renderControlAdapters();
|
manager.renderControlAdapters();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
|
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
|
||||||
logIfDebugging('Rendering document bounds overlay');
|
logIfDebugging('Rendering document bounds overlay');
|
||||||
manager.konvaApi.renderDocumentOverlay();
|
manager.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.konvaApi.renderBbox();
|
manager.renderBbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -459,7 +433,7 @@ export const initializeRenderer = (
|
|||||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities
|
canvasV2.regions.entities !== prevCanvasV2.regions.entities
|
||||||
) {
|
) {
|
||||||
logIfDebugging('Arranging entities');
|
logIfDebugging('Arranging entities');
|
||||||
manager.konvaApi.arrangeEntities();
|
manager.arrangeEntities();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevCanvasV2 = canvasV2;
|
prevCanvasV2 = canvasV2;
|
||||||
@ -473,16 +447,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 resizeObserver = new ResizeObserver(manager.konvaApi.fitStageToContainer);
|
const resizeObserver = new ResizeObserver(manager.fitStageToContainer);
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
manager.konvaApi.fitStageToContainer();
|
manager.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.konvaApi.fitDocumentToStage();
|
manager.fitDocumentToStage();
|
||||||
manager.konvaApi.renderToolPreview();
|
manager.renderToolPreview();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -45,7 +45,7 @@ export const getFitStageToContainer = (manager: KonvaNodeManager) => {
|
|||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.konvaApi.renderBackground();
|
manager.konvaApi.renderBackground();
|
||||||
manager.konvaApi.renderDocumentOverlay();
|
manager.renderDocumentOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
return fitStageToContainer;
|
return fitStageToContainer;
|
||||||
|
@ -2,7 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|||||||
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
||||||
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
||||||
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
import { DEFAULT_RGBA_COLOR, imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
@ -309,7 +309,7 @@ export const regionsReducers = {
|
|||||||
},
|
},
|
||||||
rgBrushLineAdded: {
|
rgBrushLineAdded: {
|
||||||
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
|
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
|
||||||
const { id, points, lineId, color, width, clip } = action.payload;
|
const { id, points, lineId, width, clip } = action.payload;
|
||||||
const rg = selectRG(state, id);
|
const rg = selectRG(state, id);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
return;
|
return;
|
||||||
@ -319,7 +319,7 @@ export const regionsReducers = {
|
|||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points,
|
points,
|
||||||
strokeWidth: width,
|
strokeWidth: width,
|
||||||
color,
|
color: DEFAULT_RGBA_COLOR,
|
||||||
clip,
|
clip,
|
||||||
});
|
});
|
||||||
rg.bboxNeedsUpdate = true;
|
rg.bboxNeedsUpdate = true;
|
||||||
@ -366,7 +366,7 @@ export const regionsReducers = {
|
|||||||
},
|
},
|
||||||
rgRectAdded: {
|
rgRectAdded: {
|
||||||
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
|
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
|
||||||
const { id, rect, rectId, color } = action.payload;
|
const { id, rect, rectId } = action.payload;
|
||||||
if (rect.height === 0 || rect.width === 0) {
|
if (rect.height === 0 || rect.width === 0) {
|
||||||
// Ignore zero-area rectangles
|
// Ignore zero-area rectangles
|
||||||
return;
|
return;
|
||||||
@ -379,7 +379,7 @@ export const regionsReducers = {
|
|||||||
type: 'rect_shape',
|
type: 'rect_shape',
|
||||||
id: getRectShapeId(id, rectId),
|
id: getRectShapeId(id, rectId),
|
||||||
...rect,
|
...rect,
|
||||||
color,
|
color: DEFAULT_RGBA_COLOR,
|
||||||
});
|
});
|
||||||
rg.bboxNeedsUpdate = true;
|
rg.bboxNeedsUpdate = true;
|
||||||
rg.imageCache = null;
|
rg.imageCache = null;
|
||||||
|
Loading…
Reference in New Issue
Block a user