mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): split out tool state from canvas rendering state
This commit is contained in:
parent
9c3da8de8e
commit
3ce8294379
@ -8,6 +8,7 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
|
||||
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice';
|
||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
@ -59,6 +60,7 @@ const allReducers = {
|
||||
[upscaleSlice.name]: upscaleSlice.reducer,
|
||||
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||
[paramsSlice.name]: paramsSlice.reducer,
|
||||
[toolSlice.name]: toolSlice.reducer,
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers(allReducers);
|
||||
@ -101,6 +103,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
||||
[upscalePersistConfig.name]: upscalePersistConfig,
|
||||
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
||||
[paramsPersistConfig.name]: paramsPersistConfig,
|
||||
[toolPersistConfig.name]: toolPersistConfig,
|
||||
};
|
||||
|
||||
const unserialize: UnserializeFunction = (data, key) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { invertScrollChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
||||
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
|
||||
const invertScroll = useAppSelector((s) => s.tool.invertScroll);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
|
||||
[dispatch]
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { brushWidthChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`;
|
||||
export const ToolBrushWidth = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const width = useAppSelector((s) => s.canvasV2.tool.brush.width);
|
||||
const width = useAppSelector((s) => s.tool.brush.width);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(brushWidthChanged(Math.round(v)));
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`;
|
||||
export const ToolEraserWidth = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const width = useAppSelector((s) => s.canvasV2.tool.eraser.width);
|
||||
const width = useAppSelector((s) => s.tool.eraser.width);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(eraserWidthChanged(Math.round(v)));
|
||||
|
@ -2,14 +2,14 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { fillChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { fillChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ToolFillColorPicker = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const fill = useAppSelector((s) => s.canvasV2.tool.fill);
|
||||
const fill = useAppSelector((s) => s.tool.fill);
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(color: RgbaColor) => {
|
||||
|
@ -11,7 +11,6 @@ import type {
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEraserLineState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@ -83,11 +82,7 @@ export class CanvasLayerAdapter extends CanvasModuleBase {
|
||||
this.konva.layer.destroy();
|
||||
};
|
||||
|
||||
update = async (arg?: {
|
||||
state: CanvasLayerAdapter['state'];
|
||||
toolState: CanvasV2State['tool'];
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
update = async (arg?: { state: CanvasLayerAdapter['state'] }) => {
|
||||
const state = get(arg, 'state', this.state);
|
||||
|
||||
if (!this.isFirstRender && state === this.state) {
|
||||
|
@ -112,7 +112,7 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
// These atoms require the canvas manager to be set up before we can provide their initial values
|
||||
this.stateApi.$transformingEntity.set(null);
|
||||
this.stateApi.$toolState.set(this.stateApi.getToolState());
|
||||
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier);
|
||||
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier);
|
||||
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
|
||||
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
|
||||
|
||||
|
@ -11,7 +11,6 @@ import type {
|
||||
CanvasEraserLineState,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@ -83,11 +82,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase {
|
||||
this.konva.layer.destroy();
|
||||
};
|
||||
|
||||
update = async (arg?: {
|
||||
state: CanvasMaskAdapter['state'];
|
||||
toolState: CanvasV2State['tool'];
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
update = async (arg?: { state: CanvasMaskAdapter['state'] }) => {
|
||||
const state = get(arg, 'state', this.state);
|
||||
|
||||
if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) {
|
||||
|
@ -28,7 +28,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
render = async () => {
|
||||
const state = this.manager.stateApi.getState();
|
||||
const state = this.manager.stateApi.getCanvasState();
|
||||
|
||||
if (!this.state) {
|
||||
this.log.trace('First render');
|
||||
@ -51,7 +51,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
await this.renderStagingArea(state, prevState);
|
||||
this.arrangeEntities(state, prevState);
|
||||
|
||||
this.manager.stateApi.$toolState.set(state.tool);
|
||||
this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState());
|
||||
this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier);
|
||||
this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity());
|
||||
this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill());
|
||||
@ -96,11 +96,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
adapterMap.set(adapter.id, adapter);
|
||||
this.manager.stage.addLayer(adapter.konva.layer);
|
||||
}
|
||||
await adapter.update({
|
||||
state: entityState,
|
||||
toolState: state.tool,
|
||||
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
|
||||
});
|
||||
await adapter.update({ state: entityState });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -129,11 +125,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
adapterMap.set(adapter.id, adapter);
|
||||
this.manager.stage.addLayer(adapter.konva.layer);
|
||||
}
|
||||
await adapter.update({
|
||||
state: entityState,
|
||||
toolState: state.tool,
|
||||
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
|
||||
});
|
||||
await adapter.update({ state: entityState });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -167,11 +159,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
adapterMap.set(adapter.id, adapter);
|
||||
this.manager.stage.addLayer(adapter.konva.layer);
|
||||
}
|
||||
await adapter.update({
|
||||
state: entityState,
|
||||
toolState: state.tool,
|
||||
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
|
||||
});
|
||||
await adapter.update({ state: entityState });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -205,11 +193,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
|
||||
adapterMap.set(adapter.id, adapter);
|
||||
this.manager.stage.addLayer(adapter.konva.layer);
|
||||
}
|
||||
await adapter.update({
|
||||
state: entityState,
|
||||
toolState: state.tool,
|
||||
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
|
||||
});
|
||||
await adapter.update({ state: entityState });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,6 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
bboxChanged,
|
||||
brushWidthChanged,
|
||||
entityBrushLineAdded,
|
||||
entityEraserLineAdded,
|
||||
entityMoved,
|
||||
@ -15,17 +14,20 @@ import {
|
||||
entityRectAdded,
|
||||
entityReset,
|
||||
entitySelected,
|
||||
eraserWidthChanged,
|
||||
fillChanged,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors';
|
||||
import {
|
||||
brushWidthChanged,
|
||||
eraserWidthChanged,
|
||||
fillChanged,
|
||||
type ToolState,
|
||||
} from 'features/controlLayers/store/toolSlice';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
EntityBrushLineAddedPayload,
|
||||
EntityEraserLineAddedPayload,
|
||||
@ -97,7 +99,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
// Reminder - use arrow functions to avoid binding issues
|
||||
getState = () => {
|
||||
getCanvasState = () => {
|
||||
return this.store.getState().canvasV2;
|
||||
};
|
||||
resetEntity = (arg: EntityIdentifierPayload) => {
|
||||
@ -141,36 +143,36 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
);
|
||||
};
|
||||
getBbox = () => {
|
||||
return this.getState().bbox;
|
||||
return this.getCanvasState().bbox;
|
||||
};
|
||||
|
||||
getToolState = () => {
|
||||
return this.getState().tool;
|
||||
return this.store.getState().tool;
|
||||
};
|
||||
getSettings = () => {
|
||||
return this.getState().settings;
|
||||
return this.getCanvasState().settings;
|
||||
};
|
||||
getRegionsState = () => {
|
||||
return this.getState().regions;
|
||||
return this.getCanvasState().regions;
|
||||
};
|
||||
getRasterLayersState = () => {
|
||||
return this.getState().rasterLayers;
|
||||
return this.getCanvasState().rasterLayers;
|
||||
};
|
||||
getControlLayersState = () => {
|
||||
return this.getState().controlLayers;
|
||||
return this.getCanvasState().controlLayers;
|
||||
};
|
||||
getInpaintMasksState = () => {
|
||||
return this.getState().inpaintMasks;
|
||||
return this.getCanvasState().inpaintMasks;
|
||||
};
|
||||
getSession = () => {
|
||||
return this.getState().session;
|
||||
return this.getCanvasState().session;
|
||||
};
|
||||
getIsSelected = (id: string) => {
|
||||
return this.getState().selectedEntityIdentifier?.id === id;
|
||||
return this.getCanvasState().selectedEntityIdentifier?.id === id;
|
||||
};
|
||||
|
||||
getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null {
|
||||
const state = this.getState();
|
||||
const state = this.getCanvasState();
|
||||
|
||||
let entityState: EntityStateAndAdapter['state'] | null = null;
|
||||
let entityAdapter: EntityStateAndAdapter['adapter'] | null = null;
|
||||
@ -202,7 +204,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
getRenderedEntityCount = () => {
|
||||
const renderableEntities = selectAllRenderableEntities(this.getState());
|
||||
const renderableEntities = selectAllRenderableEntities(this.getCanvasState());
|
||||
let count = 0;
|
||||
for (const entity of renderableEntities) {
|
||||
if (entity.isEnabled) {
|
||||
@ -213,7 +215,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
getSelectedEntity = () => {
|
||||
const state = this.getState();
|
||||
const state = this.getCanvasState();
|
||||
if (state.selectedEntityIdentifier) {
|
||||
return this.getEntity(state.selectedEntityIdentifier);
|
||||
}
|
||||
@ -221,8 +223,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
getCurrentFill = () => {
|
||||
const state = this.getState();
|
||||
let currentFill: RgbaColor = state.tool.fill;
|
||||
let currentFill: RgbaColor = this.getToolState().fill;
|
||||
const selectedEntity = this.getSelectedEntity();
|
||||
if (selectedEntity) {
|
||||
// These two entity types use a compositing rect for opacity. Their fill is always a solid color.
|
||||
@ -239,15 +240,14 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
// The brush should use the mask opacity for these enktity types
|
||||
return { ...selectedEntity.state.fill.color, a: 1 };
|
||||
} else {
|
||||
const state = this.getState();
|
||||
return state.tool.fill;
|
||||
return this.getToolState().fill;
|
||||
}
|
||||
};
|
||||
|
||||
$transformingEntity = atom<CanvasEntityIdentifier | null>(null);
|
||||
$isProcessingTransform = atom<boolean>(false);
|
||||
|
||||
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
|
||||
$toolState: WritableAtom<ToolState> = atom();
|
||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
|
||||
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
||||
|
@ -15,7 +15,6 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
|
||||
import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors';
|
||||
import { sessionReducers } from 'features/controlLayers/store/sessionReducers';
|
||||
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
|
||||
import { toolReducers } from 'features/controlLayers/store/toolReducers';
|
||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
|
||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||
@ -57,16 +56,6 @@ const initialState: CanvasV2State = {
|
||||
},
|
||||
loras: [],
|
||||
ipAdapters: { entities: [] },
|
||||
tool: {
|
||||
invertScroll: false,
|
||||
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
|
||||
brush: {
|
||||
width: 50,
|
||||
},
|
||||
eraser: {
|
||||
width: 50,
|
||||
},
|
||||
},
|
||||
bbox: {
|
||||
rect: { x: 0, y: 0, width: 512, height: 512 },
|
||||
optimalDimension: 512,
|
||||
@ -109,7 +98,6 @@ export const canvasV2Slice = createSlice({
|
||||
// move out
|
||||
...lorasReducers,
|
||||
...settingsReducers,
|
||||
...toolReducers,
|
||||
...sessionReducers,
|
||||
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
@ -364,7 +352,6 @@ export const canvasV2Slice = createSlice({
|
||||
const size = pick(state.bbox.rect, 'width', 'height');
|
||||
state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension);
|
||||
state.session = deepClone(initialState.session);
|
||||
state.tool = deepClone(initialState.tool);
|
||||
|
||||
state.ipAdapters = deepClone(initialState.ipAdapters);
|
||||
state.rasterLayers = deepClone(initialState.rasterLayers);
|
||||
@ -403,10 +390,6 @@ export const canvasV2Slice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
brushWidthChanged,
|
||||
eraserWidthChanged,
|
||||
fillChanged,
|
||||
invertScrollChanged,
|
||||
clipToBboxChanged,
|
||||
canvasReset,
|
||||
settingsDynamicGridToggled,
|
||||
|
@ -1,17 +0,0 @@
|
||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
export const toolReducers = {
|
||||
brushWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.tool.brush.width = Math.round(action.payload);
|
||||
},
|
||||
eraserWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.tool.eraser.width = Math.round(action.payload);
|
||||
},
|
||||
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
|
||||
state.tool.fill = action.payload;
|
||||
},
|
||||
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.tool.invertScroll = action.payload;
|
||||
},
|
||||
} satisfies SliceCaseReducers<CanvasV2State>;
|
@ -0,0 +1,54 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
export type ToolState = {
|
||||
invertScroll: boolean;
|
||||
brush: { width: number };
|
||||
eraser: { width: number };
|
||||
fill: RgbaColor;
|
||||
};
|
||||
|
||||
const initialState: ToolState = {
|
||||
invertScroll: false,
|
||||
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
|
||||
brush: {
|
||||
width: 50,
|
||||
},
|
||||
eraser: {
|
||||
width: 50,
|
||||
},
|
||||
};
|
||||
|
||||
export const toolSlice = createSlice({
|
||||
name: 'tool',
|
||||
initialState,
|
||||
reducers: {
|
||||
brushWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.brush.width = Math.round(action.payload);
|
||||
},
|
||||
eraserWidthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.eraser.width = Math.round(action.payload);
|
||||
},
|
||||
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
|
||||
state.fill = action.payload;
|
||||
},
|
||||
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.invertScroll = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export const toolPersistConfig: PersistConfig<ToolState> = {
|
||||
name: toolSlice.name,
|
||||
initialState,
|
||||
migrate,
|
||||
persistDenylist: [],
|
||||
};
|
@ -715,12 +715,6 @@ export type CanvasV2State = {
|
||||
entities: CanvasIPAdapterState[];
|
||||
};
|
||||
loras: LoRA[];
|
||||
tool: {
|
||||
invertScroll: boolean;
|
||||
brush: { width: number };
|
||||
eraser: { width: number };
|
||||
fill: RgbaColor;
|
||||
};
|
||||
settings: {
|
||||
imageSmoothing: boolean;
|
||||
showHUD: boolean;
|
||||
|
Loading…
x
Reference in New Issue
Block a user