From fc5467150e24e051597de617bf2bd2c2593c1aff Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:10:26 +1000 Subject: [PATCH] feat(ui): clip lines to bbox --- .../components/StageComponent.tsx | 3 +- .../features/controlLayers/konva/events.ts | 42 +++++++++++++++++++ .../controlLayers/konva/renderers/objects.ts | 21 +++++++--- .../konva/renderers/previewLayer.ts | 2 +- .../konva/renderers/rasterLayer.ts | 10 ++--- .../controlLayers/konva/renderers/rgLayer.ts | 8 ++-- .../controlLayers/store/canvasV2Slice.ts | 4 +- .../controlLayers/store/layersReducers.ts | 6 ++- .../controlLayers/store/regionsReducers.ts | 6 ++- .../src/features/controlLayers/store/types.ts | 20 +++++---- 10 files changed, 92 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 8b56b9598f..6d7ce825d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -116,7 +116,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { $toolState.set(tool); $selectedEntity.set(selectedEntity); - $bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); + $bbox.set(bbox); $currentFill.set(currentFill); $document.set(document); }, [selectedEntity, tool, bbox, currentFill, document]); @@ -255,6 +255,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, getDocument: $document.get, + getBbox: $bbox.get, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 548baa5c8f..cb1f83a334 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -47,6 +47,7 @@ type Arg = { getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; getDocument: () => CanvasV2State['document']; + getBbox: () => CanvasV2State['bbox']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; @@ -147,6 +148,7 @@ export const setStageEventHandlers = ({ getSelectedEntity, getSpaceKey, getDocument, + getBbox, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, @@ -190,6 +192,7 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(pos); if (toolState.selected === 'brush') { + const bbox = getBbox(); if (e.evt.shiftKey) { const lastAddedPoint = getLastAddedPoint(); // Create a straight line if holding shift @@ -205,6 +208,12 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -221,6 +230,12 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -229,6 +244,7 @@ export const setStageEventHandlers = ({ } if (toolState.selected === 'eraser') { + const bbox = getBbox(); if (e.evt.shiftKey) { // Create a straight line if holding shift const lastAddedPoint = getLastAddedPoint(); @@ -243,6 +259,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -258,6 +280,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -348,6 +376,7 @@ export const setStageEventHandlers = ({ // Continue the last line maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { + const bbox = getBbox(); // Start a new line onBrushLineAdded( { @@ -360,6 +389,12 @@ export const setStageEventHandlers = ({ ], width: toolState.brush.width, color: getCurrentFill(), + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -373,6 +408,7 @@ export const setStageEventHandlers = ({ // Continue the last line maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { + const bbox = getBbox(); // Start a new line onEraserLineAdded( { @@ -384,6 +420,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index dd4ba645e2..ce626e9952 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -23,10 +23,19 @@ import { v4 as uuidv4 } from 'uuid'; * @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, name: string): Konva.Line => { - const konvaLine = new Konva.Line({ +export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { + let konvaLineGroup = layerObjectGroup.findOne(`#${brushLine.id}_group`); + let konvaLine = konvaLineGroup?.findOne(`#${brushLine.id}`); + if (konvaLine) { + return konvaLine; + } + + konvaLineGroup = new Konva.Group({ + id: `${brushLine.id}_group`, + // clip: brushLine.clip, + }); + konvaLine = new Konva.Line({ id: brushLine.id, - key: brushLine.id, name, strokeWidth: brushLine.strokeWidth, tension: 0, @@ -37,7 +46,8 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr listening: false, stroke: rgbaColorToString(brushLine.color), }); - layerObjectGroup.add(konvaLine); + konvaLineGroup.add(konvaLine); + layerObjectGroup.add(konvaLineGroup); return konvaLine; }; @@ -47,7 +57,7 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr * @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, name: string): Konva.Line => { +export const getEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { const konvaLine = new Konva.Line({ id: eraserLine.id, key: eraserLine.id, @@ -60,6 +70,7 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + clip: eraserLine.clip, }); layerObjectGroup.add(konvaLine); return konvaLine; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index a3b8f7a893..7416c3db20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -253,7 +253,7 @@ export const renderBboxPreview = ( stage: Konva.Stage, bbox: IRect, tool: Tool, - getBbox: () => IRect, + getBbox: () => CanvasV2State['bbox'], onBboxTransformed: (bbox: IRect) => void, getShiftKey: () => boolean, getCtrlKey: () => boolean, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts index 772475b953..4f5ebfc720 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -7,11 +7,11 @@ import { RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { - createBrushLine, - createEraserLine, createImageObjectGroup, createObjectGroup, createRectShape, + getBrushLine, + getEraserLine, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; @@ -92,9 +92,7 @@ export const renderRasterLayer = async ( for (const obj of layerState.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = - konvaObjectGroup.findOne(`#${obj.id}`) ?? - createBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME); + const konvaBrushLine = getBrushLine(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); @@ -102,7 +100,7 @@ export const renderRasterLayer = async ( } else if (obj.type === 'eraser_line') { const konvaEraserLine = konvaObjectGroup.findOne(`#${obj.id}`) ?? - createEraserLine(obj, konvaObjectGroup, RASTER_LAYER_ERASER_LINE_NAME); + getEraserLine(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); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts index 11a096487e..916c6c9c47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -12,10 +12,10 @@ import { import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createBboxRect, - createBrushLine, - createEraserLine, createObjectGroup, createRectShape, + getBrushLine, + getEraserLine, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types'; @@ -117,7 +117,7 @@ export const renderRGLayer = ( for (const obj of rg.objects) { if (obj.type === 'brush_line') { const konvaBrushLine = - stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); + stage.findOne(`#${obj.id}`) ?? getBrushLine(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. @@ -132,7 +132,7 @@ export const renderRGLayer = ( } } else if (obj.type === 'eraser_line') { const konvaEraserLine = - stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup, RG_LAYER_ERASER_LINE_NAME); + stage.findOne(`#${obj.id}`) ?? getEraserLine(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. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c6f6a2bd7a..652366ca1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,7 +15,7 @@ import { settingsReducers } from 'features/controlLayers/store/settingsReducers' import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { IRect, Vector2d } from 'konva/lib/types'; +import type { Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; @@ -327,7 +327,7 @@ export const $stageAttrs = atom({ export const $toolState = atom(deepClone(initialState.tool)); export const $currentFill = atom(DEFAULT_RGBA_COLOR); export const $selectedEntity = atom(null); -export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); +export const $bbox = atom(deepClone(initialState.bbox)); export const $document = atom(deepClone(initialState.document)); export const canvasV2PersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 9ba7df2589..2fef5abc4a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -136,7 +136,7 @@ export const layersReducers = { }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; + const { id, points, lineId, color, width, clip } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; @@ -148,6 +148,7 @@ export const layersReducers = { points, strokeWidth: width, color, + clip, }); layer.bboxNeedsUpdate = true; }, @@ -157,7 +158,7 @@ export const layersReducers = { }, layerEraserLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; + const { id, points, lineId, width, clip } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; @@ -168,6 +169,7 @@ export const layersReducers = { type: 'eraser_line', points, strokeWidth: width, + clip, }); layer.bboxNeedsUpdate = true; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index af586c4d83..f2009ab15c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -304,7 +304,7 @@ export const regionsReducers = { }, rgBrushLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; + const { id, points, lineId, color, width, clip } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -315,6 +315,7 @@ export const regionsReducers = { points, strokeWidth: width, color, + clip, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; @@ -325,7 +326,7 @@ export const regionsReducers = { }, rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; + const { id, points, lineId, width, clip } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -335,6 +336,7 @@ export const regionsReducers = { type: 'eraser_line', points, strokeWidth: width, + clip, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f4c594c3bb..6841e23d83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -498,12 +498,21 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); +const zRect = z.object({ + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +export type Rect = z.infer; + const zBrushLine = z.object({ id: zId, type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, color: zRgbaColor, + clip: zRect.nullable(), }); export type BrushLine = z.infer; @@ -512,6 +521,7 @@ const zEraserline = z.object({ type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, + clip: zRect.nullable(), }); export type EraserLine = z.infer; @@ -566,13 +576,6 @@ const zLayerObject = z.discriminatedUnion('type', [ ]); export type LayerObject = z.infer; -const zRect = z.object({ - x: z.number(), - y: z.number(), - width: z.number().min(1), - height: z.number().min(1), -}); - export const zLayerEntity = z.object({ id: zId, type: z.literal('layer'), @@ -614,12 +617,14 @@ const zMaskObject = z ...rest, type: 'brush_line', color: { r: 255, g: 255, b: 255, a: 1 }, + clip: null, }; return asBrushline; } else if (tool === 'eraser') { const asEraserLine: EraserLine = { ...rest, type: 'eraser_line', + clip: null, }; return asEraserLine; } @@ -881,6 +886,7 @@ export type EraserLineAddedArg = { id: string; points: [number, number, number, number]; width: number; + clip: Rect | null; }; export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type PointAddedToLineArg = { id: string; point: [number, number] };