feat: add features of undo and redo

This commit is contained in:
chenshenhai 2025-05-31 13:57:09 +08:00
parent 61e01719d2
commit d8289d4d83
45 changed files with 93 additions and 62 deletions

View file

@ -1,6 +1,6 @@
{
"private": false,
"version": "0.4.0-beta.45",
"version": "0.4.0-rc.0",
"workspaces": [
"packages/*"
],

View file

@ -34,7 +34,7 @@ import {
} from '@idraw/util';
import { Board, Sharer, Calculator } from './board';
import { createBoardContent, validateElements } from '@idraw/util';
import { Cursor } from './lib/cursor';
import { Cursor } from './cursor/cursor';
import { getModifyElementRecord } from './record';
export { coreEventKeys } from './config';
@ -43,15 +43,15 @@ export type { CoreEventKeys } from './config';
export { Board, Sharer, Calculator };
// export { MiddlewareSelector } from './middleware/selector';
export { MiddlewareSelector } from './middleware/selector';
export { MiddlewareScroller } from './middleware/scroller';
export { MiddlewareScaler } from './middleware/scaler';
export { MiddlewareRuler } from './middleware/ruler';
export { MiddlewareTextEditor } from './middleware/text-editor';
export { MiddlewareDragger } from './middleware/dragger';
export { MiddlewareInfo } from './middleware/info';
export { MiddlewareLayoutSelector } from './middleware/layout-selector';
export { MiddlewarePointer } from './middleware/pointer';
export { MiddlewareSelector } from './middlewares/selector';
export { MiddlewareScroller } from './middlewares/scroller';
export { MiddlewareScaler } from './middlewares/scaler';
export { MiddlewareRuler } from './middlewares/ruler';
export { MiddlewareTextEditor } from './middlewares/text-editor';
export { MiddlewareDragger } from './middlewares/dragger';
export { MiddlewareInfo } from './middlewares/info';
export { MiddlewareLayoutSelector } from './middlewares/layout-selector';
export { MiddlewarePointer } from './middlewares/pointer';
export class Core<E extends CoreEventMap = CoreEventMap> {
#board: Board<E>;

View file

@ -391,7 +391,7 @@ export const MiddlewareLayoutSelector: Middleware<
};
}
eventHub.trigger(coreEventKeys.CHANGE, {
type: 'dragLayout',
type: 'resizeLayout',
data,
modifyRecord
});

View file

@ -120,6 +120,9 @@ export const MiddlewareSelector: Middleware<
devicePixelRatio: sharer.getActiveViewSizeInfo().devicePixelRatio
});
let startResizeGroupRecord: ModifyRecord<'resizeElements'> | null = null;
let endResizeGroupRecord: ModifyRecord<'resizeElements'> | null = null;
sharer.setSharedStorage(keyActionType, null);
sharer.setSharedStorage(keyEnableSnapToGrid, true);
@ -213,6 +216,8 @@ export const MiddlewareSelector: Middleware<
};
const clear = () => {
startResizeGroupRecord = null;
endResizeGroupRecord = null;
sharer.setSharedStorage(keyActionType, null);
sharer.setSharedStorage(keyResizeType, null);
sharer.setSharedStorage(keyAreaStart, null);
@ -419,6 +424,8 @@ export const MiddlewareSelector: Middleware<
prevPoint = e.point;
moveOriginalStartPoint = e.point;
startResizeGroupRecord = null;
endResizeGroupRecord = null;
sharer.setSharedStorage(keyActionType, null);
sharer.setSharedStorage(keyResizeType, null);
sharer.setSharedStorage(keyAreaStart, null);
@ -674,7 +681,7 @@ export const MiddlewareSelector: Middleware<
const gridW = calculator.toGridNum(resizedElemSize.w, calcOpts);
const gridH = calculator.toGridNum(resizedElemSize.h, calcOpts);
if (elems[0].type === 'group') {
resizeEffectGroupElement(
endResizeGroupRecord = resizeEffectGroupElement(
elems[0] as Element<'group'>,
{
x: gridX,
@ -684,6 +691,9 @@ export const MiddlewareSelector: Middleware<
},
{ resizeEffect: elems[0].operations?.resizeEffect }
);
if (!startResizeGroupRecord) {
startResizeGroupRecord = endResizeGroupRecord;
}
elems[0].x = gridX;
elems[0].y = gridY;
} else {
@ -791,16 +801,16 @@ export const MiddlewareSelector: Middleware<
}
if (data && (['drag', 'drag-list', 'drag-list-end', 'resize'] as ActionType[]).includes(actionType)) {
let type: any = 'dragElement';
let type: any = 'resizeElement';
if (type === 'resize') {
type = 'resizeElement';
}
if (hasChangedData) {
const startSize = pointStartElementSizeList[0] as ElementSize & { uuid: string };
let modifyRecord: ModifyRecord | undefined = undefined;
let modifyRecord: ModifyRecord | null | undefined = null;
if (selectedElements.length === 1) {
modifyRecord = {
type: 'dragElement',
type: 'resizeElement',
time: 0,
content: {
method: 'modifyElement',
@ -809,9 +819,18 @@ export const MiddlewareSelector: Middleware<
after: toFlattenElement(getElementSize(selectedElements[0]))
}
};
if (selectedElements[0].type === 'group' && startResizeGroupRecord && endResizeGroupRecord) {
modifyRecord = {
...endResizeGroupRecord,
content: {
...endResizeGroupRecord.content,
before: startResizeGroupRecord.content.before
}
};
}
} else if (selectedElements.length > 1) {
modifyRecord = {
type: 'dragElements',
type: 'resizeElements',
time: 0,
content: {
method: 'modifyElements',

View file

@ -238,15 +238,15 @@ export class iDraw {
return this.#core.forceRender();
}
// getHistoryHandler() {
// return this.#historyHandler;
// }
getHistoryHandler() {
return this.#historyHandler;
}
// redo() {
// this.#historyHandler?.redo();
// }
redo() {
this.#historyHandler?.redo();
}
// undo() {
// this.#historyHandler?.undo();
// }
undo() {
this.#historyHandler?.undo();
}
}

View file

@ -18,9 +18,9 @@ const supportRecordTypes = [
'deleteElement',
'moveElement',
'addElement',
'dragElement',
'resizeElement',
'dragLayout',
'resizeElements',
'resizeLayout',
'modifyLayout',
'modifyGlobal'
];
@ -90,7 +90,7 @@ export const useHistory = (opts: { core: Core; limit?: number }) => {
undoRecord = core.modifyGlobal(info) as ModifyRecord;
} else if (record.content.method === 'modifyElements') {
undoRecord = core.modifyElements(
record.content.before.forEach((item) => unflatObject(item)) as unknown as Array<
record.content.before.map((item) => unflatObject(item)) as unknown as Array<
RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
>
) as ModifyRecord;
@ -153,7 +153,7 @@ export const useHistory = (opts: { core: Core; limit?: number }) => {
redoRecord = core.modifyGlobal(info) as ModifyRecord;
} else if (record.content.method === 'modifyElements') {
redoRecord = core.modifyElements(
record.content.before.forEach((item) => unflatObject(item)) as unknown as Array<
record.content.before.map((item) => unflatObject(item)) as unknown as Array<
RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
>
) as ModifyRecord;

View file

@ -1,7 +1,7 @@
import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types';
import { rotateElement, calcViewElementSize, enhanceFontFamliy } from '@idraw/util';
import { is, isColorStr, getDefaultElementDetailConfig } from '@idraw/util';
import { drawBox, drawBoxShadow } from './box';
import { drawBox, drawBoxShadow, getOpacity } from './box';
const detailConfig = getDefaultElementDetailConfig();
@ -35,6 +35,9 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
return;
}
const { parentOpacity } = opts;
const opacity = getOpacity(elem) * parentOpacity;
ctx.globalAlpha = opacity;
ctx.fillStyle = elem.detail.color || detailConfig.color;
ctx.textBaseline = 'top';
ctx.$setFont({
@ -64,6 +67,8 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
});
}
}
ctx.globalAlpha = parentOpacity;
}
});
}

View file

@ -1,5 +1,12 @@
import type { ElementBaseDetail, ElementTextDetail, ElementGroupDetail } from './element';
export type DefaultElementDetailConfig = Required<Omit<ElementBaseDetail, 'clipPath' | 'background'>> &
Required<Pick<ElementTextDetail, 'color' | 'textAlign' | 'verticalAlign' | 'fontSize' | 'fontFamily' | 'fontWeight' | 'minInlineSize' | 'wordBreak'>> &
export type DefaultElementDetailConfig = Required<
Omit<ElementBaseDetail, 'clipPath' | 'clipPathStrokeWidth' | 'clipPathStrokeColor' | 'background'>
> &
Required<
Pick<
ElementTextDetail,
'color' | 'textAlign' | 'verticalAlign' | 'fontSize' | 'fontFamily' | 'fontWeight' | 'minInlineSize' | 'wordBreak'
>
> &
Required<Pick<ElementGroupDetail, 'overflow'>>;

View file

@ -39,7 +39,7 @@ export interface CoreEventChange<T extends ModifyType = ModifyType> {
type: T | 'setData' | 'other' | string;
selectedElements?: Element[] | null;
hoverElement?: Element | null;
modifyRecord?: ModifyRecord<T>;
modifyRecord?: ModifyRecord<T> | null;
}
export interface CoreEventScale {
scale: number;

View file

@ -7,10 +7,10 @@ export type ModifyMethod =
| 'deleteElement'
| 'moveElement'
| 'addElement'
| 'dragElement'
| 'dragElements'
| 'resizeElement'
| 'resizeElements'
| 'modifyElements'
| 'dragLayout'
| 'resizeLayout'
| 'modifyLayout'
| 'modifyGlobal';
@ -92,19 +92,19 @@ export interface ModifyContentMap {
from: ElementPosition;
to: ElementPosition;
};
dragElement: {
resizeElement: {
method: 'modifyElement';
uuid: string;
before: FlattenElement | null;
after: FlattenElement | null;
};
dragElements: {
resizeElements: {
method: 'modifyElements';
before: (FlattenLayout & { uuid: string })[];
after: (FlattenLayout & { uuid: string })[];
};
dragLayout: {
method: 'modifyElement';
resizeLayout: {
method: 'modifyLayout';
before: FlattenLayout;
after: FlattenLayout;
};
@ -151,13 +151,13 @@ export interface ModifyRecordMap {
time: number;
content: ModifyContentMap['moveElement'];
};
dragElement: {
type: 'dragElement';
resizeElement: {
type: 'resizeElement';
time: number;
content: ModifyContentMap['modifyElement'];
};
dragElements: {
type: 'dragElements';
resizeElements: {
type: 'resizeElements';
time: number;
content: ModifyContentMap['modifyElements'];
};
@ -166,10 +166,10 @@ export interface ModifyRecordMap {
time: number;
content: ModifyContentMap['modifyElements'];
};
dragLayout: {
type: 'dragLayout';
resizeLayout: {
type: 'resizeLayout';
time: number;
content: ModifyContentMap['dragLayout'];
content: ModifyContentMap['resizeLayout'];
};
modifyLayout: {
type: 'modifyLayout';

View file

@ -61,7 +61,7 @@ export function getDefaultElementTextDetail(elementSize: ElementSize): ElementTe
color: detailConfig.color,
fontFamily: detailConfig.fontFamily,
fontWeight: detailConfig.fontWeight,
lineHeight: elementSize.w / defaultText.length,
// lineHeight: elementSize.w / defaultText.length,
fontSize: elementSize.w / defaultText.length,
textAlign: 'center',
verticalAlign: 'middle'

View file

@ -105,6 +105,10 @@ function resizeElementBaseDetailByRatio(elem: Element, opts: DeepResizeRatioOpti
function resizeElementBaseByRatio(elem: Element, opts: DeepResizeRatioOptions): ModifyRecord<'modifyElement'> {
const { xRatio, yRatio } = opts;
const { uuid, x, y, w, h } = elem;
elem.x = doNum(x * xRatio);
elem.y = doNum(y * yRatio);
elem.w = doNum(w * xRatio);
elem.h = doNum(h * yRatio);
const record: ModifyRecord<'modifyElement'> = {
type: 'modifyElement',
time: Date.now(),
@ -112,13 +116,10 @@ function resizeElementBaseByRatio(elem: Element, opts: DeepResizeRatioOptions):
method: 'modifyElement',
uuid: uuid,
before: { x, y, w, h },
after: { x, y, w, h }
after: { x: elem.x, y: elem.y, w: elem.w, h: elem.h }
}
};
elem.x = doNum(x * xRatio);
elem.y = doNum(y * yRatio);
elem.w = doNum(w * xRatio);
elem.h = doNum(h * yRatio);
const detailRecord = resizeElementBaseDetailByRatio(elem, opts);
record.content.before = {
...record.content.before,
@ -170,7 +171,7 @@ function resizeTextElementDetailByRatio(
function deepResizeElementByRatio(
elem: Element,
opts: DeepResizeRatioOptions,
record?: ModifyRecord<'modifyElements'>
record?: ModifyRecord<'resizeElements'>
) {
const { type, uuid } = elem;
@ -210,7 +211,7 @@ function deepResizeElementByRatio(
function fixedResizeGroupElementChildren(
elem: Element<'group'>,
opts: FixedResizeOptions,
record?: ModifyRecord<'modifyElements'>
record?: ModifyRecord<'resizeElements'>
) {
if (!(elem.type === 'group' && Array.isArray(elem.detail.children))) {
return;
@ -250,12 +251,9 @@ export function resizeEffectGroupElement(
opts?: {
resizeEffect?: ElementOperations['resizeEffect'];
}
): ModifyRecord<'modifyElements'> | null {
if (!istype.number(size.x) && !istype.number(size.y)) {
return null;
}
const record: ModifyRecord<'modifyElements'> = {
type: 'modifyElements',
): ModifyRecord<'resizeElements'> | null {
const record: ModifyRecord<'resizeElements'> = {
type: 'resizeElements',
time: Date.now(),
content: {
method: 'modifyElements',
@ -279,6 +277,8 @@ export function resizeEffectGroupElement(
const afterGroupElem: FlattenLayout & { uuid: string } = { uuid, x: resizeX, y: resizeY, w: resizeW, h: resizeH };
if (opts?.resizeEffect === 'deepResize') {
record.content.before.push(beforeGroupElem);
record.content.after.push(afterGroupElem);
const xRatio = resizeW / elem.w;
const yRatio = resizeH / elem.h;
if (xRatio === yRatio && xRatio === 1) {