mirror of
https://github.com/idrawjs/idraw
synced 2026-05-23 17:48:23 +00:00
feat: add middleware of layout selector
This commit is contained in:
parent
9f4ba90090
commit
c0de3034ec
20 changed files with 626 additions and 27 deletions
1
packages/core/src/config.ts
Normal file
1
packages/core/src/config.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const eventChange = 'change';
|
||||
|
|
@ -2,6 +2,7 @@ import type { Data, PointSize, CoreOptions, BoardMiddleware, ViewSizeInfo, CoreE
|
|||
import { Board } from '@idraw/board';
|
||||
import { createBoardContent, validateElements } from '@idraw/util';
|
||||
import { Cursor } from './lib/cursor';
|
||||
export { eventChange } from './config';
|
||||
|
||||
// export { MiddlewareSelector } from './middleware/selector';
|
||||
export { MiddlewareSelector, middlewareEventSelect, middlewareEventSelectClear } from './middleware/selector';
|
||||
|
|
@ -11,6 +12,7 @@ export { MiddlewareRuler, middlewareEventRuler } from './middleware/ruler';
|
|||
export { MiddlewareTextEditor, middlewareEventTextEdit, middlewareEventTextChange } from './middleware/text-editor';
|
||||
export { MiddlewareDragger } from './middleware/dragger';
|
||||
export { MiddlewareInfo } from './middleware/info';
|
||||
export { MiddlewareLayoutSelector } from './middleware/layout-selector';
|
||||
|
||||
export class Core<E extends CoreEventMap = CoreEventMap> {
|
||||
#board: Board<E>;
|
||||
|
|
|
|||
8
packages/core/src/middleware/layout-selector/config.ts
Normal file
8
packages/core/src/middleware/layout-selector/config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const key = 'LAYOUT_SELECT';
|
||||
// export const keyHoverElement = Symbol(`${key}_hoverElementSize`);
|
||||
export const keyLayoutActionType = Symbol(`${key}_layoutActionType`); // 'hover' | 'resize' | null = null;
|
||||
export const keyLayoutControlType = Symbol(`${key}_layoutControlType`); // ControlType | null;
|
||||
export const keyLayoutController = Symbol(`${key}_layoutController`); // ElementSizeController | null = null;
|
||||
|
||||
export const selectColor = '#1973ba';
|
||||
export const disableColor = '#5b5959b5';
|
||||
261
packages/core/src/middleware/layout-selector/index.ts
Normal file
261
packages/core/src/middleware/layout-selector/index.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import type { BoardMiddleware, ElementSize, Point } from '@idraw/types';
|
||||
import { calcLayoutSizeController, isViewPointInVertexes, getViewScaleInfoFromSnapshot } from '@idraw/util';
|
||||
import type { LayoutSelectorSharedStorage, ControlType } from './types';
|
||||
import { keyLayoutActionType, keyLayoutController, keyLayoutControlType } from './config';
|
||||
import { keyActionType as keyElementActionType, middlewareEventSelectClear } from '../selector';
|
||||
import { drawLayoutController } from './util';
|
||||
import { eventChange } from '../../config';
|
||||
|
||||
export const MiddlewareLayoutSelector: BoardMiddleware<LayoutSelectorSharedStorage> = (opts) => {
|
||||
const { sharer, boardContent, calculator, viewer, eventHub } = opts;
|
||||
const { helperContext } = boardContent;
|
||||
|
||||
let prevPoint: Point | null = null;
|
||||
|
||||
const clear = () => {
|
||||
prevPoint = null;
|
||||
sharer.setSharedStorage(keyLayoutActionType, null);
|
||||
sharer.setSharedStorage(keyLayoutControlType, null);
|
||||
sharer.setSharedStorage(keyLayoutController, null);
|
||||
};
|
||||
|
||||
const isInElementAction = () => {
|
||||
const elementType = sharer.getSharedStorage(keyElementActionType);
|
||||
if (elementType) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isDisbaledControl = (controlType: ControlType) => {
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data?.layout?.operations) {
|
||||
const operations = data.layout.operations;
|
||||
if (controlType === 'left' && operations.disableLeft === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'top' && operations.disableTop === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'right' && operations.disableRight === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'bottom' && operations.disableBottom === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'top-left' && operations.disableTopLeft === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'top-right' && operations.disableTopRight === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'bottom-left' && operations.disableBottomLeft === true) {
|
||||
return true;
|
||||
}
|
||||
if (controlType === 'bottom-right' && operations.disableBottomRight === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getLayoutSize = () => {
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data?.layout) {
|
||||
const { x, y, w, h } = data.layout;
|
||||
return { x, y, w, h };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const resetController = () => {
|
||||
const viewScaleInfo = sharer.getActiveViewScaleInfo();
|
||||
const size: ElementSize | null = getLayoutSize();
|
||||
if (size) {
|
||||
const controller = calcLayoutSizeController(size, { viewScaleInfo, controllerSize: 10 });
|
||||
sharer.setSharedStorage(keyLayoutController, controller);
|
||||
} else {
|
||||
sharer.setSharedStorage(keyLayoutController, null);
|
||||
}
|
||||
};
|
||||
|
||||
const resetControlType = (e?: { point: Point }) => {
|
||||
const data = sharer.getActiveStorage('data');
|
||||
const controller = sharer.getSharedStorage(keyLayoutController);
|
||||
if (controller && data?.layout && e?.point) {
|
||||
// sharer.setSharedStorage(keyLayoutControlType, null);
|
||||
let layoutControlType: ControlType | null = null;
|
||||
if (controller) {
|
||||
const { topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left } = controller;
|
||||
const list = [topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (isViewPointInVertexes(e.point, item.vertexes)) {
|
||||
layoutControlType = `${item.type}` as ControlType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (layoutControlType) {
|
||||
sharer.setSharedStorage(keyLayoutControlType, layoutControlType);
|
||||
eventHub.trigger(middlewareEventSelectClear, {});
|
||||
return layoutControlType;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
name: '@middleware/layout-selector',
|
||||
use: () => {
|
||||
clear();
|
||||
resetController();
|
||||
},
|
||||
hover: (e) => {
|
||||
if (isInElementAction()) {
|
||||
return;
|
||||
}
|
||||
const prevLayoutActionType = sharer.getSharedStorage(keyLayoutActionType);
|
||||
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data?.layout && prevLayoutActionType !== 'resize') {
|
||||
resetController();
|
||||
const layoutControlType = resetControlType(e);
|
||||
if (layoutControlType) {
|
||||
sharer.setSharedStorage(keyLayoutActionType, 'hover');
|
||||
if (!isDisbaledControl(layoutControlType)) {
|
||||
eventHub.trigger('cursor', {
|
||||
type: `resize-${layoutControlType}`,
|
||||
groupQueue: [],
|
||||
element: getLayoutSize()
|
||||
});
|
||||
}
|
||||
|
||||
viewer.drawFrame();
|
||||
} else {
|
||||
sharer.setSharedStorage(keyLayoutActionType, null);
|
||||
}
|
||||
}
|
||||
if (['hover', 'resize'].includes(sharer.getSharedStorage(keyLayoutActionType) as string)) {
|
||||
return false;
|
||||
}
|
||||
if (prevLayoutActionType === 'hover' && !sharer.getSharedStorage(keyLayoutActionType)) {
|
||||
viewer.drawFrame();
|
||||
}
|
||||
},
|
||||
pointStart: (e) => {
|
||||
if (isInElementAction()) {
|
||||
return;
|
||||
}
|
||||
resetController();
|
||||
const layoutControlType = resetControlType(e);
|
||||
prevPoint = e.point;
|
||||
if (layoutControlType) {
|
||||
if (isDisbaledControl(layoutControlType)) {
|
||||
return;
|
||||
}
|
||||
sharer.setSharedStorage(keyLayoutActionType, 'resize');
|
||||
return false;
|
||||
}
|
||||
const layoutActionType = sharer.getSharedStorage(keyLayoutActionType);
|
||||
if (['hover', 'resize'].includes(layoutActionType as string)) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
pointMove: (e) => {
|
||||
if (isInElementAction()) {
|
||||
return;
|
||||
}
|
||||
const layoutActionType = sharer.getSharedStorage(keyLayoutActionType);
|
||||
const layoutControlType = sharer.getSharedStorage(keyLayoutControlType);
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (layoutControlType && isDisbaledControl(layoutControlType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutActionType === 'resize' && layoutControlType && data?.layout) {
|
||||
if (prevPoint) {
|
||||
const scale = sharer.getActiveStorage('scale');
|
||||
const moveX = (e.point.x - prevPoint.x) / scale;
|
||||
const moveY = (e.point.y - prevPoint.y) / scale;
|
||||
const { x, y, w, h } = data.layout;
|
||||
|
||||
if (layoutControlType === 'top') {
|
||||
data.layout.y = calculator.toGridNum(y + moveY);
|
||||
data.layout.h = calculator.toGridNum(h - moveY);
|
||||
} else if (layoutControlType === 'right') {
|
||||
data.layout.w = calculator.toGridNum(w + moveX);
|
||||
} else if (layoutControlType === 'bottom') {
|
||||
data.layout.h = calculator.toGridNum(h + moveY);
|
||||
} else if (layoutControlType === 'left') {
|
||||
data.layout.x = calculator.toGridNum(x + moveX);
|
||||
data.layout.w = calculator.toGridNum(w - moveX);
|
||||
} else if (layoutControlType === 'top-left') {
|
||||
data.layout.x = calculator.toGridNum(x + moveX);
|
||||
data.layout.y = calculator.toGridNum(y + moveY);
|
||||
data.layout.w = calculator.toGridNum(w - moveX);
|
||||
data.layout.h = calculator.toGridNum(h - moveY);
|
||||
} else if (layoutControlType === 'top-right') {
|
||||
data.layout.y = calculator.toGridNum(y + moveY);
|
||||
data.layout.w = calculator.toGridNum(w + moveX);
|
||||
data.layout.h = calculator.toGridNum(h - moveY);
|
||||
} else if (layoutControlType === 'bottom-right') {
|
||||
data.layout.w = calculator.toGridNum(w + moveX);
|
||||
data.layout.h = calculator.toGridNum(h + moveY);
|
||||
} else if (layoutControlType === 'bottom-left') {
|
||||
data.layout.x = calculator.toGridNum(x + moveX);
|
||||
data.layout.w = calculator.toGridNum(w - moveX);
|
||||
data.layout.h = calculator.toGridNum(h + moveY);
|
||||
}
|
||||
}
|
||||
prevPoint = e.point;
|
||||
resetController();
|
||||
viewer.drawFrame();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (['hover', 'resize'].includes(layoutActionType as string)) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
pointEnd: () => {
|
||||
const layoutActionType = sharer.getSharedStorage(keyLayoutActionType);
|
||||
const layoutControlType = sharer.getSharedStorage(keyLayoutControlType);
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data && layoutActionType === 'resize' && layoutControlType && !isDisbaledControl(layoutControlType)) {
|
||||
eventHub.trigger(eventChange, {
|
||||
type: 'changeLayout',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
clear();
|
||||
},
|
||||
beforeDrawFrame: ({ snapshot }) => {
|
||||
const { sharedStore, activeStore } = snapshot;
|
||||
const layoutActionType = sharedStore[keyLayoutActionType];
|
||||
const layoutControlType = sharedStore[keyLayoutControlType];
|
||||
|
||||
if (activeStore.data?.layout && layoutActionType && layoutControlType) {
|
||||
if (['hover', 'resize'].includes(layoutActionType)) {
|
||||
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
|
||||
const { x, y, w, h } = activeStore.data.layout;
|
||||
const size = { x, y, w, h };
|
||||
const controller = calcLayoutSizeController(size, { viewScaleInfo, controllerSize: 10 });
|
||||
|
||||
drawLayoutController(helperContext, { controller, operations: activeStore.data.layout.operations || {} });
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollX: () => {
|
||||
clear();
|
||||
},
|
||||
scrollY: () => {
|
||||
clear();
|
||||
},
|
||||
wheelScale: () => {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
};
|
||||
15
packages/core/src/middleware/layout-selector/types.ts
Normal file
15
packages/core/src/middleware/layout-selector/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { LayoutSizeController } from '@idraw/types';
|
||||
import { keyLayoutActionType, keyLayoutControlType, keyLayoutController } from './config';
|
||||
import { keyActionType as keyElementActionType } from '../selector';
|
||||
import type { ActionType as ElementActionType } from '../selector';
|
||||
|
||||
export type ActionType = 'hover' | 'resize' | null;
|
||||
|
||||
export type ControlType = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
|
||||
export type LayoutSelectorSharedStorage = {
|
||||
[keyLayoutActionType]: ActionType | null;
|
||||
[keyLayoutControlType]: ControlType | null;
|
||||
[keyLayoutController]: LayoutSizeController | null;
|
||||
[keyElementActionType]: ElementActionType | null;
|
||||
};
|
||||
111
packages/core/src/middleware/layout-selector/util.ts
Normal file
111
packages/core/src/middleware/layout-selector/util.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import type { ViewContext2D, LayoutSizeController, DataLayout, ViewRectVertexes, PointSize } from '@idraw/types';
|
||||
import { selectColor, disableColor } from './config';
|
||||
|
||||
function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes) {
|
||||
ctx.setLineDash([]);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boxVertexes[0].x, boxVertexes[0].y);
|
||||
ctx.lineTo(boxVertexes[1].x, boxVertexes[1].y);
|
||||
ctx.lineTo(boxVertexes[2].x, boxVertexes[2].y);
|
||||
ctx.lineTo(boxVertexes[3].x, boxVertexes[3].y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = selectColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boxVertexes[0].x, boxVertexes[0].y);
|
||||
ctx.lineTo(boxVertexes[1].x, boxVertexes[1].y);
|
||||
ctx.lineTo(boxVertexes[2].x, boxVertexes[2].y);
|
||||
ctx.lineTo(boxVertexes[3].x, boxVertexes[3].y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawControllerCross(ctx: ViewContext2D, opts: { vertexes: ViewRectVertexes; strokeStyle: string; lineWidth: number }) {
|
||||
const { vertexes, strokeStyle, lineWidth } = opts;
|
||||
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(vertexes[0].x, vertexes[0].y);
|
||||
ctx.lineTo(vertexes[2].x, vertexes[2].y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(vertexes[1].x, vertexes[1].y);
|
||||
ctx.lineTo(vertexes[3].x, vertexes[3].y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawControllerLine(ctx: ViewContext2D, opts: { start: PointSize; end: PointSize; centerVertexes: ViewRectVertexes; disabled: boolean }) {
|
||||
const { start, end, centerVertexes, disabled } = opts;
|
||||
const lineWidth = disabled === true ? 1 : 2;
|
||||
const strokeStyle = disabled === true ? disableColor : selectColor;
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(start.x, start.y);
|
||||
ctx.lineTo(end.x, end.y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
if (disabled === true) {
|
||||
drawControllerCross(ctx, {
|
||||
vertexes: centerVertexes,
|
||||
lineWidth,
|
||||
strokeStyle
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function drawLayoutController(
|
||||
ctx: ViewContext2D,
|
||||
opts: {
|
||||
controller: LayoutSizeController;
|
||||
operations: DataLayout['operations'];
|
||||
}
|
||||
) {
|
||||
// TODO
|
||||
const { controller, operations } = opts;
|
||||
const { topLeft, topRight, bottomLeft, bottomRight, topMiddle, rightMiddle, bottomMiddle, leftMiddle } = controller;
|
||||
|
||||
drawControllerLine(ctx, { start: topLeft.center, end: topRight.center, centerVertexes: topMiddle.vertexes, disabled: !!operations?.disableTop });
|
||||
drawControllerLine(ctx, { start: topRight.center, end: bottomRight.center, centerVertexes: rightMiddle.vertexes, disabled: !!operations?.disableRight });
|
||||
drawControllerLine(ctx, { start: bottomRight.center, end: bottomLeft.center, centerVertexes: bottomMiddle.vertexes, disabled: !!operations?.disableBottom });
|
||||
drawControllerLine(ctx, { start: bottomLeft.center, end: topLeft.center, centerVertexes: leftMiddle.vertexes, disabled: !!operations?.disableLeft });
|
||||
|
||||
const disabledOpts = {
|
||||
lineWidth: 1,
|
||||
strokeStyle: disableColor
|
||||
};
|
||||
if (operations?.disableTopLeft === true) {
|
||||
drawControllerCross(ctx, { vertexes: topLeft.vertexes, ...disabledOpts });
|
||||
} else {
|
||||
drawControllerBox(ctx, topLeft.vertexes);
|
||||
}
|
||||
|
||||
if (operations?.disableTopRight === true) {
|
||||
drawControllerCross(ctx, { vertexes: topRight.vertexes, ...disabledOpts });
|
||||
} else {
|
||||
drawControllerBox(ctx, topRight.vertexes);
|
||||
}
|
||||
|
||||
if (operations?.disableBottomRight === true) {
|
||||
drawControllerCross(ctx, { vertexes: bottomRight.vertexes, ...disabledOpts });
|
||||
} else {
|
||||
drawControllerBox(ctx, bottomRight.vertexes);
|
||||
}
|
||||
|
||||
if (operations?.disableBottomLeft === true) {
|
||||
drawControllerCross(ctx, { vertexes: bottomLeft.vertexes, ...disabledOpts });
|
||||
} else {
|
||||
drawControllerBox(ctx, bottomLeft.vertexes);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,9 +70,10 @@ import {
|
|||
} from './config';
|
||||
import { calcReferenceInfo } from './reference';
|
||||
import { middlewareEventTextEdit } from '../text-editor';
|
||||
import { eventChange } from '../../config';
|
||||
|
||||
export { keySelectedElementList, keyActionType, keyResizeType, keyGroupQueue };
|
||||
export type { DeepSelectorSharedStorage };
|
||||
export type { DeepSelectorSharedStorage, ActionType };
|
||||
|
||||
export const middlewareEventSelect: string = '@middleware/select';
|
||||
|
||||
|
|
@ -292,6 +293,11 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
triggerCursor(target);
|
||||
|
||||
if (target.type === null) {
|
||||
if (sharer.getSharedStorage(keyHoverElement) || sharer.getSharedStorage(keyHoverElementVertexes)) {
|
||||
sharer.setSharedStorage(keyHoverElement, null);
|
||||
sharer.setSharedStorage(keyHoverElementVertexes, null);
|
||||
viewer.drawFrame();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +320,6 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
}
|
||||
|
||||
if (target.type === 'over-element' && target?.elements?.length === 1) {
|
||||
// sharer.setSharedStorage(keyHoverElement, target.elements[0]);
|
||||
updateHoverElement(target.elements[0]);
|
||||
viewer.drawFrame();
|
||||
return;
|
||||
|
|
@ -580,7 +585,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
prevPoint = e.point;
|
||||
|
||||
// if (data && (['drag', 'drag-list', 'drag-list-end', 'resize'] as ActionType[]).includes(actionType)) {
|
||||
// eventHub.trigger('change', { data });
|
||||
// eventHub.trigger(eventChange, { data });
|
||||
// }
|
||||
},
|
||||
|
||||
|
|
@ -657,7 +662,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
if (type === 'resize') {
|
||||
type = 'resizeElement';
|
||||
}
|
||||
eventHub.trigger('change', { data, type });
|
||||
eventHub.trigger(eventChange, { data, type });
|
||||
}
|
||||
viewer.drawFrame();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CoreEventMap, Data } from '@idraw/types';
|
||||
import { eventChange } from '@idraw/core';
|
||||
|
||||
import {
|
||||
middlewareEventRuler,
|
||||
|
|
@ -9,12 +10,23 @@ import {
|
|||
middlewareEventTextChange
|
||||
} from '@idraw/core';
|
||||
|
||||
const idrawEventChange = 'change';
|
||||
const idrawEventChange = eventChange;
|
||||
|
||||
export type IDrawEvent = CoreEventMap & {
|
||||
change: {
|
||||
[idrawEventChange]: {
|
||||
data: Data;
|
||||
type: 'updateElement' | 'deleteElement' | 'moveElement' | 'addElement' | 'dragElement' | 'resizeElement' | 'setData' | 'undo' | 'redo' | 'other';
|
||||
type:
|
||||
| 'updateElement'
|
||||
| 'deleteElement'
|
||||
| 'moveElement'
|
||||
| 'addElement'
|
||||
| 'dragElement'
|
||||
| 'resizeElement'
|
||||
| 'setData'
|
||||
| 'undo'
|
||||
| 'redo'
|
||||
| 'changeLayout' // TODO
|
||||
| 'other';
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export class iDraw {
|
|||
centerContent(opts?: { data?: Data }) {
|
||||
const data = opts?.data || this.#core.getData();
|
||||
const { viewSizeInfo } = this.getViewInfo();
|
||||
if (Array.isArray(data?.elements) && data?.elements.length > 0) {
|
||||
if (data?.layout || (Array.isArray(data?.elements) && data?.elements.length > 0)) {
|
||||
const result = calcViewCenterContent(data, { viewSizeInfo });
|
||||
this.setViewScale(result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { IDrawMode, IDrawStorage } from '@idraw/types';
|
|||
import { Store } from '@idraw/util';
|
||||
import {
|
||||
Core,
|
||||
MiddlewareLayoutSelector,
|
||||
MiddlewareSelector,
|
||||
MiddlewareScroller,
|
||||
MiddlewareScaler,
|
||||
|
|
@ -25,8 +26,10 @@ export function runMiddlewares(core: Core<IDrawEvent>, store: Store<IDrawStorage
|
|||
}
|
||||
|
||||
if (enableSelect === true) {
|
||||
core.use(MiddlewareLayoutSelector);
|
||||
core.use(MiddlewareSelector);
|
||||
} else if (enableSelect === false) {
|
||||
core.disuse(MiddlewareLayoutSelector);
|
||||
core.disuse(MiddlewareSelector);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { drawBoxShadow, drawBoxBackground, drawBoxBorder } from './box';
|
|||
|
||||
export function drawLayout(ctx: ViewContext2D, layout: DataLayout, opts: RendererDrawElementOptions, renderContent: (ctx: ViewContext2D) => void) {
|
||||
const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const elem: Element = { uuid: 'layout', type: 'group', x: 0, y: 0, ...layout };
|
||||
const elem: Element = { uuid: 'layout', type: 'group', ...layout };
|
||||
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
|
||||
const angle = 0;
|
||||
const viewElem: Element = { ...elem, ...{ x, y, w, h, angle } } as Element;
|
||||
|
|
@ -19,7 +19,7 @@ export function drawLayout(ctx: ViewContext2D, layout: DataLayout, opts: Rendere
|
|||
|
||||
if (layout.detail.overflow === 'hidden') {
|
||||
const { viewScaleInfo, viewSizeInfo } = opts;
|
||||
const elem: Element<'group'> = { uuid: 'layout', type: 'group', x: 0, y: 0, ...layout } as Element<'group'>;
|
||||
const elem: Element<'group'> = { uuid: 'layout', type: 'group', ...layout } as Element<'group'>;
|
||||
const viewElemSize = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
|
||||
const viewElem = { ...elem, ...viewElemSize };
|
||||
const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const detailConfig = getDefaultElementDetailConfig();
|
|||
|
||||
export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) {
|
||||
const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
|
||||
const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
|
||||
const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem;
|
||||
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
|
||||
rotateElement(ctx, { x, y, w, h, angle }, () => {
|
||||
drawBox(ctx, viewElem, {
|
||||
|
|
@ -21,8 +21,11 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
|
|||
...detailConfig,
|
||||
...elem.detail
|
||||
};
|
||||
const fontSize = (detail.fontSize || detailConfig.fontSize) * viewScaleInfo.scale;
|
||||
const lineHeight = detail.lineHeight ? detail.lineHeight * viewScaleInfo.scale : fontSize;
|
||||
const originFontSize = detail.fontSize || detailConfig.fontSize;
|
||||
const fontSize = originFontSize * viewScaleInfo.scale;
|
||||
|
||||
const originLineHeight = detail.lineHeight || originFontSize;
|
||||
const lineHeight = originLineHeight * viewScaleInfo.scale;
|
||||
|
||||
ctx.fillStyle = elem.detail.color || detailConfig.color;
|
||||
ctx.textBaseline = 'top';
|
||||
|
|
@ -42,7 +45,7 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
|
|||
|
||||
if (tempText.length > 0) {
|
||||
for (let i = 0; i < tempText.length; i++) {
|
||||
if (ctx.measureText(lineText + (tempText[i] || '')).width < ctx.$doPixelRatio(w)) {
|
||||
if (ctx.measureText(lineText + (tempText[i] || '')).width <= ctx.$doPixelRatio(w)) {
|
||||
lineText += tempText[i] || '';
|
||||
} else {
|
||||
lines.push({
|
||||
|
|
@ -56,7 +59,7 @@ export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: Render
|
|||
break;
|
||||
}
|
||||
if (tempText.length - 1 === i) {
|
||||
if ((lineNum + 1) * fontHeight < h) {
|
||||
if ((lineNum + 1) * fontHeight <= h) {
|
||||
lines.push({
|
||||
text: lineText,
|
||||
width: ctx.$undoPixelRatio(ctx.measureText(lineText).width)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ElementBaseDetail, ElementTextDetail, ElementGroupDetail } from './element';
|
||||
|
||||
export type DefaultElementDetailConfig = Required<Omit<ElementBaseDetail, 'clipPath' | 'background'>> &
|
||||
Required<Pick<ElementTextDetail, 'color' | 'textAlign' | 'verticalAlign' | 'fontSize' | 'fontFamily' | 'fontWeight' | 'lineHeight'>> &
|
||||
Required<Pick<ElementTextDetail, 'color' | 'textAlign' | 'verticalAlign' | 'fontSize' | 'fontFamily' | 'fontWeight'>> &
|
||||
Required<Pick<ElementGroupDetail, 'overflow'>>;
|
||||
|
|
|
|||
|
|
@ -38,3 +38,5 @@ export interface ElementSizeController {
|
|||
rightMiddle: ElementSizeControllerItem;
|
||||
rotate: ElementSizeControllerItem;
|
||||
}
|
||||
|
||||
export type LayoutSizeController = Omit<ElementSizeController, 'rotate' | 'elementWrapper'>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
import type { Element, ElementType, ElementAssets, ElementSize, ElementGroupDetail } from './element';
|
||||
|
||||
export type DataLayout = Pick<ElementSize, 'w' | 'h'> & {
|
||||
detail: Omit<ElementGroupDetail, 'children'>;
|
||||
export type DataLayout = Pick<ElementSize, 'x' | 'y' | 'w' | 'h'> & {
|
||||
detail: Pick<
|
||||
ElementGroupDetail,
|
||||
'background' | 'borderWidth' | 'overflow' | 'borderColor' | 'borderDash' | 'borderRadius' | 'shadowBlur' | 'shadowColor' | 'shadowOffsetX' | 'shadowOffsetY'
|
||||
>;
|
||||
operations?: {
|
||||
disableLeft?: boolean;
|
||||
disableTop?: boolean;
|
||||
disableRight?: boolean;
|
||||
disableBottom?: boolean;
|
||||
disableTopLeft?: boolean;
|
||||
disableTopRight?: boolean;
|
||||
disableBottomLeft?: boolean;
|
||||
disableBottomRight?: boolean;
|
||||
};
|
||||
};
|
||||
export interface Data<E extends Record<string, any> = Record<string, any>> {
|
||||
elements: Element<ElementType, E>[];
|
||||
|
|
|
|||
|
|
@ -59,12 +59,14 @@ export {
|
|||
calcElementViewRectInfo,
|
||||
calcElementOriginRectInfo,
|
||||
calcElementViewRectInfoMap,
|
||||
originRectInfoToRangeRectInfo
|
||||
originRectInfoToRangeRectInfo,
|
||||
isViewPointInElementSize,
|
||||
isViewPointInVertexes
|
||||
} from './lib/view-calc';
|
||||
export { sortElementsViewVisiableInfoMap, calcVisibleOriginCanvasRectInfo, updateViewVisibleInfoMapStatus } from './lib/view-visible';
|
||||
export { rotatePoint, rotateVertexes, rotateByCenter } from './lib/rotate';
|
||||
export { getElementVertexes, calcElementVertexesInGroup, calcElementVertexesQueueInGroup, calcElementQueueVertexesQueueInGroup } from './lib/vertex';
|
||||
export { calcElementSizeController } from './lib/controller';
|
||||
export { calcElementSizeController, calcLayoutSizeController } from './lib/controller';
|
||||
export { generateSVGPath, parseSVGPath } from './lib/svg-path';
|
||||
export { generateHTML, parseHTML } from './lib/html';
|
||||
export { compressImage } from './lib/image';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function getDefaultElementDetailConfig(): DefaultElementDetailConfig {
|
|||
textAlign: 'left',
|
||||
verticalAlign: 'top',
|
||||
fontSize: 16,
|
||||
lineHeight: 20,
|
||||
// lineHeight: 20,
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import type { Element, ElementSize, ElementSizeController, ViewRectVertexes, PointSize, ViewScaleInfo, LayoutSizeController } from '@idraw/types';
|
||||
import { createUUID } from './uuid';
|
||||
import { getCenterFromTwoPoints } from './point';
|
||||
import { calcElementVertexesInGroup, calcElementVertexes } from './vertex';
|
||||
import type { Element, ElementSize, ElementSizeController, ViewRectVertexes, PointSize, ViewScaleInfo } from '@idraw/types';
|
||||
import { calcViewElementSize } from './view-calc';
|
||||
import { calcElementCenter } from './rotate';
|
||||
|
||||
function createControllerElementSizeFromCenter(center: PointSize, opts: { size: number; angle: number }) {
|
||||
const { x, y } = center;
|
||||
|
|
@ -185,3 +187,117 @@ export function calcElementSizeController(
|
|||
};
|
||||
return sizeController;
|
||||
}
|
||||
|
||||
export function calcLayoutSizeController(
|
||||
layoutSize: Pick<ElementSize, 'x' | 'y' | 'w' | 'h'>,
|
||||
opts: {
|
||||
controllerSize?: number;
|
||||
viewScaleInfo: ViewScaleInfo;
|
||||
}
|
||||
): LayoutSizeController {
|
||||
const { controllerSize, viewScaleInfo } = opts;
|
||||
|
||||
const ctrlSize = controllerSize && controllerSize > 0 ? controllerSize : 8;
|
||||
|
||||
const { x, y, w, h } = calcViewElementSize(layoutSize, { viewScaleInfo });
|
||||
const center = calcElementCenter({ x, y, w, h });
|
||||
|
||||
const topCenter = { x: center.x, y };
|
||||
const rightCenter = { x: x + w, y: center.y };
|
||||
const bottomCenter = { x: center.x, y: y + h };
|
||||
const leftCenter = { x, y: center.y };
|
||||
|
||||
const topLeftCenter = { x, y };
|
||||
const topRightCenter = { x: x + w, y };
|
||||
const bottomRightCenter = { x: x + w, y: y + h };
|
||||
const bottomLeftCenter = { x, y: y + h };
|
||||
|
||||
const topMiddleSize = createControllerElementSizeFromCenter(topCenter, { size: ctrlSize, angle: 0 });
|
||||
const rightMiddleSize = createControllerElementSizeFromCenter(rightCenter, { size: ctrlSize, angle: 0 });
|
||||
const bottomMiddleSize = createControllerElementSizeFromCenter(bottomCenter, { size: ctrlSize, angle: 0 });
|
||||
const leftMiddleSize = createControllerElementSizeFromCenter(leftCenter, { size: ctrlSize, angle: 0 });
|
||||
|
||||
const topLeftSize = createControllerElementSizeFromCenter(topLeftCenter, { size: ctrlSize, angle: 0 });
|
||||
const topRightSize = createControllerElementSizeFromCenter(topRightCenter, { size: ctrlSize, angle: 0 });
|
||||
const bottomLeftSize = createControllerElementSizeFromCenter(bottomLeftCenter, { size: ctrlSize, angle: 0 });
|
||||
const bottomRightSize = createControllerElementSizeFromCenter(bottomRightCenter, { size: ctrlSize, angle: 0 });
|
||||
|
||||
const topLeftVertexes = calcElementVertexes(topLeftSize);
|
||||
const topRightVertexes = calcElementVertexes(topRightSize);
|
||||
const bottomLeftVertexes = calcElementVertexes(bottomLeftSize);
|
||||
const bottomRightVertexes = calcElementVertexes(bottomRightSize);
|
||||
|
||||
const topVertexes: ViewRectVertexes = [topLeftVertexes[1], topRightVertexes[0], topRightVertexes[3], topLeftVertexes[2]];
|
||||
const rightVertexes: ViewRectVertexes = [topRightVertexes[3], topRightVertexes[2], bottomRightVertexes[1], bottomRightVertexes[0]];
|
||||
const bottomVertexes: ViewRectVertexes = [bottomLeftVertexes[1], bottomRightVertexes[0], bottomRightVertexes[3], bottomLeftVertexes[2]];
|
||||
const leftVertexes: ViewRectVertexes = [topLeftVertexes[3], topLeftVertexes[2], bottomLeftVertexes[1], bottomLeftVertexes[0]];
|
||||
|
||||
const topMiddleVertexes = calcElementVertexes(topMiddleSize);
|
||||
const rightMiddleVertexes = calcElementVertexes(rightMiddleSize);
|
||||
const bottomMiddleVertexes = calcElementVertexes(bottomMiddleSize);
|
||||
const leftMiddleVertexes = calcElementVertexes(leftMiddleSize);
|
||||
|
||||
const sizeController: LayoutSizeController = {
|
||||
left: {
|
||||
type: 'left',
|
||||
vertexes: leftVertexes,
|
||||
center: leftCenter
|
||||
},
|
||||
right: {
|
||||
type: 'right',
|
||||
vertexes: rightVertexes,
|
||||
center: rightCenter
|
||||
},
|
||||
top: {
|
||||
type: 'top',
|
||||
vertexes: topVertexes,
|
||||
center: topCenter
|
||||
},
|
||||
bottom: {
|
||||
type: 'bottom',
|
||||
vertexes: bottomVertexes,
|
||||
center: bottomCenter
|
||||
},
|
||||
topLeft: {
|
||||
type: 'top-left',
|
||||
vertexes: topLeftVertexes,
|
||||
center: topLeftCenter
|
||||
},
|
||||
topRight: {
|
||||
type: 'top-right',
|
||||
vertexes: topRightVertexes,
|
||||
center: topRightCenter
|
||||
},
|
||||
bottomLeft: {
|
||||
type: 'bottom-left',
|
||||
vertexes: bottomLeftVertexes,
|
||||
center: bottomLeftCenter
|
||||
},
|
||||
bottomRight: {
|
||||
type: 'bottom-right',
|
||||
vertexes: bottomRightVertexes,
|
||||
center: bottomRightCenter
|
||||
},
|
||||
leftMiddle: {
|
||||
type: 'left-middle',
|
||||
vertexes: leftMiddleVertexes,
|
||||
center: leftCenter
|
||||
},
|
||||
rightMiddle: {
|
||||
type: 'right-middle',
|
||||
vertexes: rightMiddleVertexes,
|
||||
center: rightCenter
|
||||
},
|
||||
topMiddle: {
|
||||
type: 'top-middle',
|
||||
vertexes: topMiddleVertexes,
|
||||
center: topCenter
|
||||
},
|
||||
bottomMiddle: {
|
||||
type: 'bottom-middle',
|
||||
vertexes: bottomMiddleVertexes,
|
||||
center: bottomCenter
|
||||
}
|
||||
};
|
||||
return sizeController;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@idraw/types';
|
||||
import { rotateElementVertexes } from './rotate';
|
||||
import { checkRectIntersect } from './rect';
|
||||
import { calcElementVertexesInGroup } from './vertex';
|
||||
import { calcElementVertexesInGroup, calcElementVertexes } from './vertex';
|
||||
import { getCenterFromTwoPoints } from './point';
|
||||
|
||||
export function calcViewScaleInfo(info: { scale: number; offsetX: number; offsetY: number }, opts: { viewSizeInfo: ViewSizeInfo }): ViewScaleInfo {
|
||||
|
|
@ -83,7 +83,7 @@ export function viewScroll(opts: { moveX?: number; moveY?: number; viewScaleInfo
|
|||
};
|
||||
}
|
||||
|
||||
export function calcViewElementSize(size: ElementSize, opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): ElementSize {
|
||||
export function calcViewElementSize(size: ElementSize, opts: { viewScaleInfo: ViewScaleInfo }): ElementSize {
|
||||
const { viewScaleInfo } = opts;
|
||||
const { x, y, w, h, angle } = size;
|
||||
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
|
||||
|
|
@ -126,10 +126,10 @@ export function isViewPointInElement(
|
|||
p: Point,
|
||||
opts: { context2d: ViewContext2D; element: ElementSize; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
|
||||
): boolean {
|
||||
const { context2d: ctx, element: elem, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { context2d: ctx, element: elem, viewScaleInfo } = opts;
|
||||
|
||||
const { angle = 0 } = elem;
|
||||
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo });
|
||||
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo });
|
||||
const vertexes = rotateElementVertexes({ x, y, w, h, angle });
|
||||
if (vertexes.length >= 2) {
|
||||
ctx.beginPath();
|
||||
|
|
@ -145,6 +145,40 @@ export function isViewPointInElement(
|
|||
return false;
|
||||
}
|
||||
|
||||
export function isViewPointInElementSize(
|
||||
p: Point,
|
||||
elemSize: ElementSize,
|
||||
opts?: {
|
||||
includeBorder?: boolean;
|
||||
}
|
||||
): boolean {
|
||||
const vertexes = calcElementVertexes(elemSize);
|
||||
return isViewPointInVertexes(p, vertexes, opts);
|
||||
}
|
||||
|
||||
export function isViewPointInVertexes(
|
||||
p: Point,
|
||||
vertexes: ViewRectVertexes,
|
||||
opts?: {
|
||||
includeBorder?: boolean;
|
||||
}
|
||||
): boolean {
|
||||
const xList = [vertexes[0].x, vertexes[1].x, vertexes[2].x, vertexes[3].x];
|
||||
const yList = [vertexes[0].y, vertexes[1].y, vertexes[2].y, vertexes[3].y];
|
||||
const mixX = Math.min(...xList);
|
||||
const maxX = Math.max(...xList);
|
||||
const mixY = Math.min(...yList);
|
||||
const maxY = Math.max(...yList);
|
||||
|
||||
if (p.x > mixX && p.x < maxX && p.y > mixY && p.y < maxY) {
|
||||
return true;
|
||||
}
|
||||
if (opts?.includeBorder === true && (p.x === mixX || p.x === maxX || p.y === mixY || p.y === maxY)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getViewPointAtElement(
|
||||
p: Point,
|
||||
opts: {
|
||||
|
|
@ -234,7 +268,7 @@ export function isElementInView(elem: ElementSize, opts: { viewScaleInfo: ViewSc
|
|||
const { viewSizeInfo, viewScaleInfo } = opts;
|
||||
const { width, height } = viewSizeInfo;
|
||||
const { angle } = elem;
|
||||
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo });
|
||||
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo });
|
||||
const ves = rotateElementVertexes({ x, y, w, h, angle });
|
||||
const viewSize = { x: 0, y: 0, w: width, h: height };
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Data, ViewSizeInfo, Element, ElementSize, ViewScaleInfo, PointSize
|
|||
import { rotateElementVertexes } from './rotate';
|
||||
import {} from './view-calc';
|
||||
import { formatNumber } from './number';
|
||||
import { is } from './is';
|
||||
|
||||
interface ViewCenterContentResult {
|
||||
offsetX: number;
|
||||
|
|
@ -58,6 +59,16 @@ export function calcViewCenterContent(data: Data, opts: { viewSizeInfo: ViewSize
|
|||
});
|
||||
}
|
||||
|
||||
if (data.layout) {
|
||||
const { x, y, w, h } = data.layout;
|
||||
if (is.x(x) && is.y(y) && is.w(w) && is.h(h)) {
|
||||
contentX = Math.min(contentX, x);
|
||||
contentY = Math.min(contentY, y);
|
||||
contentW = Math.max(contentW, w);
|
||||
contentH = Math.max(contentH, h);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentW > 0 && contentH > 0) {
|
||||
const scaleW = formatNumber(width / contentW, { decimalPlaces: 4 });
|
||||
const scaleH = formatNumber(height / contentH, { decimalPlaces: 4 });
|
||||
|
|
|
|||
Loading…
Reference in a new issue