feat(ui): raster layer reset, object group util

This commit is contained in:
psychedelicious 2024-06-06 09:26:29 +10:00
parent 7595d05191
commit 3870ebdf29
7 changed files with 116 additions and 63 deletions

View File

@ -29,6 +29,9 @@ export const LayerMenu = memo(({ layerId }: Props) => {
layerType === 'raster_layer'
);
}, [layerType]);
const shouldShowResetAction = useMemo(() => {
return layerType === 'regional_guidance_layer' || layerType === 'raster_layer';
}, [layerType]);
return (
<Menu>
@ -52,7 +55,7 @@ export const LayerMenu = memo(({ layerId }: Props) => {
<MenuDivider />
</>
)}
{layerType === 'regional_guidance_layer' && (
{shouldShowResetAction && (
<MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>

View File

@ -14,21 +14,27 @@ export const BACKGROUND_RECT_ID = 'background_layer.rect';
export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message';
// Names for Konva layers and objects (comparable to CSS classes)
export const LAYER_BBOX_NAME = 'layer.bbox';
export const COMPOSITING_RECT_NAME = 'compositing-rect';
export const CA_LAYER_NAME = 'control_adapter_layer';
export const CA_LAYER_IMAGE_NAME = 'control_adapter_layer.image';
export const RG_LAYER_NAME = 'regional_guidance_layer';
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer';
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
export const LAYER_BBOX_NAME = 'layer.bbox';
export const COMPOSITING_RECT_NAME = 'compositing-rect';
export const RG_LAYER_NAME = 'regional_guidance_layer';
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
export const RG_LAYER_BRUSH_LINE_NAME = 'regional_guidance_layer.brush_line';
export const RG_LAYER_ERASER_LINE_NAME = 'regional_guidance_layer.eraser_line';
export const RG_LAYER_RECT_SHAPE_NAME = 'regional_guidance_layer.rect_shape';
export const RASTER_LAYER_NAME = 'raster_layer';
export const RASTER_LAYER_LINE_NAME = 'raster_layer.line';
export const RASTER_LAYER_OBJECT_GROUP_NAME = 'raster_layer.object_group';
export const RASTER_LAYER_RECT_NAME = 'raster_layer.rect';
export const RASTER_LAYER_BRUSH_LINE_NAME = 'raster_layer.brush_line';
export const RASTER_LAYER_ERASER_LINE_NAME = 'raster_layer.eraser_line';
export const RASTER_LAYER_RECT_SHAPE_NAME = 'raster_layer.rect_shape';
// Getters for non-singleton layer and object IDs
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;

View File

@ -1,8 +1,9 @@
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { RG_LAYER_LINE_NAME, RG_LAYER_RECT_NAME } from 'features/controlLayers/konva/naming';
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import type { BrushLine, EraserLine, RectShape } from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import Konva from 'konva';
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
@ -13,12 +14,13 @@ import Konva from 'konva';
* 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 createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => {
export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => {
const konvaLine = new Konva.Line({
id: brushLine.id,
key: brushLine.id,
name: RG_LAYER_LINE_NAME,
name,
strokeWidth: brushLine.strokeWidth,
tension: 0,
lineCap: 'round',
@ -36,12 +38,13 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr
* 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 createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => {
export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => {
const konvaLine = new Konva.Line({
id: eraserLine.id,
key: eraserLine.id,
name: RG_LAYER_LINE_NAME,
name,
strokeWidth: eraserLine.strokeWidth,
tension: 0,
lineCap: 'round',
@ -58,13 +61,14 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva
/**
* 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 line to
* @param layerObjectGroup The konva layer's object group to add the rect to
* @param name The konva name for the rect
*/
export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => {
export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group, name: string): Konva.Rect => {
const konvaRect = new Konva.Rect({
id: rectShape.id,
key: rectShape.id,
name: RG_LAYER_RECT_NAME,
name,
x: rectShape.x,
y: rectShape.y,
width: rectShape.width,
@ -75,3 +79,19 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr
layerObjectGroup.add(konvaRect);
return konvaRect;
};
/**
* Creates a konva group for a layer's objects.
* @param konvaLayer The konva layer to add the object group to
* @param name The konva name for the group
* @returns
*/
export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva.Group => {
const konvaObjectGroup = new Konva.Group({
id: getObjectGroupId(konvaLayer.id(), uuidv4()),
name,
listening: false,
});
konvaLayer.add(konvaObjectGroup);
return konvaObjectGroup;
};

View File

@ -1,14 +1,19 @@
import {
getObjectGroupId,
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME,
RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming';
import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects';
import { getScaledFlooredCursorPosition, mapId } from 'features/controlLayers/konva/util';
import {
createBrushLine,
createEraserLine,
createObjectGroup,
createRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { getScaledFlooredCursorPosition, mapId, selectRasterObjects } from 'features/controlLayers/konva/util';
import type { RasterLayer, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
/**
* Logic for creating and rendering raster layers.
@ -59,14 +64,6 @@ const createRasterLayer = (
return pos;
});
// The object group holds all of the layer's objects (e.g. lines and rects)
const konvaObjectGroup = new Konva.Group({
id: getObjectGroupId(layerState.id, uuidv4()),
name: RASTER_LAYER_OBJECT_GROUP_NAME,
listening: false,
});
konvaLayer.add(konvaObjectGroup);
stage.add(konvaLayer);
return konvaLayer;
@ -95,12 +92,15 @@ export const renderRasterLayer = (
y: Math.floor(layerState.y),
});
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`);
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
const konvaObjectGroup =
konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`) ??
createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME);
const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in the redux state
for (const objectNode of konvaObjectGroup.getChildren()) {
// TODO(psyche): `konvaObjectGroup.getChildren()` seems to return a stale array of children, but find is never stale.
// Should report upstream
for (const objectNode of konvaObjectGroup.find(selectRasterObjects)) {
if (!objectIds.includes(objectNode.id())) {
objectNode.destroy();
}
@ -108,20 +108,23 @@ export const renderRasterLayer = (
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
const konvaBrushLine =
stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed.
if (konvaBrushLine.points().length !== obj.points.length) {
konvaBrushLine.points(obj.points);
}
} else if (obj.type === 'eraser_line') {
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
const konvaEraserLine =
stage.findOne<Konva.Line>(`#${obj.id}`) ??
createEraserLine(obj, konvaObjectGroup, RASTER_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed.
if (konvaEraserLine.points().length !== obj.points.length) {
konvaEraserLine.points(obj.points);
}
} else if (obj.type === 'rect_shape') {
if (!stage.findOne<Konva.Rect>(`#${obj.id}`)) {
createRectShape(obj, konvaObjectGroup);
createRectShape(obj, konvaObjectGroup, RASTER_LAYER_RECT_SHAPE_NAME);
}
}
}

View File

@ -1,17 +1,22 @@
import { rgbColorToString } from 'features/canvas/util/colorToString';
import {
COMPOSITING_RECT_NAME,
getObjectGroupId,
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 { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects';
import {
createBrushLine,
createEraserLine,
createObjectGroup,
createRectShape,
} from 'features/controlLayers/konva/renderers/objects';
import { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
/**
* Logic for creating and rendering regional guidance layers.
@ -75,14 +80,6 @@ const createRGLayer = (
return pos;
});
// The object group holds all of the layer's objects (e.g. lines and rects)
const konvaObjectGroup = new Konva.Group({
id: getObjectGroupId(layerState.id, uuidv4()),
name: RG_LAYER_OBJECT_GROUP_NAME,
listening: false,
});
konvaLayer.add(konvaObjectGroup);
stage.add(konvaLayer);
return konvaLayer;
@ -116,8 +113,9 @@ export const renderRGLayer = (
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(layerState.previewColor);
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`);
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
const konvaObjectGroup =
konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ??
createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME);
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
let groupNeedsCache = false;
@ -133,7 +131,8 @@ export const renderRGLayer = (
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
const konvaBrushLine =
stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
@ -147,7 +146,8 @@ export const renderRGLayer = (
groupNeedsCache = true;
}
} else if (obj.type === 'eraser_line') {
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
const konvaEraserLine =
stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup, RG_LAYER_ERASER_LINE_NAME);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
@ -161,7 +161,8 @@ export const renderRGLayer = (
groupNeedsCache = true;
}
} else if (obj.type === 'rect_shape') {
const konvaRectShape = stage.findOne<Konva.Rect>(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup);
const konvaRectShape =
stage.findOne<Konva.Rect>(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup, RG_LAYER_RECT_SHAPE_NAME);
// Only update the color if it has changed.
if (konvaRectShape.fill() !== rgbColor) {

View File

@ -1,10 +1,14 @@
import {
CA_LAYER_NAME,
INITIAL_IMAGE_LAYER_NAME,
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_NAME,
RG_LAYER_LINE_NAME,
RASTER_LAYER_RECT_SHAPE_NAME,
RG_LAYER_BRUSH_LINE_NAME,
RG_LAYER_ERASER_LINE_NAME,
RG_LAYER_NAME,
RG_LAYER_RECT_NAME,
RG_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
@ -89,17 +93,27 @@ export const mapId = (object: { id: string }): string => object.id;
* Konva selection callback to select all renderable layers. This includes RG, CA II and Raster layers.
* This can be provided to the `find` or `findOne` konva node methods.
*/
export const selectRenderableLayers = (n: Konva.Node): boolean =>
n.name() === RG_LAYER_NAME ||
n.name() === CA_LAYER_NAME ||
n.name() === INITIAL_IMAGE_LAYER_NAME ||
n.name() === RASTER_LAYER_NAME;
export const selectRenderableLayers = (node: Konva.Node): boolean =>
node.name() === RG_LAYER_NAME ||
node.name() === CA_LAYER_NAME ||
node.name() === INITIAL_IMAGE_LAYER_NAME ||
node.name() === RASTER_LAYER_NAME;
/**
* Konva selection callback to select RG mask objects. This includes lines and rects.
* This can be provided to the `find` or `findOne` konva node methods.
*/
export const selectVectorMaskObjects = (node: Konva.Node): boolean => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
};
export const selectVectorMaskObjects = (node: Konva.Node): boolean =>
node.name() === RG_LAYER_BRUSH_LINE_NAME ||
node.name() === RG_LAYER_ERASER_LINE_NAME ||
node.name() === RG_LAYER_RECT_SHAPE_NAME;
/**
* Konva selection callback to select raster layer objects. This includes lines and rects.
* This can be provided to the `find` or `findOne` konva node methods.
*/
export const selectRasterObjects = (node: Konva.Node): boolean =>
node.name() === RASTER_LAYER_BRUSH_LINE_NAME ||
node.name() === RASTER_LAYER_ERASER_LINE_NAME ||
node.name() === RASTER_LAYER_RECT_SHAPE_NAME;
//#endregion

View File

@ -177,6 +177,12 @@ export const controlLayersSlice = createSlice({
layer.bboxNeedsUpdate = false;
layer.uploadedMaskImage = null;
}
if (isRasterLayer(layer)) {
layer.isEnabled = true;
layer.objects = [];
layer.bbox = null;
layer.bboxNeedsUpdate = false;
}
},
layerDeleted: (state, action: PayloadAction<string>) => {
state.layers = state.layers.filter((l) => l.id !== action.payload);