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(() => { useLayoutEffect(() => {
$toolState.set(tool); $toolState.set(tool);
$selectedEntity.set(selectedEntity); $selectedEntity.set(selectedEntity);
$bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); $bbox.set(bbox);
$currentFill.set(currentFill); $currentFill.set(currentFill);
$document.set(document); $document.set(document);
}, [selectedEntity, tool, bbox, currentFill, document]); }, [selectedEntity, tool, bbox, currentFill, document]);
@ -255,6 +255,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
getSpaceKey: $spaceKey.get, getSpaceKey: $spaceKey.get,
setStageAttrs: $stageAttrs.set, setStageAttrs: $stageAttrs.set,
getDocument: $document.get, getDocument: $document.get,
getBbox: $bbox.get,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,

View File

@ -47,6 +47,7 @@ type Arg = {
getSelectedEntity: () => CanvasEntity | null; getSelectedEntity: () => CanvasEntity | null;
getSpaceKey: () => boolean; getSpaceKey: () => boolean;
getDocument: () => CanvasV2State['document']; getDocument: () => CanvasV2State['document'];
getBbox: () => CanvasV2State['bbox'];
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
@ -147,6 +148,7 @@ export const setStageEventHandlers = ({
getSelectedEntity, getSelectedEntity,
getSpaceKey, getSpaceKey,
getDocument, getDocument,
getBbox,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
@ -190,6 +192,7 @@ export const setStageEventHandlers = ({
setLastMouseDownPos(pos); setLastMouseDownPos(pos);
if (toolState.selected === 'brush') { if (toolState.selected === 'brush') {
const bbox = getBbox();
if (e.evt.shiftKey) { if (e.evt.shiftKey) {
const lastAddedPoint = getLastAddedPoint(); const lastAddedPoint = getLastAddedPoint();
// Create a straight line if holding shift // Create a straight line if holding shift
@ -205,6 +208,12 @@ export const setStageEventHandlers = ({
], ],
color: getCurrentFill(), color: getCurrentFill(),
width: toolState.brush.width, width: toolState.brush.width,
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type selectedEntity.type
); );
@ -221,6 +230,12 @@ export const setStageEventHandlers = ({
], ],
color: getCurrentFill(), color: getCurrentFill(),
width: toolState.brush.width, width: toolState.brush.width,
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type selectedEntity.type
); );
@ -229,6 +244,7 @@ export const setStageEventHandlers = ({
} }
if (toolState.selected === 'eraser') { if (toolState.selected === 'eraser') {
const bbox = getBbox();
if (e.evt.shiftKey) { if (e.evt.shiftKey) {
// Create a straight line if holding shift // Create a straight line if holding shift
const lastAddedPoint = getLastAddedPoint(); const lastAddedPoint = getLastAddedPoint();
@ -243,6 +259,12 @@ export const setStageEventHandlers = ({
pos.y - selectedEntity.y, pos.y - selectedEntity.y,
], ],
width: toolState.eraser.width, width: toolState.eraser.width,
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type selectedEntity.type
); );
@ -258,6 +280,12 @@ export const setStageEventHandlers = ({
pos.y - selectedEntity.y, pos.y - selectedEntity.y,
], ],
width: toolState.eraser.width, width: toolState.eraser.width,
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type selectedEntity.type
); );
@ -348,6 +376,7 @@ export const setStageEventHandlers = ({
// Continue the last line // Continue the last line
maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
} else { } else {
const bbox = getBbox();
// Start a new line // Start a new line
onBrushLineAdded( onBrushLineAdded(
{ {
@ -360,6 +389,12 @@ export const setStageEventHandlers = ({
], ],
width: toolState.brush.width, width: toolState.brush.width,
color: getCurrentFill(), color: getCurrentFill(),
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type selectedEntity.type
); );
@ -373,6 +408,7 @@ export const setStageEventHandlers = ({
// Continue the last line // Continue the last line
maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
} else { } else {
const bbox = getBbox();
// Start a new line // Start a new line
onEraserLineAdded( onEraserLineAdded(
{ {
@ -384,6 +420,12 @@ export const setStageEventHandlers = ({
pos.y - selectedEntity.y, pos.y - selectedEntity.y,
], ],
width: toolState.eraser.width, width: toolState.eraser.width,
clip: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
},
}, },
selectedEntity.type 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 layerObjectGroup The konva layer's object group to add the line to
* @param name The konva name for the line * @param name The konva name for the line
*/ */
export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => {
const konvaLine = new 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, id: brushLine.id,
key: brushLine.id,
name, name,
strokeWidth: brushLine.strokeWidth, strokeWidth: brushLine.strokeWidth,
tension: 0, tension: 0,
@ -37,7 +46,8 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr
listening: false, listening: false,
stroke: rgbaColorToString(brushLine.color), stroke: rgbaColorToString(brushLine.color),
}); });
layerObjectGroup.add(konvaLine); konvaLineGroup.add(konvaLine);
layerObjectGroup.add(konvaLineGroup);
return konvaLine; 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 layerObjectGroup The konva layer's object group to add the line to
* @param name The konva name for the line * @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({ const konvaLine = new Konva.Line({
id: eraserLine.id, id: eraserLine.id,
key: eraserLine.id, key: eraserLine.id,
@ -60,6 +70,7 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva
globalCompositeOperation: 'destination-out', globalCompositeOperation: 'destination-out',
listening: false, listening: false,
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
clip: eraserLine.clip,
}); });
layerObjectGroup.add(konvaLine); layerObjectGroup.add(konvaLine);
return konvaLine; return konvaLine;

View File

@ -253,7 +253,7 @@ export const renderBboxPreview = (
stage: Konva.Stage, stage: Konva.Stage,
bbox: IRect, bbox: IRect,
tool: Tool, tool: Tool,
getBbox: () => IRect, getBbox: () => CanvasV2State['bbox'],
onBboxTransformed: (bbox: IRect) => void, onBboxTransformed: (bbox: IRect) => void,
getShiftKey: () => boolean, getShiftKey: () => boolean,
getCtrlKey: () => boolean, getCtrlKey: () => boolean,

View File

@ -7,11 +7,11 @@ import {
RASTER_LAYER_RECT_SHAPE_NAME, RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { import {
createBrushLine,
createEraserLine,
createImageObjectGroup, createImageObjectGroup,
createObjectGroup, createObjectGroup,
createRectShape, createRectShape,
getBrushLine,
getEraserLine,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util';
import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; 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) { for (const obj of layerState.objects) {
if (obj.type === 'brush_line') { if (obj.type === 'brush_line') {
const konvaBrushLine = const konvaBrushLine = getBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME);
konvaObjectGroup.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);
@ -102,7 +100,7 @@ export const renderRasterLayer = async (
} else if (obj.type === 'eraser_line') { } else if (obj.type === 'eraser_line') {
const konvaEraserLine = const konvaEraserLine =
konvaObjectGroup.findOne<Konva.Line>(`#${obj.id}`) ?? 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. // 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);

View File

@ -12,10 +12,10 @@ import {
import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox';
import { import {
createBboxRect, createBboxRect,
createBrushLine,
createEraserLine,
createObjectGroup, createObjectGroup,
createRectShape, createRectShape,
getBrushLine,
getEraserLine,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types';
@ -117,7 +117,7 @@ export const renderRGLayer = (
for (const obj of rg.objects) { for (const obj of rg.objects) {
if (obj.type === 'brush_line') { if (obj.type === 'brush_line') {
const konvaBrushLine = 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 // 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.
@ -132,7 +132,7 @@ export const renderRGLayer = (
} }
} else if (obj.type === 'eraser_line') { } else if (obj.type === 'eraser_line') {
const konvaEraserLine = 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 // 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.

View File

@ -15,7 +15,7 @@ import { settingsReducers } from 'features/controlLayers/store/settingsReducers'
import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; 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 { atom } from 'nanostores';
import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; 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 $toolState = atom<CanvasV2State['tool']>(deepClone(initialState.tool));
export const $currentFill = atom<RgbaColor>(DEFAULT_RGBA_COLOR); export const $currentFill = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $selectedEntity = atom<CanvasEntity | null>(null); 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 $document = atom<CanvasV2State['document']>(deepClone(initialState.document));
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = { export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {

View File

@ -136,7 +136,7 @@ export const layersReducers = {
}, },
layerBrushLineAdded: { layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => { 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); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
return; return;
@ -148,6 +148,7 @@ export const layersReducers = {
points, points,
strokeWidth: width, strokeWidth: width,
color, color,
clip,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
@ -157,7 +158,7 @@ export const layersReducers = {
}, },
layerEraserLineAdded: { layerEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => { 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); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
return; return;
@ -168,6 +169,7 @@ export const layersReducers = {
type: 'eraser_line', type: 'eraser_line',
points, points,
strokeWidth: width, strokeWidth: width,
clip,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },

View File

@ -304,7 +304,7 @@ export const regionsReducers = {
}, },
rgBrushLineAdded: { rgBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => { 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); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
@ -315,6 +315,7 @@ export const regionsReducers = {
points, points,
strokeWidth: width, strokeWidth: width,
color, color,
clip,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;
@ -325,7 +326,7 @@ export const regionsReducers = {
}, },
rgEraserLineAdded: { rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => { 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); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
@ -335,6 +336,7 @@ export const regionsReducers = {
type: 'eraser_line', type: 'eraser_line',
points, points,
strokeWidth: width, strokeWidth: width,
clip,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; 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 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({ const zBrushLine = z.object({
id: zId, id: zId,
type: z.literal('brush_line'), type: z.literal('brush_line'),
strokeWidth: z.number().min(1), strokeWidth: z.number().min(1),
points: zPoints, points: zPoints,
color: zRgbaColor, color: zRgbaColor,
clip: zRect.nullable(),
}); });
export type BrushLine = z.infer<typeof zBrushLine>; export type BrushLine = z.infer<typeof zBrushLine>;
@ -512,6 +521,7 @@ const zEraserline = z.object({
type: z.literal('eraser_line'), type: z.literal('eraser_line'),
strokeWidth: z.number().min(1), strokeWidth: z.number().min(1),
points: zPoints, points: zPoints,
clip: zRect.nullable(),
}); });
export type EraserLine = z.infer<typeof zEraserline>; export type EraserLine = z.infer<typeof zEraserline>;
@ -566,13 +576,6 @@ const zLayerObject = z.discriminatedUnion('type', [
]); ]);
export type LayerObject = z.infer<typeof zLayerObject>; 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({ export const zLayerEntity = z.object({
id: zId, id: zId,
type: z.literal('layer'), type: z.literal('layer'),
@ -614,12 +617,14 @@ const zMaskObject = z
...rest, ...rest,
type: 'brush_line', type: 'brush_line',
color: { r: 255, g: 255, b: 255, a: 1 }, color: { r: 255, g: 255, b: 255, a: 1 },
clip: null,
}; };
return asBrushline; return asBrushline;
} else if (tool === 'eraser') { } else if (tool === 'eraser') {
const asEraserLine: EraserLine = { const asEraserLine: EraserLine = {
...rest, ...rest,
type: 'eraser_line', type: 'eraser_line',
clip: null,
}; };
return asEraserLine; return asEraserLine;
} }
@ -881,6 +886,7 @@ export type EraserLineAddedArg = {
id: string; id: string;
points: [number, number, number, number]; points: [number, number, number, number];
width: number; width: number;
clip: Rect | null;
}; };
export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor };
export type PointAddedToLineArg = { id: string; point: [number, number] }; export type PointAddedToLineArg = { id: string; point: [number, number] };