mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): initial pressure sensitivity implementation
This commit is contained in:
@@ -166,8 +166,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
reducer: rememberedRootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: import.meta.env.MODE === 'development',
|
||||
immutableCheck: import.meta.env.MODE === 'development',
|
||||
serializableCheck: false, // import.meta.env.MODE === 'development',
|
||||
immutableCheck: false, // import.meta.env.MODE === 'development',
|
||||
})
|
||||
.concat(api.middleware)
|
||||
.concat(dynamicMiddlewares)
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -113,6 +115,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'brush_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectBrushLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectBrushLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLine || !this.renderer);
|
||||
@@ -122,6 +133,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectEraserLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'rect') {
|
||||
assert(this.renderer instanceof CanvasObjectRect || !this.renderer);
|
||||
@@ -205,14 +225,18 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
|
||||
if (pushToState) {
|
||||
const entityIdentifier = this.parent.entityIdentifier;
|
||||
if (this.state.type === 'brush_line') {
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
} else if (this.state.type === 'rect') {
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
} else {
|
||||
this.log.warn({ buffer: this.state }, 'Invalid buffer object type');
|
||||
switch (this.state.type) {
|
||||
case 'brush_line':
|
||||
case 'brush_line_with_pressure':
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
break;
|
||||
case 'eraser_line':
|
||||
case 'eraser_line_with_pressure':
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
break;
|
||||
case 'rect':
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -285,6 +287,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'brush_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectBrushLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectBrushLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line') {
|
||||
assert(renderer instanceof CanvasObjectEraserLine || !renderer);
|
||||
@@ -295,6 +307,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectEraserLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectEraserLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'rect') {
|
||||
assert(renderer instanceof CanvasObjectRect || !renderer);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasBrushLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectBrushLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_brush_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasBrushLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasBrushLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'source-over',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasBrushLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating brush line with pressure');
|
||||
const { points, color, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
thinning: 1,
|
||||
}),
|
||||
fill: rgbaColorToString(color),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting brush line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEraserLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectEraserLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_eraser_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasEraserLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasEraserLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
fill: 'red', // Eraser lines use compositing, does not matter what color they have
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'destination-out',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasEraserLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating eraser line with pressure');
|
||||
const { points, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: this.parent instanceof CanvasEntityObjectRenderer,
|
||||
thinning: 1,
|
||||
}),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting eraser line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import type { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import type { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import type { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import type { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import type { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasBrushLineWithPressureState,
|
||||
CanvasEraserLineState,
|
||||
CanvasEraserLineWithPressureState,
|
||||
CanvasImageState,
|
||||
CanvasRectState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -15,9 +19,17 @@ import type {
|
||||
|
||||
export type AnyObjectRenderer =
|
||||
| CanvasObjectBrushLine
|
||||
| CanvasObjectBrushLineWithPressure
|
||||
| CanvasObjectEraserLine
|
||||
| CanvasObjectEraserLineWithPressure
|
||||
| CanvasObjectRect
|
||||
| CanvasObjectImage; /**
|
||||
* Union of all object states.
|
||||
*/
|
||||
export type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
||||
export type AnyObjectState =
|
||||
| CanvasBrushLineState
|
||||
| CanvasBrushLineWithPressureState
|
||||
| CanvasEraserLineState
|
||||
| CanvasEraserLineWithPressureState
|
||||
| CanvasImageState
|
||||
| CanvasRectState;
|
||||
|
||||
@@ -105,6 +105,7 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
visible: !this.manager.tool.$isMouseDown.get(),
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
floorCoord,
|
||||
getIsPrimaryMouseDown,
|
||||
getLastPointOfLastLine,
|
||||
getLastPointOfLastLineWithPressure,
|
||||
getLastPointOfLine,
|
||||
getPrefixedId,
|
||||
getScaledCursorPosition,
|
||||
@@ -302,14 +303,12 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
onStagePointerEnter = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
try {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const tool = this.$tool.get();
|
||||
@@ -327,14 +326,25 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
if (tool === 'brush') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y, e.evt.pressure],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -344,13 +354,23 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
if (selectedEntity.bufferRenderer.state && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
@@ -359,16 +379,13 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
onStagePointerDown = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
try {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
@@ -390,36 +407,57 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
|
||||
if (tool === 'brush') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'brush_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
@@ -428,34 +466,56 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'eraser_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
@@ -478,14 +538,12 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStagePointerUp = (e: KonvaEventObject<PointerEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStagePointerUp = (_: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(false);
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
if (!cursorPos) {
|
||||
@@ -499,7 +557,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const tool = this.$tool.get();
|
||||
|
||||
if (tool === 'brush') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'brush_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'brush_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'brush_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -507,7 +569,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'eraser_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'eraser_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'eraser_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -527,13 +593,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
onStagePointerMove = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = this.$tool.get();
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
@@ -561,7 +625,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'brush' && bufferState.type === 'brush_line') {
|
||||
if (tool === 'brush' && (bufferState.type === 'brush_line' || bufferState.type === 'brush_line_with_pressure')) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
@@ -577,8 +641,16 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'brush_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'eraser' && bufferState.type === 'eraser_line') {
|
||||
} else if (
|
||||
tool === 'eraser' &&
|
||||
(bufferState.type === 'eraser_line' || bufferState.type === 'eraser_line_with_pressure')
|
||||
) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
@@ -594,6 +666,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'eraser_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'rect' && bufferState.type === 'rect') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
@@ -609,25 +686,26 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStagePointerLeave = (e: KonvaEventObject<PointerEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
onStagePointerLeave = (_: PointerEvent) => {
|
||||
try {
|
||||
this.$cursorPos.set(null);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
} finally {
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.$cursorPos.set(null);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import type { StrokeOptions } from 'perfect-freehand';
|
||||
import getStroke from 'perfect-freehand';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
@@ -148,14 +150,32 @@ export const getLastPointOfLine = (points: number[]): Coordinate | null => {
|
||||
if (points.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const x = points[points.length - 2];
|
||||
const y = points[points.length - 1];
|
||||
const x = points.at(-2);
|
||||
const y = points.at(-1);
|
||||
if (x === undefined || y === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the last point of a line as a coordinate.
|
||||
* @param points An array of numbers representing points as [x1, y1, x2, y2, ...]
|
||||
* @returns The last point of the line as a coordinate, or null if the line has less than 1 point
|
||||
*/
|
||||
export const getLastPointOfLineWithPressure = (points: number[]): CoordinateWithPressure | null => {
|
||||
if (points.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const x = points.at(-3);
|
||||
const y = points.at(-2);
|
||||
const pressure = points.at(-1);
|
||||
if (x === undefined || y === undefined || pressure === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y, pressure };
|
||||
};
|
||||
|
||||
export function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
||||
return e.evt.buttons === 1;
|
||||
}
|
||||
@@ -436,7 +456,9 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
*/
|
||||
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
export function getPrefixedId(prefix: CanvasEntityIdentifier['type'] | (string & Record<never, never>)): string {
|
||||
export function getPrefixedId(
|
||||
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>)
|
||||
): string {
|
||||
return `${prefix}:${nanoid()}`;
|
||||
}
|
||||
|
||||
@@ -492,11 +514,32 @@ export const exhaustiveCheck = (value: never): never => {
|
||||
assert(false, `Unhandled value: ${value}`);
|
||||
};
|
||||
|
||||
type CoordinateWithPressure = {
|
||||
x: number;
|
||||
y: number;
|
||||
pressure: number;
|
||||
};
|
||||
export const getLastPointOfLastLineWithPressure = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line_with_pressure' | 'eraser_line_with_pressure'
|
||||
): CoordinateWithPressure | null => {
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastObject.type === type) {
|
||||
return getLastPointOfLineWithPressure(lastObject.points);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getLastPointOfLastLine = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line' | 'eraser_line'
|
||||
): Coordinate | null => {
|
||||
const lastObject = objects[objects.length - 1];
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
@@ -540,3 +583,53 @@ export const getKonvaNodeDebugAttrs = (node: Konva.Node) => {
|
||||
rotation: node.rotation(),
|
||||
};
|
||||
};
|
||||
|
||||
const average = (a: number, b: number) => (a + b) / 2;
|
||||
|
||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||
const len = points.length;
|
||||
|
||||
if (len < 4) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let a = points[0] as number[];
|
||||
let b = points[1] as number[];
|
||||
const c = points[2] as number[];
|
||||
|
||||
let result = `M${a[0]!.toFixed(2)},${a[1]!.toFixed(2)} Q${b[0]!.toFixed(
|
||||
2
|
||||
)},${b[1]!.toFixed(2)} ${average(b[0]!, c[0]!).toFixed(2)},${average(b[1]!, c[1]!).toFixed(2)} T`;
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i]!;
|
||||
b = points[i + 1]!;
|
||||
result += `${average(a[0]!, b[0]!).toFixed(2)},${average(a[1]!, b[1]!).toFixed(2)} `;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += 'Z';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getSVGPathDataFromPoints = (points: number[], options?: StrokeOptions): string => {
|
||||
const chunked: [number, number, number][] = [];
|
||||
for (let i = 0; i < points.length; i += 3) {
|
||||
chunked.push([points[i]!, points[i + 1]!, points[i + 2]!]);
|
||||
}
|
||||
return getSvgPathFromStroke(getStroke(chunked, options));
|
||||
};
|
||||
|
||||
export const getPointerType = (e: KonvaEventObject<PointerEvent>): 'mouse' | 'pen' | 'touch' => {
|
||||
if (e.evt.pointerType === 'mouse') {
|
||||
return 'mouse';
|
||||
}
|
||||
|
||||
if (e.evt.pointerType === 'pen') {
|
||||
return 'pen';
|
||||
}
|
||||
|
||||
return 'touch';
|
||||
};
|
||||
|
||||
@@ -78,6 +78,10 @@ type CanvasSettingsState = {
|
||||
* Whether to show only the selected layer while transforming.
|
||||
*/
|
||||
isolatedTransformingPreview: boolean;
|
||||
/**
|
||||
* Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used.
|
||||
*/
|
||||
pressureSensitivity: boolean;
|
||||
};
|
||||
|
||||
const initialState: CanvasSettingsState = {
|
||||
@@ -98,6 +102,7 @@ const initialState: CanvasSettingsState = {
|
||||
isolatedStagingPreview: true,
|
||||
isolatedFilteringPreview: true,
|
||||
isolatedTransformingPreview: true,
|
||||
pressureSensitivity: true,
|
||||
};
|
||||
|
||||
export const canvasSettingsSlice = createSlice({
|
||||
|
||||
@@ -947,7 +947,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) });
|
||||
entity.objects.push({
|
||||
...brushLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: brushLine.type === 'brush_line' ? simplifyFlatNumbersArray(brushLine.points) : brushLine.points,
|
||||
});
|
||||
},
|
||||
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
||||
const { entityIdentifier, eraserLine } = action.payload;
|
||||
@@ -962,7 +966,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) });
|
||||
entity.objects.push({
|
||||
...eraserLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: eraserLine.type === 'eraser_line' ? simplifyFlatNumbersArray(eraserLine.points) : eraserLine.points,
|
||||
});
|
||||
},
|
||||
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
||||
const { entityIdentifier, rect } = action.payload;
|
||||
|
||||
@@ -58,7 +58,10 @@ const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'colorP
|
||||
export type Tool = z.infer<typeof zTool>;
|
||||
|
||||
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
||||
message: 'Must have an even number of points',
|
||||
message: 'Must have an even number of coordinate components',
|
||||
});
|
||||
const zPointsWithPressure = z.array(z.number()).refine((points) => points.length % 3 === 0, {
|
||||
message: 'Must have a number of components divisible by 3',
|
||||
});
|
||||
|
||||
const zRgbColor = z.object({
|
||||
@@ -110,6 +113,19 @@ const zCanvasBrushLineState = z.object({
|
||||
});
|
||||
export type CanvasBrushLineState = z.infer<typeof zCanvasBrushLineState>;
|
||||
|
||||
const zCanvasBrushLineWithPressureState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('brush_line_with_pressure'),
|
||||
strokeWidth: z.number().min(1),
|
||||
points: zPointsWithPressure,
|
||||
// thinning: z.number().min(0).max(1),
|
||||
// streamline: z.number().min(0).max(1),
|
||||
// smoothing: z.number().min(0).max(1),
|
||||
color: zRgbaColor,
|
||||
clip: zRect.nullable(),
|
||||
});
|
||||
export type CanvasBrushLineWithPressureState = z.infer<typeof zCanvasBrushLineWithPressureState>;
|
||||
|
||||
const zCanvasEraserLineState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('eraser_line'),
|
||||
@@ -119,6 +135,18 @@ const zCanvasEraserLineState = z.object({
|
||||
});
|
||||
export type CanvasEraserLineState = z.infer<typeof zCanvasEraserLineState>;
|
||||
|
||||
const zCanvasEraserLineWithPressureState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('eraser_line_with_pressure'),
|
||||
strokeWidth: z.number().min(1),
|
||||
points: zPointsWithPressure,
|
||||
// thinning: z.number().min(0).max(1),
|
||||
// streamline: z.number().min(0).max(1),
|
||||
// smoothing: z.number().min(0).max(1),
|
||||
clip: zRect.nullable(),
|
||||
});
|
||||
export type CanvasEraserLineWithPressureState = z.infer<typeof zCanvasEraserLineWithPressureState>;
|
||||
|
||||
const zCanvasRectState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('rect'),
|
||||
@@ -139,6 +167,8 @@ const zCanvasObjectState = z.union([
|
||||
zCanvasBrushLineState,
|
||||
zCanvasEraserLineState,
|
||||
zCanvasRectState,
|
||||
zCanvasBrushLineWithPressureState,
|
||||
zCanvasEraserLineWithPressureState,
|
||||
]);
|
||||
export type CanvasObjectState = z.infer<typeof zCanvasObjectState>;
|
||||
|
||||
@@ -359,8 +389,12 @@ export type EntityIdentifierPayload<
|
||||
} & T;
|
||||
|
||||
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
|
||||
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;
|
||||
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>;
|
||||
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{
|
||||
brushLine: CanvasBrushLineState | CanvasBrushLineWithPressureState;
|
||||
}>;
|
||||
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{
|
||||
eraserLine: CanvasEraserLineState | CanvasEraserLineWithPressureState;
|
||||
}>;
|
||||
export type EntityRectAddedPayload = EntityIdentifierPayload<{ rect: CanvasRectState }>;
|
||||
export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||
imageObject: CanvasImageState;
|
||||
|
||||
Reference in New Issue
Block a user