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

This commit is contained in:
psychedelicious 2024-06-06 09:26:29 +10:00
parent cd4f63f2fd
commit 1bce156de1
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 === 'raster_layer'
); );
}, [layerType]); }, [layerType]);
const shouldShowResetAction = useMemo(() => {
return layerType === 'regional_guidance_layer' || layerType === 'raster_layer';
}, [layerType]);
return ( return (
<Menu> <Menu>
@ -52,7 +55,7 @@ export const LayerMenu = memo(({ layerId }: Props) => {
<MenuDivider /> <MenuDivider />
</> </>
)} )}
{layerType === 'regional_guidance_layer' && ( {shouldShowResetAction && (
<MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}> <MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')} {t('accessibility.reset')}
</MenuItem> </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'; export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message';
// Names for Konva layers and objects (comparable to CSS classes) // 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_NAME = 'control_adapter_layer';
export const CA_LAYER_IMAGE_NAME = 'control_adapter_layer.image'; 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_ID = 'singleton_initial_image_layer';
export const INITIAL_IMAGE_LAYER_NAME = '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 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_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_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 // Getters for non-singleton layer and object IDs
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;

View File

@ -1,8 +1,9 @@
import { rgbaColorToString } from 'features/canvas/util/colorToString'; 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 type { BrushLine, EraserLine, 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 Konva from 'konva'; 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 * 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. * Creates a konva line for a brush line.
* @param brushLine The brush line state * @param brushLine The brush line 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 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({ const konvaLine = new Konva.Line({
id: brushLine.id, id: brushLine.id,
key: brushLine.id, key: brushLine.id,
name: RG_LAYER_LINE_NAME, name,
strokeWidth: brushLine.strokeWidth, strokeWidth: brushLine.strokeWidth,
tension: 0, tension: 0,
lineCap: 'round', lineCap: 'round',
@ -36,12 +38,13 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr
* Creates a konva line for a eraser line. * Creates a konva line for a eraser line.
* @param eraserLine The eraser line state * @param eraserLine The eraser line 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 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({ const konvaLine = new Konva.Line({
id: eraserLine.id, id: eraserLine.id,
key: eraserLine.id, key: eraserLine.id,
name: RG_LAYER_LINE_NAME, name,
strokeWidth: eraserLine.strokeWidth, strokeWidth: eraserLine.strokeWidth,
tension: 0, tension: 0,
lineCap: 'round', lineCap: 'round',
@ -58,13 +61,14 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva
/** /**
* Creates a konva rect for a rect shape. * Creates a konva rect for a rect shape.
* @param rectShape The rect shape state * @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({ const konvaRect = new Konva.Rect({
id: rectShape.id, id: rectShape.id,
key: rectShape.id, key: rectShape.id,
name: RG_LAYER_RECT_NAME, name,
x: rectShape.x, x: rectShape.x,
y: rectShape.y, y: rectShape.y,
width: rectShape.width, width: rectShape.width,
@ -75,3 +79,19 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr
layerObjectGroup.add(konvaRect); layerObjectGroup.add(konvaRect);
return 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 { import {
getObjectGroupId, RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_NAME, RASTER_LAYER_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME, RASTER_LAYER_OBJECT_GROUP_NAME,
RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { createBrushLine, createEraserLine, createRectShape } from 'features/controlLayers/konva/renderers/objects'; import {
import { getScaledFlooredCursorPosition, mapId } from 'features/controlLayers/konva/util'; 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 type { RasterLayer, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
/** /**
* Logic for creating and rendering raster layers. * Logic for creating and rendering raster layers.
@ -59,14 +64,6 @@ const createRasterLayer = (
return pos; 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); stage.add(konvaLayer);
return konvaLayer; return konvaLayer;
@ -95,12 +92,15 @@ export const renderRasterLayer = (
y: Math.floor(layerState.y), y: Math.floor(layerState.y),
}); });
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`); const konvaObjectGroup =
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`); konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`) ??
createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME);
const objectIds = layerState.objects.map(mapId); const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in the redux state // 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())) { if (!objectIds.includes(objectNode.id())) {
objectNode.destroy(); objectNode.destroy();
} }
@ -108,20 +108,23 @@ export const renderRasterLayer = (
for (const obj of layerState.objects) { for (const obj of layerState.objects) {
if (obj.type === 'brush_line') { 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. // Only update the points if they have changed.
if (konvaBrushLine.points().length !== obj.points.length) { if (konvaBrushLine.points().length !== obj.points.length) {
konvaBrushLine.points(obj.points); konvaBrushLine.points(obj.points);
} }
} else if (obj.type === 'eraser_line') { } 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. // Only update the points if they have changed.
if (konvaEraserLine.points().length !== obj.points.length) { if (konvaEraserLine.points().length !== obj.points.length) {
konvaEraserLine.points(obj.points); konvaEraserLine.points(obj.points);
} }
} else if (obj.type === 'rect_shape') { } else if (obj.type === 'rect_shape') {
if (!stage.findOne<Konva.Rect>(`#${obj.id}`)) { 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 { rgbColorToString } from 'features/canvas/util/colorToString';
import { import {
COMPOSITING_RECT_NAME, COMPOSITING_RECT_NAME,
getObjectGroupId, RG_LAYER_BRUSH_LINE_NAME,
RG_LAYER_ERASER_LINE_NAME,
RG_LAYER_NAME, RG_LAYER_NAME,
RG_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME,
RG_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; 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 { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
/** /**
* Logic for creating and rendering regional guidance layers. * Logic for creating and rendering regional guidance layers.
@ -75,14 +80,6 @@ const createRGLayer = (
return pos; 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); stage.add(konvaLayer);
return 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. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(layerState.previewColor); const rgbColor = rgbColorToString(layerState.previewColor);
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`); const konvaObjectGroup =
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`); 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. // 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;
@ -133,7 +131,8 @@ export const renderRGLayer = (
for (const obj of layerState.objects) { for (const obj of layerState.objects) {
if (obj.type === 'brush_line') { 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 // 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. // array, so checking the length is sufficient to determine if we need to re-cache.
@ -147,7 +146,8 @@ export const renderRGLayer = (
groupNeedsCache = true; groupNeedsCache = true;
} }
} else if (obj.type === 'eraser_line') { } 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 // 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. // array, so checking the length is sufficient to determine if we need to re-cache.
@ -161,7 +161,8 @@ export const renderRGLayer = (
groupNeedsCache = true; groupNeedsCache = true;
} }
} else if (obj.type === 'rect_shape') { } 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. // Only update the color if it has changed.
if (konvaRectShape.fill() !== rgbColor) { if (konvaRectShape.fill() !== rgbColor) {

View File

@ -1,10 +1,14 @@
import { import {
CA_LAYER_NAME, CA_LAYER_NAME,
INITIAL_IMAGE_LAYER_NAME, INITIAL_IMAGE_LAYER_NAME,
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_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_NAME,
RG_LAYER_RECT_NAME, RG_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import type Konva from 'konva'; import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; 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. * 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. * This can be provided to the `find` or `findOne` konva node methods.
*/ */
export const selectRenderableLayers = (n: Konva.Node): boolean => export const selectRenderableLayers = (node: Konva.Node): boolean =>
n.name() === RG_LAYER_NAME || node.name() === RG_LAYER_NAME ||
n.name() === CA_LAYER_NAME || node.name() === CA_LAYER_NAME ||
n.name() === INITIAL_IMAGE_LAYER_NAME || node.name() === INITIAL_IMAGE_LAYER_NAME ||
n.name() === RASTER_LAYER_NAME; node.name() === RASTER_LAYER_NAME;
/** /**
* Konva selection callback to select RG mask objects. This includes lines and rects. * 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. * This can be provided to the `find` or `findOne` konva node methods.
*/ */
export const selectVectorMaskObjects = (node: Konva.Node): boolean => { export const selectVectorMaskObjects = (node: Konva.Node): boolean =>
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; 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 //#endregion

View File

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