tidy(ui): organise renderers

This commit is contained in:
psychedelicious 2024-06-19 00:18:44 +10:00
parent dd09723a2a
commit 995c26751e
11 changed files with 196 additions and 195 deletions

View File

@ -3,7 +3,7 @@ import {
renderDocumentBoundsOverlay,
renderToolPreview,
scaleToolPreview,
} from 'features/controlLayers/konva/renderers/previewLayer';
} from 'features/controlLayers/konva/renderers/preview';
import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type {

View File

@ -0,0 +1,23 @@
import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types';
import type Konva from 'konva';
export const arrangeEntities = (
stage: Konva.Stage,
layers: LayerEntity[],
controlAdapters: ControlAdapterEntity[],
regions: RegionEntity[]
): void => {
let zIndex = 0;
stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex);
for (const layer of layers) {
stage.findOne<Konva.Layer>(`#${layer.id}`)?.zIndex(++zIndex);
}
for (const ca of controlAdapters) {
stage.findOne<Konva.Layer>(`#${ca.id}`)?.zIndex(++zIndex);
}
for (const rg of regions) {
stage.findOne<Konva.Layer>(`#${rg.id}`)?.zIndex(++zIndex);
}
stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex);
};

View File

@ -1,5 +1,5 @@
import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap';
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
import type { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap';
import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming';
import type { ControlAdapterEntity } from 'features/controlLayers/store/types';
import Konva from 'konva';
@ -49,7 +49,7 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement):
* @param ca The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const updateCALayerImageSource = async (
const updateControlAdapterImageSource = async (
stage: Konva.Stage,
konvaLayer: Konva.Layer,
ca: ControlAdapterEntity,
@ -74,7 +74,7 @@ const updateCALayerImageSource = async (
id: imageId,
image: imageEl,
});
updateCALayerImageAttrs(stage, konvaImage, ca);
updateControlAdapterImageAttrs(stage, konvaImage, ca);
// Must cache after this to apply the filters
konvaImage.cache();
imageEl.id = imageId;
@ -92,7 +92,7 @@ const updateCALayerImageSource = async (
* @param ca The control adapter layer state
*/
const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => {
const updateControlAdapterImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => {
let needsCache = false;
// TODO(psyche): `node.filters()` returns null if no filters; report upstream
const filters = konvaImage.filters() ?? [];
@ -128,7 +128,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca
* @param ca The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
export const renderCALayer = (
export const renderControlAdapter = (
stage: Konva.Stage,
controlAdapterMap: EntityToKonvaMap,
ca: ControlAdapterEntity,
@ -152,9 +152,9 @@ export const renderCALayer = (
}
if (imageSourceNeedsUpdate) {
updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO);
updateControlAdapterImageSource(stage, konvaLayer, ca, getImageDTO);
} else if (konvaImage) {
updateCALayerImageAttrs(stage, konvaImage, ca);
updateControlAdapterImageAttrs(stage, konvaImage, ca);
}
};
@ -171,6 +171,6 @@ export const renderControlAdapters = (
}
}
for (const ca of controlAdapters) {
renderCALayer(stage, controlAdapterMap, ca, getImageDTO);
renderControlAdapter(stage, controlAdapterMap, ca, getImageDTO);
}
};

View File

@ -1,23 +1,157 @@
import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap';
import {
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME,
RASTER_LAYER_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME,
RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming';
import {
createImageObjectGroup,
createObjectGroup,
getBrushLine,
getEraserLine,
getRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
export const arrangeEntities = (
/**
* Logic for creating and rendering raster layers.
*/
/**
* Creates a raster layer.
* @param stage The konva stage
* @param layerState The raster layer state
* @param onPosChanged Callback for when the layer's position changes
*/
const getLayer = (
stage: Konva.Stage,
layers: LayerEntity[],
controlAdapters: ControlAdapterEntity[],
regions: RegionEntity[]
): void => {
let zIndex = 0;
stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex);
for (const layer of layers) {
stage.findOne<Konva.Layer>(`#${layer.id}`)?.zIndex(++zIndex);
layerMap: EntityToKonvaMap,
layerState: LayerEntity,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): EntityToKonvaMapping => {
let mapping = layerMap.getMapping(layerState.id);
if (mapping) {
return mapping;
}
for (const ca of controlAdapters) {
stage.findOne<Konva.Layer>(`#${ca.id}`)?.zIndex(++zIndex);
// This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({
id: layerState.id,
name: RASTER_LAYER_NAME,
draggable: true,
dragDistance: 0,
});
// 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.
if (onPosChanged) {
konvaLayer.on('dragend', function (e) {
onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer');
});
}
const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME);
konvaLayer.add(konvaObjectGroup);
stage.add(konvaLayer);
mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup);
return mapping;
};
/**
* Renders a regional guidance layer.
* @param stage The konva stage
* @param layerState The regional guidance layer state
* @param tool The current tool
* @param onPosChanged Callback for when the layer's position changes
*/
export const renderLayer = async (
stage: Konva.Stage,
layerMap: EntityToKonvaMap,
layerState: LayerEntity,
tool: Tool,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
) => {
const mapping = getLayer(stage, layerMap, layerState, onPosChanged);
// Update the layer's position and listening state
mapping.konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(layerState.x),
y: Math.floor(layerState.y),
});
const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in state
for (const entry of mapping.getEntries()) {
if (!objectIds.includes(entry.id)) {
mapping.destroyEntry(entry.id);
}
}
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed.
if (entry.konvaLine.points().length !== obj.points.length) {
entry.konvaLine.points(obj.points);
}
} else if (obj.type === 'eraser_line') {
const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed.
if (entry.konvaLine.points().length !== obj.points.length) {
entry.konvaLine.points(obj.points);
}
} else if (obj.type === 'rect_shape') {
getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME);
} else if (obj.type === 'image') {
createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME);
}
}
// Only update layer visibility if it has changed.
if (mapping.konvaLayer.visible() !== layerState.isEnabled) {
mapping.konvaLayer.visible(layerState.isEnabled);
}
// const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
// if (layerState.bbox) {
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
// bboxRect.setAttrs({
// visible: active,
// listening: active,
// x: layerState.bbox.x,
// y: layerState.bbox.y,
// width: layerState.bbox.width,
// height: layerState.bbox.height,
// stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
// strokeWidth: 1 / stage.scaleX(),
// });
// } else {
// bboxRect.visible(false);
// }
mapping.konvaObjectGroup.opacity(layerState.opacity);
};
export const renderLayers = (
stage: Konva.Stage,
layerMap: EntityToKonvaMap,
layers: LayerEntity[],
tool: Tool,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => {
// Destroy nonexistent layers
for (const mapping of layerMap.getMappings()) {
if (!layers.find((l) => l.id === mapping.id)) {
layerMap.destroyMapping(mapping.id);
}
}
for (const layer of layers) {
renderLayer(stage, layerMap, layer, tool, onPosChanged);
}
for (const rg of regions) {
stage.findOne<Konva.Layer>(`#${rg.id}`)?.zIndex(++zIndex);
}
stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex);
};

View File

@ -5,7 +5,7 @@ import type {
EraserLineEntry,
ImageEntry,
RectShapeEntry,
} from 'features/controlLayers/konva/konvaMap';
} from 'features/controlLayers/konva/entityToKonvaMap';
import {
getLayerBboxId,
getObjectGroupId,

View File

@ -1,157 +0,0 @@
import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap';
import {
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME,
RASTER_LAYER_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME,
RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming';
import {
createImageObjectGroup,
createObjectGroup,
getBrushLine,
getEraserLine,
getRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { mapId } from 'features/controlLayers/konva/util';
import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
/**
* Logic for creating and rendering raster layers.
*/
/**
* Creates a raster layer.
* @param stage The konva stage
* @param layerState The raster layer state
* @param onPosChanged Callback for when the layer's position changes
*/
const getLayer = (
stage: Konva.Stage,
layerMap: EntityToKonvaMap,
layerState: LayerEntity,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): EntityToKonvaMapping => {
let mapping = layerMap.getMapping(layerState.id);
if (mapping) {
return mapping;
}
// This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({
id: layerState.id,
name: RASTER_LAYER_NAME,
draggable: true,
dragDistance: 0,
});
// 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.
if (onPosChanged) {
konvaLayer.on('dragend', function (e) {
onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer');
});
}
const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME);
konvaLayer.add(konvaObjectGroup);
stage.add(konvaLayer);
mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup);
return mapping;
};
/**
* Renders a regional guidance layer.
* @param stage The konva stage
* @param layerState The regional guidance layer state
* @param tool The current tool
* @param onPosChanged Callback for when the layer's position changes
*/
export const renderRasterLayer = async (
stage: Konva.Stage,
layerMap: EntityToKonvaMap,
layerState: LayerEntity,
tool: Tool,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
) => {
const mapping = getLayer(stage, layerMap, layerState, onPosChanged);
// Update the layer's position and listening state
mapping.konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(layerState.x),
y: Math.floor(layerState.y),
});
const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in state
for (const entry of mapping.getEntries()) {
if (!objectIds.includes(entry.id)) {
mapping.destroyEntry(entry.id);
}
}
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed.
if (entry.konvaLine.points().length !== obj.points.length) {
entry.konvaLine.points(obj.points);
}
} else if (obj.type === 'eraser_line') {
const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed.
if (entry.konvaLine.points().length !== obj.points.length) {
entry.konvaLine.points(obj.points);
}
} else if (obj.type === 'rect_shape') {
getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME);
} else if (obj.type === 'image') {
createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME);
}
}
// Only update layer visibility if it has changed.
if (mapping.konvaLayer.visible() !== layerState.isEnabled) {
mapping.konvaLayer.visible(layerState.isEnabled);
}
// const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
// if (layerState.bbox) {
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
// bboxRect.setAttrs({
// visible: active,
// listening: active,
// x: layerState.bbox.x,
// y: layerState.bbox.y,
// width: layerState.bbox.width,
// height: layerState.bbox.height,
// stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
// strokeWidth: 1 / stage.scaleX(),
// });
// } else {
// bboxRect.visible(false);
// }
mapping.konvaObjectGroup.opacity(layerState.opacity);
};
export const renderLayers = (
stage: Konva.Stage,
layerMap: EntityToKonvaMap,
layers: LayerEntity[],
tool: Tool,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => {
// Destroy nonexistent layers
for (const mapping of layerMap.getMappings()) {
if (!layers.find((l) => l.id === mapping.id)) {
layerMap.destroyMapping(mapping.id);
}
}
for (const layer of layers) {
renderRasterLayer(stage, layerMap, layer, tool, onPosChanged);
}
};

View File

@ -1,5 +1,5 @@
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap';
import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap';
import {
COMPOSITING_RECT_NAME,
RG_LAYER_BRUSH_LINE_NAME,
@ -90,7 +90,7 @@ const getRegion = (
* @param tool The current tool
* @param onPosChanged Callback for when the layer's position changes
*/
export const renderRGLayer = (
export const renderRegion = (
stage: Konva.Stage,
regionMap: EntityToKonvaMap,
region: RegionEntity,
@ -255,6 +255,6 @@ export const renderRegions = (
}
}
for (const rg of regions) {
renderRGLayer(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged);
renderRegion(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged);
}
};

View File

@ -3,19 +3,19 @@ import type { Store } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import type { RootState } from 'app/store/store';
import { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap';
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap';
import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange';
import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background';
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer';
import { arrangeEntities } from 'features/controlLayers/konva/renderers/layers';
import { renderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters';
import { renderLayers } from 'features/controlLayers/konva/renderers/layers';
import {
renderBboxPreview,
renderDocumentBoundsOverlay,
scaleToolPreview,
} from 'features/controlLayers/konva/renderers/previewLayer';
import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer';
} from 'features/controlLayers/konva/renderers/preview';
import { renderRegions } from 'features/controlLayers/konva/renderers/regions';
import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage';
import {
$stageAttrs,
@ -55,6 +55,7 @@ import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
import { getImageDTO } from 'services/api/endpoints/images';
/**
* Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the
* react rendering cycle entirely, improving canvas performance.

View File

@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer';
import { renderRegions } from 'features/controlLayers/konva/renderers/regions';
import { blobToDataURL } from 'features/controlLayers/konva/util';
import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice';
import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types';