refactor(ui): fix renderer stuff

This commit is contained in:
psychedelicious 2024-06-16 11:50:14 +10:00
parent 426f1b6f9a
commit 04a44c8ea7
2 changed files with 32 additions and 172 deletions

View File

@ -1,13 +1,13 @@
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
import {
CA_LAYER_IMAGE_NAME,
LAYER_BBOX_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME,
RG_LAYER_OBJECT_GROUP_NAME,
} from 'features/controlLayers/konva/naming';
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
import type { LayerData } from 'features/controlLayers/store/types';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
@ -175,37 +175,52 @@ export const getLayerBboxFast = (layer: Konva.Layer): IRect => {
};
const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME;
const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME;
const filterLayerChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME;
const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER_IMAGE_NAME;
/**
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
* @param stage The konva stage
* @param layerStates An array of layers to calculate bboxes for
* @param entityStates An array of layers to calculate bboxes for
* @param onBboxChanged Callback for when the bounding box changes
*/
export const updateBboxes = (
stage: Konva.Stage,
layerStates: LayerData[],
entityStates: (ControlAdapterData | LayerData | RegionalGuidanceData)[],
onBboxChanged: (layerId: string, bbox: IRect | null) => void
): void => {
for (const layerState of layerStates) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`);
assert(konvaLayer, `Layer ${layerState.id} not found in stage`);
for (const entityState of entityStates) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${entityState.id}`);
assert(konvaLayer, `Layer ${entityState.id} not found in stage`);
// We only need to recalculate the bbox if the layer has changed
if (layerState.bboxNeedsUpdate) {
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
if (entityState.bboxNeedsUpdate) {
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(entityState, konvaLayer);
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
const visible = bboxRect.visible();
bboxRect.visible(false);
if (layerState.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged(layerState.id, null);
} else {
// Calculate the bbox by rendering the layer and checking its pixels
const filterChildren = isRegionalGuidanceLayer(layerState) ? filterRGChildren : filterRasterChildren;
onBboxChanged(layerState.id, getLayerBboxPixels(konvaLayer, filterChildren));
if (entityState.type === 'layer') {
if (entityState.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged(entityState.id, null);
} else {
onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterLayerChildren));
}
} else if (entityState.type === 'control_adapter') {
if (!entityState.image && !entityState.processedImage) {
// No objects - no bbox to calculate
onBboxChanged(entityState.id, null);
} else {
onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterCAChildren));
}
} else if (entityState.type === 'regional_guidance') {
if (entityState.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged(entityState.id, null);
} else {
onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterRGChildren));
}
}
// Restore the visibility of the bbox

View File

@ -1,155 +0,0 @@
import {
getCALayerImageId,
getIILayerImageId,
INITIAL_IMAGE_LAYER_IMAGE_NAME,
INITIAL_IMAGE_LAYER_NAME,
} from 'features/controlLayers/konva/naming';
import type { InitialImageLayer } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { ImageDTO } from 'services/api/types';
/**
* Logic for creating and rendering initial image layers. Well, just the one, actually, because it's a singleton.
* TODO(psyche): Raster layers effectively supersede the initial image layer type.
*/
/**
* Creates an initial image konva layer.
* @param stage The konva stage
* @param layerState The initial image layer state
*/
const createIILayer = (stage: Konva.Stage, layerState: InitialImageLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({
id: layerState.id,
name: INITIAL_IMAGE_LAYER_NAME,
imageSmoothingEnabled: true,
listening: false,
});
stage.add(konvaLayer);
return konvaLayer;
};
/**
* Creates the konva image for an initial image layer.
* @param konvaLayer The konva layer
* @param imageEl The image element
*/
const createIILayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): Konva.Image => {
const konvaImage = new Konva.Image({
name: INITIAL_IMAGE_LAYER_IMAGE_NAME,
image: imageEl,
});
konvaLayer.add(konvaImage);
return konvaImage;
};
/**
* Updates an initial image layer's attributes (width, height, opacity, visibility).
* @param stage The konva stage
* @param konvaImage The konva image
* @param layerState The initial image layer state
*/
const updateIILayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, layerState: InitialImageLayer): void => {
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
// but it doesn't seem to break anything.
// TODO(psyche): Investigate and report upstream.
const newWidth = stage.width() / stage.scaleX();
const newHeight = stage.height() / stage.scaleY();
if (
konvaImage.width() !== newWidth ||
konvaImage.height() !== newHeight ||
konvaImage.visible() !== layerState.isEnabled
) {
konvaImage.setAttrs({
opacity: layerState.opacity,
scaleX: 1,
scaleY: 1,
width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(),
visible: layerState.isEnabled,
});
}
if (konvaImage.opacity() !== layerState.opacity) {
konvaImage.opacity(layerState.opacity);
}
};
/**
* Update an initial image layer's image source when the image changes.
* @param stage The konva stage
* @param konvaLayer The konva layer
* @param layerState The initial image layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
const updateIILayerImageSource = async (
stage: Konva.Stage,
konvaLayer: Konva.Layer,
layerState: InitialImageLayer,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): Promise<void> => {
if (layerState.image) {
const imageName = layerState.image.name;
const imageDTO = await getImageDTO(imageName);
if (!imageDTO) {
return;
}
const imageEl = new Image();
const imageId = getIILayerImageId(layerState.id, imageName);
imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage =
konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ??
createIILayerImage(konvaLayer, imageEl);
// Update the image's attributes
konvaImage.setAttrs({
id: imageId,
image: imageEl,
});
updateIILayerImageAttrs(stage, konvaImage, layerState);
imageEl.id = imageId;
};
imageEl.src = imageDTO.image_url;
} else {
konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy();
}
};
/**
* Renders an initial image layer.
* @param stage The konva stage
* @param layerState The initial image layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/
export const renderIILayer = (
stage: Konva.Stage,
layerState: InitialImageLayer,
zIndex: number,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): void => {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createIILayer(stage, layerState);
konvaLayer.zIndex(zIndex);
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`);
const canvasImageSource = konvaImage?.image();
let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) {
const image = layerState.image;
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
imageSourceNeedsUpdate = true;
} else if (!image) {
imageSourceNeedsUpdate = true;
}
} else if (!canvasImageSource) {
imageSourceNeedsUpdate = true;
}
if (imageSourceNeedsUpdate) {
updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO);
} else if (konvaImage) {
updateIILayerImageAttrs(stage, konvaImage, layerState);
}
};