feat(ui): clip lines to bbox

This commit is contained in:
psychedelicious 2024-06-17 21:10:26 +10:00
parent 4dcab357a0
commit fc5467150e
10 changed files with 92 additions and 30 deletions

View File

@ -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,

View File

@ -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
);

View File

@ -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<Konva.Group>(`#${brushLine.id}_group`);
let konvaLine = konvaLineGroup?.findOne<Konva.Line>(`#${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;

View File

@ -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,

View File

@ -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<Konva.Line>(`#${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<Konva.Line>(`#${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);

View File

@ -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<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME);
stage.findOne<Konva.Line>(`#${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<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup, RG_LAYER_ERASER_LINE_NAME);
stage.findOne<Konva.Line>(`#${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.

View File

@ -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<StageAttrs>({
export const $toolState = atom<CanvasV2State['tool']>(deepClone(initialState.tool));
export const $currentFill = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $selectedEntity = atom<CanvasEntity | null>(null);
export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
export const $bbox = atom<CanvasV2State['bbox']>(deepClone(initialState.bbox));
export const $document = atom<CanvasV2State['document']>(deepClone(initialState.document));
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {

View File

@ -136,7 +136,7 @@ export const layersReducers = {
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
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<EraserLineAddedArg & { lineId: string }>) => {
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;
},

View File

@ -304,7 +304,7 @@ export const regionsReducers = {
},
rgBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
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<EraserLineAddedArg & { lineId: string }>) => {
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;

View File

@ -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<typeof zRect>;
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<typeof zBrushLine>;
@ -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<typeof zEraserline>;
@ -566,13 +576,6 @@ const zLayerObject = z.discriminatedUnion('type', [
]);
export type LayerObject = z.infer<typeof zLayerObject>;
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] };