diff --git a/.eslintrc.js b/.eslintrc.js index f555e9d..9f520e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,28 +1,33 @@ module.exports = { - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "extends": ["plugin:@typescript-eslint/recommended"], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "semi": "error", - "indent": ["error", 2, { - "SwitchCase": 1, - "VariableDeclarator": 1, - "outerIIFEBody": 1, - "MemberExpression": 1, - "FunctionDeclaration": { "parameters": 1, "body": 1 }, - "FunctionExpression": { "parameters": 1, "body": 1 }, - "CallExpression": { "arguments": 1 }, - "ArrayExpression": 1, - "ObjectExpression": 1, - "ImportDeclaration": 1, - "flatTernaryExpressions": false, - "ignoreComments": false - }], - "@typescript-eslint/rule-name": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-module-boundary-types": "off" + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['plugin:@typescript-eslint/recommended'], + parserOptions: { + sourceType: 'module' }, + rules: { + semi: 'error', + indent: [ + 'error', + 2, + { + SwitchCase: 1, + VariableDeclarator: 1, + outerIIFEBody: 1, + MemberExpression: 1, + FunctionDeclaration: { parameters: 1, body: 1 }, + FunctionExpression: { parameters: 1, body: 1 }, + CallExpression: { arguments: 1 }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoreComments: false + } + ], + 'no-console': 1, + '@typescript-eslint/rule-name': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off' + } }; diff --git a/packages/board/src/index.ts b/packages/board/src/index.ts index 44bc919..323b751 100644 --- a/packages/board/src/index.ts +++ b/packages/board/src/index.ts @@ -71,6 +71,7 @@ export class Board { this._handleHover(e); }, frameTime) ); + this._watcher.on( 'wheelX', throttle((e) => { @@ -87,6 +88,7 @@ export class Board { this._watcher.on('scrollX', this._handleScrollX.bind(this)); this._watcher.on('scrollY', this._handleScrollY.bind(this)); this._watcher.on('resize', this._handleResize.bind(this)); + this._watcher.on('doubleClick', this._handleDoubleClick.bind(this)); } private _handlePointStart(e: BoardWatcherEventMap['pointStart']) { @@ -129,6 +131,16 @@ export class Board { } } + private _handleDoubleClick(e: BoardWatcherEventMap['doubleClick']) { + for (let i = 0; i < this._activeMiddlewareObjs.length; i++) { + const obj = this._activeMiddlewareObjs[i]; + const result = obj?.doubleClick?.(e); + if (result === false) { + return; + } + } + } + private _handleWheelX(e: BoardWatcherEventMap['wheelX']) { for (let i = 0; i < this._activeMiddlewareObjs.length; i++) { const obj = this._activeMiddlewareObjs[i]; diff --git a/packages/board/src/lib/calculator.ts b/packages/board/src/lib/calculator.ts index 3652bad..8b34fa1 100644 --- a/packages/board/src/lib/calculator.ts +++ b/packages/board/src/lib/calculator.ts @@ -11,7 +11,7 @@ export class Calculator implements ViewCalculator { viewScale(num: number, prevScaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeInfo): ViewScaleInfo { const scale = num; - const { width, height, contextX, contextY, contextWidth, contextHeight } = viewSizeInfo; + const { width, height, contextWidth, contextHeight } = viewSizeInfo; let offsetLeft = 0; let offsetRight = 0; let offsetTop = 0; @@ -47,7 +47,7 @@ export class Calculator implements ViewCalculator { viewScroll(opts: { moveX?: number; moveY?: number }, scaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeInfo): ViewScaleInfo { const scale = scaleInfo.scale; const { moveX, moveY } = opts; - const { width, height, contextWidth, contextHeight, contextX, contextY } = viewSizeInfo; + const { width, height, contextWidth, contextHeight } = viewSizeInfo; let offsetLeft = scaleInfo.offsetLeft; let offsetRight = scaleInfo.offsetRight; let offsetTop = scaleInfo.offsetTop; @@ -103,7 +103,8 @@ export class Calculator implements ViewCalculator { const { x, y, w, h, angle } = size; const { contextX = 0, contextY = 0 } = viewSizeInfo; const { scale, offsetTop, offsetLeft } = scaleInfo; - return { + + const newSize = { x: x * scale + offsetLeft - contextX, y: y * scale + offsetTop - contextY, w: w * scale, @@ -111,6 +112,8 @@ export class Calculator implements ViewCalculator { angle }; + return newSize; + // const { x, y, w, h, angle } = size; // const { scale, offsetTop, offsetLeft } = scaleInfo; // return { diff --git a/packages/board/src/lib/watcher.ts b/packages/board/src/lib/watcher.ts index 1d6fb2c..f9aecd8 100644 --- a/packages/board/src/lib/watcher.ts +++ b/packages/board/src/lib/watcher.ts @@ -10,7 +10,7 @@ export class BoardWatcher extends EventEmitter { private _store: Store; constructor(opts: BoardWatcherOptions) { super(); - const store = new Store({ defaultStorage: { hasPointDown: true } }); + const store = new Store({ defaultStorage: { hasPointDown: true, prevClickPoint: null } }); this._store = store; this._opts = opts; this._init(); @@ -97,6 +97,25 @@ export class BoardWatcher extends EventEmitter { }, { passive: false } ); + container.addEventListener('click', (e: MouseEvent) => { + if (!this._isInTarget(e)) { + return; + } + e.preventDefault(); + this._store.set('hasPointDown', true); + const point = this._getPoint(e); + if (!this._isVaildPoint(point)) { + return; + } + const maxLimitTime = 500; + const t = Date.now(); + const preClickPoint = this._store.get('prevClickPoint'); + if (preClickPoint && t - preClickPoint.t <= maxLimitTime && Math.abs(preClickPoint.x - point.x) <= 5 && Math.abs(preClickPoint.y - point.y) <= 5) { + this.trigger('doubleClick', { point }); + } else { + this._store.set('prevClickPoint', point); + } + }); } private _isInTarget(e: MouseEvent | WheelEvent) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f50e851..12e8ea3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,8 @@ import type { Data, CoreOptions, BoardMiddleware, ViewSizeInfo } from '@idraw/ty import { Board } from '@idraw/board'; import { createBoardContexts, validateElements, calcElementsContextSize } from '@idraw/util'; -export { MiddlewareSelector } from './middleware/selector'; +// export { MiddlewareSelector } from './middleware/selector'; +export { MiddlewareSelector } from './middleware/deep-selector'; export { MiddlewareScroller } from './middleware/scroller'; export { MiddlewareRuler } from './middleware/rule'; @@ -13,6 +14,7 @@ export class Core { private _canvas: HTMLCanvasElement; constructor(mount: HTMLDivElement, opts: CoreOptions) { const { devicePixelRatio = 1, width, height } = opts; + this._opts = opts; this._mount = mount; const canvas = document.createElement('canvas'); @@ -45,13 +47,12 @@ export class Core { this._board.setData(data); const sharer = this._board.getSharer(); const currentViewSize = sharer.getActiveViewSizeInfo(); - const currentScaleInfo = sharer.getActiveScaleInfo(); - + // const currentScaleInfo = sharer.getActiveScaleInfo(); const newViewContextSize = calcElementsContextSize(data.elements, { viewWidth: currentViewSize.width, - viewHeight: currentViewSize.height + viewHeight: currentViewSize.height, + extend: true }); - this.resize({ ...currentViewSize, ...newViewContextSize diff --git a/packages/core/src/middleware/deep-selector/config.ts b/packages/core/src/middleware/deep-selector/config.ts new file mode 100644 index 0000000..4afee2c --- /dev/null +++ b/packages/core/src/middleware/deep-selector/config.ts @@ -0,0 +1,14 @@ +export const key = 'SELECT'; +export const keyHoverElementSize = Symbol(`${key}_hoverElementSize`); +export const keyActionType = Symbol(`${key}_actionType`); // 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null = null; +export const keyResizeType = Symbol(`${key}_resizeType`); // ResizeType | null; +export const keyAreaStart = Symbol(`${key}_areaStart`); // Point +export const keyAreaEnd = Symbol(`${key}_areaEnd`); // Point +export const keyInGroupQueue = Symbol(`${key}_targetQueue`); // Element<'group'>[] + +// export const keyHoverElementSize = `${key}_hoverElementSize`; +// export const keyActionType = `${key}_actionType`; // 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null = null; +// export const keyResizeType = `${key}_resizeType`; // ResizeType | null; +// export const keyAreaStart = `${key}_areaStart`; // Point +// export const keyAreaEnd = `${key}_areaEnd`; // Point +// export const keyInGroupQueue = `${key}_targetQueue`; // Element<'group'>[] diff --git a/packages/core/src/middleware/deep-selector/controller.ts b/packages/core/src/middleware/deep-selector/controller.ts new file mode 100644 index 0000000..a954be8 --- /dev/null +++ b/packages/core/src/middleware/deep-selector/controller.ts @@ -0,0 +1,88 @@ +import type { ElementSize } from '@idraw/types'; +import type { ElementSizeController } from './types'; +const wrapperColor = '#1973ba'; + +export function calcElementControllerStyle(elemSize: ElementSize): ElementSizeController { + const bw = 0; // TODO + + const ctrlSize = 8; + const ctrlBgColor = '#FFFFFF'; + const ctrlBorderWidth = 2; + const ctrlBorderColor = wrapperColor; + const { x, y, w, h } = elemSize; + const sizeControllers: ElementSizeController = { + // topLeft: { + // x: x - bw - ctrlSize / 2, + // y: y - bw - ctrlSize / 2, + // w: ctrlSize, + // h: ctrlSize, + // borderWidth: ctrlBorderWidth, + // borderColor: ctrlBorderColor, + // bgColor: ctrlBgColor + // }, + top: { + x: x - bw + w / 2 - ctrlSize / 2, + y: y - bw - ctrlSize / 2, + w: ctrlSize, + h: ctrlSize, + borderWidth: ctrlBorderWidth, + borderColor: ctrlBorderColor, + bgColor: ctrlBgColor + }, + // topRight: { + // x: x + w - bw - ctrlSize / 2, + // y: y - bw - ctrlSize / 2, + // w: ctrlSize, + // h: ctrlSize, + // borderWidth: ctrlBorderWidth, + // borderColor: ctrlBorderColor, + // bgColor: ctrlBgColor + // }, + right: { + x: x + w - bw - ctrlSize / 2, + y: y + h / 2 - bw - ctrlSize / 2, + w: ctrlSize, + h: ctrlSize, + borderWidth: ctrlBorderWidth, + borderColor: ctrlBorderColor, + bgColor: ctrlBgColor + }, + // bottomRight: { + // x: x + w - bw - ctrlSize / 2, + // y: y + h - bw - ctrlSize / 2, + // w: ctrlSize, + // h: ctrlSize, + // borderWidth: ctrlBorderWidth, + // borderColor: ctrlBorderColor, + // bgColor: ctrlBgColor + // }, + bottom: { + x: x + w / 2 - bw - ctrlSize / 2, + y: y + h - bw - ctrlSize / 2, + w: ctrlSize, + h: ctrlSize, + borderWidth: ctrlBorderWidth, + borderColor: ctrlBorderColor, + bgColor: ctrlBgColor + }, + // bottomLeft: { + // x: x - bw - ctrlSize / 2, + // y: y + h - bw - ctrlSize / 2, + // w: ctrlSize, + // h: ctrlSize, + // borderWidth: ctrlBorderWidth, + // borderColor: ctrlBorderColor, + // bgColor: ctrlBgColor + // }, + left: { + x: x - bw - ctrlSize / 2, + y: y + h / 2 - bw - ctrlSize / 2, + w: ctrlSize, + h: ctrlSize, + borderWidth: ctrlBorderWidth, + borderColor: ctrlBorderColor, + bgColor: ctrlBgColor + } + }; + return sizeControllers; +} diff --git a/packages/core/src/middleware/deep-selector/draw-wrapper.ts b/packages/core/src/middleware/deep-selector/draw-wrapper.ts new file mode 100644 index 0000000..4b6de18 --- /dev/null +++ b/packages/core/src/middleware/deep-selector/draw-wrapper.ts @@ -0,0 +1,189 @@ +import type { Element, ElementSize, ElementType, PointSize, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; +import { rotateElement, rotateElementVertexes } from '@idraw/util'; +// import { calcElementControllerStyle } from './controller'; +import type { AreaSize, ControllerStyle, ElementSizeController } from './types'; + +const wrapperColor = '#1973ba'; + +export function drawPointWrapper(ctx: ViewContext2D, elem: ElementSize) { + const bw = 0; + const { x, y, w, h } = elem; + const { angle = 0 } = elem; + + rotateElement(ctx, { x, y, w, h, angle }, () => { + ctx.setLineDash([]); + ctx.lineWidth = 1; + ctx.strokeStyle = wrapperColor; + + ctx.beginPath(); + ctx.moveTo(x - bw, y - bw); + ctx.lineTo(x + w + bw, y - bw); + ctx.lineTo(x + w + bw, y + h + bw); + ctx.lineTo(x - bw, y + h + bw); + ctx.lineTo(x - bw, y - bw); + ctx.closePath(); + ctx.stroke(); + }); +} + +export function drawHoverWrapper(ctx: ViewContext2D, elem: ElementSize) { + const bw = 0; + const { x, y, w, h } = elem; + const { angle = 0 } = elem; + rotateElement(ctx, { x, y, w, h, angle }, () => { + // ctx.setLineDash([4, 4]); + ctx.setLineDash([]); + ctx.lineWidth = 1; + ctx.strokeStyle = wrapperColor; + ctx.beginPath(); + ctx.moveTo(x - bw, y - bw); + ctx.lineTo(x + w + bw, y - bw); + ctx.lineTo(x + w + bw, y + h + bw); + ctx.lineTo(x - bw, y + h + bw); + ctx.lineTo(x - bw, y - bw); + ctx.closePath(); + ctx.stroke(); + }); +} + +function drawController(ctx: ViewContext2D, style: ControllerStyle) { + const { x, y, w, h, borderColor, borderWidth, bgColor } = style; + + ctx.setLineDash([]); + ctx.lineWidth = borderWidth; + ctx.strokeStyle = borderColor; + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + w, y); + ctx.lineTo(x + w, y + h); + ctx.lineTo(x, y + h); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); +} + +export function drawElementControllers( + ctx: ViewContext2D, + elem: ElementSize, + opts: Omit & { sizeControllers: ElementSizeController } +) { + const bw = 0; + const { x, y, w, h } = elem; + const { angle = 0 } = elem; + const { sizeControllers } = opts; + + rotateElement(ctx, { x, y, w, h, angle }, () => { + ctx.setLineDash([]); + ctx.lineWidth = 2; + ctx.strokeStyle = wrapperColor; + + ctx.beginPath(); + ctx.moveTo(x - bw, y - bw); + ctx.lineTo(x + w + bw, y - bw); + ctx.lineTo(x + w + bw, y + h + bw); + ctx.lineTo(x - bw, y + h + bw); + ctx.lineTo(x - bw, y - bw); + ctx.closePath(); + ctx.stroke(); + + Object.keys(sizeControllers).forEach((name: string) => { + const ctrl = sizeControllers[name]; + drawController(ctx, { ...ctrl, ...{} }); + }); + }); +} + +export function drawElementListShadows(ctx: ViewContext2D, elements: Element[], opts?: Omit) { + elements.forEach((elem) => { + let { x, y, w, h } = elem; + const { angle = 0 } = elem; + if (opts?.calculator) { + const { calculator } = opts; + const size = calculator.elementSize({ x, y, w, h }, opts.scaleInfo, opts.viewSize); + x = size.x; + y = size.y; + w = size.w; + h = size.h; + } + const vertexes = rotateElementVertexes({ x, y, w, h, angle }); + if (vertexes.length >= 2) { + ctx.setLineDash([]); + ctx.lineWidth = 1; + ctx.strokeStyle = '#aaaaaa'; + ctx.fillStyle = '#0000001A'; + ctx.beginPath(); + ctx.moveTo(vertexes[0].x, vertexes[0].y); + for (let i = 0; i < vertexes.length; i++) { + const p = vertexes[i]; + ctx.lineTo(p.x, p.y); + } + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + } + }); +} + +export function drawArea(ctx: ViewContext2D, opts: { start: PointSize; end: PointSize }) { + const { start, end } = opts; + ctx.setLineDash([]); + ctx.lineWidth = 1; + ctx.strokeStyle = '#1976d2'; + ctx.fillStyle = '#1976d24f'; + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.lineTo(start.x, end.y); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); +} + +export function drawListArea(ctx: ViewContext2D, opts: { areaSize: AreaSize }) { + const { areaSize } = opts; + const { x, y, w, h } = areaSize; + ctx.setLineDash([]); + ctx.lineWidth = 1; + ctx.strokeStyle = '#1976d2'; + ctx.fillStyle = '#1976d21c'; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + w, y); + ctx.lineTo(x + w, y + h); + ctx.lineTo(x, y + h); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); +} + +export function drawGroupsWrapper(ctx: ViewContext2D, elemList: ElementSize[]) { + let totalX = 0; + let totalY = 0; + let totalAngle = 0; + + for (let i = 0; i < elemList.length; i++) { + const elem = elemList[i]; + const bw = 0; + const { x, y, w, h, angle = 0 } = elem; + totalX += x; + totalY += y; + totalAngle += angle; + + rotateElement(ctx, { x: totalX, y: totalY, w, h, angle: totalAngle }, () => { + ctx.setLineDash([4, 4]); + ctx.lineWidth = 2; + ctx.strokeStyle = wrapperColor; + ctx.beginPath(); + ctx.moveTo(x - bw, y - bw); + ctx.lineTo(x + w + bw, y - bw); + ctx.lineTo(x + w + bw, y + h + bw); + ctx.lineTo(x - bw, y + h + bw); + ctx.lineTo(x - bw, y - bw); + ctx.closePath(); + ctx.stroke(); + }); + } +} diff --git a/packages/core/src/middleware/deep-selector/index.ts b/packages/core/src/middleware/deep-selector/index.ts new file mode 100644 index 0000000..92e95a0 --- /dev/null +++ b/packages/core/src/middleware/deep-selector/index.ts @@ -0,0 +1,379 @@ +import { getSelectedElementIndexes, getSelectedElements, calcElementsViewInfo } from '@idraw/util'; +import type { Point, PointWatcherEvent, BoardMiddleware, Element, ElementSize, ActionType, ResizeType, DeepSelectorSharedStorage } from './types'; +import { drawPointWrapper, drawHoverWrapper, drawElementControllers, drawArea, drawListArea, drawGroupsWrapper } from './draw-wrapper'; +import { calcElementControllerStyle } from './controller'; +import { getPointTarget, resizeElement, getSelectedListArea, calcSelectedElementsArea, isElementInGroup } from './util'; +import { key, keyHoverElementSize, keyActionType, keyResizeType, keyAreaStart, keyAreaEnd, keyInGroupQueue } from './config'; + +export const MiddlewareSelector: BoardMiddleware = (opts) => { + const { viewer, sharer, viewContent, calculator } = opts; + const { helperContext } = viewContent; + let prevPoint: Point | null = null; + + sharer.setSharedStorage(keyActionType, null); + + const getIndexes = () => { + const data = sharer.getActiveStorage('data'); + if (data) { + const uuids = sharer.getActiveStorage('selectedUUIDs'); + const idxes: Array = getSelectedElementIndexes(data, uuids); + return idxes; + } else { + return []; + } + }; + + const getActiveElements = () => { + const data = sharer.getActiveStorage('data'); + if (data) { + const uuids = sharer.getActiveStorage('selectedUUIDs'); + const elems = getSelectedElements(data, uuids); + return elems; + } else { + return []; + } + }; + + const pushGroupQueue = (elem: Element<'group'>) => { + let groupQueue = sharer.getSharedStorage(keyInGroupQueue); + if (!Array.isArray(groupQueue)) { + groupQueue = []; + } + if (groupQueue.length > 0) { + if (isElementInGroup(elem, groupQueue[groupQueue.length - 1])) { + groupQueue.push(elem); + } else { + groupQueue = []; + } + } else if (groupQueue.length === 0) { + groupQueue.push(elem); + } + sharer.setSharedStorage(keyInGroupQueue, groupQueue); + return groupQueue.length > 0; + }; + + const clear = () => { + sharer.setSharedStorage(keyActionType, null); + sharer.setSharedStorage(keyHoverElementSize, null); + sharer.setSharedStorage(keyResizeType, null); + sharer.setSharedStorage(keyAreaStart, null); + sharer.setSharedStorage(keyAreaEnd, null); + sharer.setSharedStorage(keyInGroupQueue, null); + }; + + clear(); + + return { + mode: key, + hover: (e: PointWatcherEvent) => { + const data = sharer.getActiveStorage('data'); + const resizeType = sharer.getSharedStorage(keyResizeType); + const actionType = sharer.getSharedStorage(keyActionType); + if (resizeType || (['area', 'drag', 'drag-list'] as ActionType[]).includes(actionType)) { + sharer.setSharedStorage(keyHoverElementSize, null); + return; + } + + if (actionType === 'drag') { + sharer.setSharedStorage(keyHoverElementSize, null); + } else if (data) { + const selectedElements = getActiveElements(); + const scaleInfo = sharer.getActiveScaleInfo(); + const viewSize = sharer.getActiveViewSizeInfo(); + const target = getPointTarget(e.point, { + ctx: helperContext, + data, + selectedIndexes: getIndexes(), + selectedUUIDs: sharer.getActiveStorage('selectedUUIDs') || [], + selectedElements: selectedElements, + scaleInfo, + viewSize, + calculator, + areaSize: calcSelectedElementsArea(selectedElements, { + scaleInfo, + viewSize, + calculator + }), + groupQueue: [] // TODO + }); + if (target.type === 'over-element' && target?.elements?.length === 1) { + const { x, y, w, h, angle } = target.elements[0]; + sharer.setSharedStorage(keyHoverElementSize, { x, y, w, h, angle }); + viewer.drawFrame(); + return; + } + if (sharer.getSharedStorage(keyHoverElementSize)) { + sharer.setSharedStorage(keyHoverElementSize, null); + viewer.drawFrame(); + return; + } + } + }, + + pointStart: (e: PointWatcherEvent) => { + // reset all shared storage + // clear(); + sharer.setSharedStorage(keyHoverElementSize, null); + const data = sharer.getActiveStorage('data'); + const listAreaSize = calcSelectedElementsArea(getActiveElements(), { + scaleInfo: sharer.getActiveScaleInfo(), + viewSize: sharer.getActiveViewSizeInfo(), + calculator + }); + const target = getPointTarget(e.point, { + ctx: helperContext, + data, + selectedIndexes: getIndexes(), + selectedUUIDs: sharer.getActiveStorage('selectedUUIDs') || [], + selectedElements: getActiveElements(), + scaleInfo: sharer.getActiveScaleInfo(), + viewSize: sharer.getActiveViewSizeInfo(), + calculator, + areaSize: listAreaSize, + groupQueue: [] // TODO + }); + + if (target.type === 'list-area') { + sharer.setSharedStorage(keyActionType, 'drag-list'); + } else if (target.type === 'over-element' && target?.uuids?.length === 1 && target?.elements?.length === 1) { + sharer.setActiveStorage('selectedUUIDs', target?.uuids[0] ? [target?.uuids[0]] : []); + sharer.setSharedStorage(keyActionType, 'drag'); + } else if (target.type?.startsWith('resize-')) { + sharer.setSharedStorage(keyResizeType, target.type as ResizeType); + sharer.setSharedStorage(keyActionType, 'resize'); + } else { + clear(); + sharer.setSharedStorage(keyActionType, 'area'); + sharer.setSharedStorage(keyAreaStart, e.point); + sharer.setActiveStorage('selectedUUIDs', []); + } + if (target.type) { + prevPoint = e.point; + } else { + prevPoint = null; + } + viewer.drawFrame(); + }, + + pointMove: (e: PointWatcherEvent) => { + const data = sharer.getActiveStorage('data'); + const indexes = getIndexes(); + const elems = getActiveElements(); + const scale = sharer.getActiveStorage('scale') || 1; + const start = prevPoint; + const end = e.point; + const resizeType = sharer.getSharedStorage(keyResizeType); + const actionType = sharer.getSharedStorage(keyActionType); + + if (actionType === 'drag') { + if (data && elems?.length === 1 && indexes?.length === 1 && typeof indexes[0] === 'number' && indexes[0] >= 0 && start && end) { + data.elements[indexes[0]].x += (end.x - start.x) / scale; + data.elements[indexes[0]].y += (end.y - start.y) / scale; + sharer.setActiveStorage('data', data); + prevPoint = e.point; + } else { + prevPoint = null; + } + viewer.drawFrame(); + } else if (actionType === 'drag-list') { + if (data && start && end && indexes?.length > 1) { + const moveX = (end.x - start.x) / scale; + const moveY = (end.y - start.y) / scale; + indexes.forEach((idx: number | string) => { + if (typeof idx === 'number' && data.elements[idx]) { + data.elements[idx].x += moveX; + data.elements[idx].y += moveY; + } + }); + + sharer.setActiveStorage('data', data); + prevPoint = e.point; + } else { + prevPoint = null; + } + viewer.drawFrame(); + } else if (actionType === 'resize') { + if ( + data && + elems?.length === 1 && + indexes?.length === 1 && + typeof indexes[0] === 'number' && + indexes[0] >= 0 && + start && + resizeType?.startsWith('resize-') + ) { + const resizedElemSize = resizeElement(elems[0], { scale, start, end, resizeType }); + data.elements[indexes[0]].x = resizedElemSize.x; + data.elements[indexes[0]].y = resizedElemSize.y; + data.elements[indexes[0]].w = resizedElemSize.w; + data.elements[indexes[0]].h = resizedElemSize.h; + prevPoint = e.point; + viewer.drawFrame(); + } + } else if (actionType === 'area') { + sharer.setSharedStorage(keyAreaEnd, e.point); + viewer.drawFrame(); + } + }, + + pointEnd(e: PointWatcherEvent) { + const data = sharer.getActiveStorage('data'); + const resizeType = sharer.getSharedStorage(keyResizeType); + const actionType = sharer.getSharedStorage(keyActionType); + const scaleInfo = sharer.getActiveScaleInfo(); + const viewSize = sharer.getActiveViewSizeInfo(); + const { offsetLeft, offsetTop } = scaleInfo; + let needDrawFrame = false; + + if (actionType === 'resize' && resizeType) { + sharer.setSharedStorage(keyResizeType, null); + } else if (actionType === 'area') { + sharer.setSharedStorage(keyActionType, null); + if (data) { + const start = sharer.getSharedStorage(keyAreaStart); + const end = sharer.getSharedStorage(keyAreaEnd); + if (start && end) { + const { uuids } = getSelectedListArea(data, { + start, + end, + calculator, + scaleInfo: sharer.getActiveScaleInfo(), + viewSize: sharer.getActiveViewSizeInfo() + }); + + if (uuids.length > 0) { + sharer.setActiveStorage('selectedUUIDs', uuids); + sharer.setSharedStorage(keyActionType, 'drag-list'); + needDrawFrame = true; + } + } + } + } else if (actionType === 'drag-list') { + sharer.setSharedStorage(keyActionType, 'drag-list-end'); + needDrawFrame = true; + } else if (data) { + const result = calculator.getPointElement(e.point, data, sharer.getActiveScaleInfo(), sharer.getActiveViewSizeInfo()); + if (result.element) { + sharer.setSharedStorage(keyActionType, 'select'); + needDrawFrame = true; + } else { + sharer.setSharedStorage(keyActionType, null); + } + } + if (sharer.getSharedStorage(keyActionType) === null) { + clear(); + needDrawFrame = true; + } + + const finalDrawFrame = () => { + if (!needDrawFrame) { + return; + } + if (data && Array.isArray(data?.elements) && (['drag', 'drag-list'] as ActionType[]).includes(actionType)) { + const viewInfo = calcElementsViewInfo(data.elements, viewSize, { extend: true }); + sharer.setActiveStorage('contextX', viewInfo.contextSize.contextX); + sharer.setActiveStorage('contextY', viewInfo.contextSize.contextY); + sharer.setActiveStorage('contextHeight', viewInfo.contextSize.contextHeight); + sharer.setActiveStorage('contextWidth', viewInfo.contextSize.contextWidth); + viewer.scrollX(offsetLeft + viewInfo.changeContextLeft); + viewer.scrollY(offsetTop + viewInfo.changeContextTop); + } + viewer.drawFrame(); + }; + + finalDrawFrame(); + }, + + pointLeave() { + clear(); + viewer.drawFrame(); + }, + + doubleClick(e: PointWatcherEvent) { + // console.log('doubleClick =====', e); + const groupQueue = sharer.getSharedStorage(keyInGroupQueue); + const data = sharer.getActiveStorage('data'); + const target = getPointTarget(e.point, { + ctx: helperContext, + data, + selectedIndexes: getIndexes(), + selectedUUIDs: sharer.getActiveStorage('selectedUUIDs') || [], + selectedElements: getActiveElements(), + scaleInfo: sharer.getActiveScaleInfo(), + viewSize: sharer.getActiveViewSizeInfo(), + calculator, + areaSize: null, + groupQueue: [] // TODO + }); + if (target.elements.length === 1 && target.elements[0]?.type === 'group') { + const pushResult = pushGroupQueue(target.elements[0] as Element<'group'>); + if (pushResult === true) { + viewer.drawFrame(); + return; + } + } + sharer.setSharedStorage(keyActionType, null); + console.log('doubleClick target ======= ', target); + }, + + beforeDrawFrame({ snapshot }) { + const { activeStore, sharedStore } = snapshot; + const { + data, + selectedUUIDs, + scale, + offsetLeft, + offsetTop, + offsetRight, + offsetBottom, + width, + height, + contextX, + contextY, + contextHeight, + contextWidth, + devicePixelRatio + } = activeStore; + const scaleInfo = { scale, offsetLeft, offsetTop, offsetRight, offsetBottom }; + const viewSize = { width, height, contextX, contextY, contextHeight, contextWidth, devicePixelRatio }; + // const elem = data?.elements?.[selectedIndexes?.[0] as number]; + const selectedElements = getSelectedElements(data, selectedUUIDs); + const elem = selectedElements[0]; + const hoverElement: ElementSize = sharedStore[keyHoverElementSize] as ElementSize; + const actionType: ActionType = sharedStore[keyActionType] as ActionType; + const areaStart: Point | null = sharedStore[keyAreaStart]; + const areaEnd: Point | null = sharedStore[keyAreaEnd]; + const groupQueue: Element<'group'>[] | null = sharedStore[keyInGroupQueue]; + + if (groupQueue && groupQueue?.length > 0) { + // in group + drawGroupsWrapper(helperContext, groupQueue); + } else { + // in root + const drawOpts = { calculator, scaleInfo, viewSize }; + if (hoverElement && actionType !== 'drag') { + const hoverElemSize = calculator.elementSize(hoverElement, scaleInfo, viewSize); + drawHoverWrapper(helperContext, hoverElemSize); + } + + if (elem && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) { + const selectedElemSize = calculator.elementSize(elem, scaleInfo, viewSize); + const sizeControllers = calcElementControllerStyle(selectedElemSize); + drawPointWrapper(helperContext, selectedElemSize); + drawElementControllers(helperContext, selectedElemSize, { ...drawOpts, sizeControllers }); + } else if (actionType === 'area' && areaStart && areaEnd) { + drawArea(helperContext, { start: areaStart, end: areaEnd }); + } else if ((['drag-list', 'drag-list-end'] as ActionType[]).includes(actionType)) { + const listAreaSize = calcSelectedElementsArea(getActiveElements(), { + scaleInfo: sharer.getActiveScaleInfo(), + viewSize: sharer.getActiveViewSizeInfo(), + calculator + }); + if (listAreaSize) { + drawListArea(helperContext, { areaSize: listAreaSize }); + } + } + } + } + }; +}; diff --git a/packages/core/src/middleware/deep-selector/types.ts b/packages/core/src/middleware/deep-selector/types.ts new file mode 100644 index 0000000..0aff95f --- /dev/null +++ b/packages/core/src/middleware/deep-selector/types.ts @@ -0,0 +1,63 @@ +import { keyHoverElementSize, keyActionType, keyResizeType, keyAreaStart, keyAreaEnd, keyInGroupQueue } from './config'; + +import { + Data, + ElementSize, + ElementType, + Element, + ViewContext2D, + Point, + PointSize, + ViewScaleInfo, + ViewSizeInfo, + ViewCalculator, + PointWatcherEvent, + BoardMiddleware +} from '@idraw/types'; + +export { + Data, + ElementType, + Element, + ElementSize, + ViewContext2D, + Point, + PointSize, + ViewScaleInfo, + ViewSizeInfo, + ViewCalculator, + PointWatcherEvent, + BoardMiddleware +}; + +export type ControllerStyle = ElementSize & { + borderWidth: number; + borderColor: string; + bgColor: string; +}; + +export type ElementSizeController = Record; + +export type ResizeType = 'resize-left' | 'resize-right' | 'resize-top' | 'resize-bottom'; + +export type PointTargetType = null | 'list-area' | 'over-element' | ResizeType; + +export interface PointTarget { + type: PointTargetType; + elements: Element[]; + indexes: Array; + uuids: string[]; +} + +export type AreaSize = ElementSize; + +export type ActionType = 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null; + +export type DeepSelectorSharedStorage = { + [keyHoverElementSize]: ElementSize | null; + [keyActionType]: ActionType; + [keyResizeType]: ResizeType | null; + [keyAreaStart]: Point | null; + [keyAreaEnd]: Point | null; + [keyInGroupQueue]: Element<'group'>[] | null; +}; diff --git a/packages/core/src/middleware/deep-selector/util.ts b/packages/core/src/middleware/deep-selector/util.ts new file mode 100644 index 0000000..d32b14e --- /dev/null +++ b/packages/core/src/middleware/deep-selector/util.ts @@ -0,0 +1,488 @@ +import type { + Data, + Element, + ViewContext2D, + Point, + PointSize, + PointTarget, + PointTargetType, + ViewScaleInfo, + ViewCalculator, + ElementType, + ElementSize, + ResizeType, + AreaSize, + ViewSizeInfo +} from './types'; +import { rotateElement, calcElementCenter, rotateElementVertexes } from '@idraw/util'; +import { calcElementControllerStyle } from './controller'; + +function parseRadian(angle: number) { + return (angle * Math.PI) / 180; +} + +function calcMoveDist(moveX: number, moveY: number) { + return Math.sqrt(moveX * moveX + moveY * moveY); +} + +function changeMoveDistDirect(moveDist: number, moveDirect: number) { + return moveDirect > 0 ? Math.abs(moveDist) : 0 - Math.abs(moveDist); +} + +export function getPointTarget( + p: PointSize, + opts: { + ctx: ViewContext2D; + data?: Data | null; + selectedIndexes?: Array; + selectedUUIDs: Array; + selectedElements?: Element[]; + areaSize?: AreaSize | null; + scaleInfo: ViewScaleInfo; + viewSize: ViewSizeInfo; + calculator: ViewCalculator; + groupQueue: Element<'group'>[]; + } +): PointTarget { + const target: PointTarget = { + type: null, + elements: [], + indexes: [], + uuids: [] + }; + const { ctx, data, calculator, selectedElements, selectedIndexes, selectedUUIDs, scaleInfo, viewSize, areaSize } = opts; + + // list area + if (areaSize && Array.isArray(selectedElements) && selectedElements?.length > 1 && Array.isArray(selectedIndexes) && selectedIndexes?.length > 1) { + const { x, y, w, h } = areaSize; + if (p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h) { + target.type = 'list-area'; + target.elements = selectedElements; + target.indexes = selectedIndexes; + target.uuids = selectedUUIDs; + return target; + } + } + + // resize + if (selectedElements?.length === 1) { + const elemSize = calculator.elementSize(selectedElements[0], scaleInfo, viewSize); + const ctrls = calcElementControllerStyle(elemSize); + rotateElement(ctx, elemSize, () => { + const ctrlKeys = Object.keys(ctrls); + for (let i = 0; i < ctrlKeys.length; i++) { + const key = ctrlKeys[i]; + const ctrl = ctrls[key]; + const { x, y, w, h } = ctrl; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + w, y); + ctx.lineTo(x + w, y + h); + ctx.lineTo(x, y + h); + ctx.closePath(); + if (ctx.isPointInPath(p.x, p.y)) { + target.type = `resize-${key}` as PointTargetType; + break; + } + } + }); + if (target.type !== null) { + return target; + } + } + + // over-element + if (data) { + const { index, element } = calculator.getPointElement(p as Point, data, scaleInfo, viewSize); + if (index >= 0 && element) { + target.indexes = [index]; + target.elements = [element]; + target.uuids = [element.uuid]; + target.type = 'over-element'; + return target; + } + } + + return target; +} + +export function resizeElement( + elem: Element, + opts: { + start: Point; + end: Point; + resizeType: ResizeType; + scale: number; + } +): ElementSize { + let { x, y, w, h, angle = 0 } = elem; + if (angle < 0) { + angle = Math.max(0, 360 + angle); + } + + angle = angle > 0 ? angle : Math.max(0, angle + 360); + const { start, end, resizeType, scale } = opts; + + let moveX = (end.x - start.x) / scale; + let moveY = (end.y - start.y) / scale; + + if (elem.operation?.limitRatio === true) { + const maxDist = Math.max(Math.abs(moveX), Math.abs(moveY)); + moveX = (moveX >= 0 ? 1 : -1) * maxDist; + moveY = (((moveY >= 0 ? 1 : -1) * maxDist) / elem.w) * elem.h; + } + + switch (resizeType) { + case 'resize-top': { + if (elem.angle === 0) { + if (h - moveY > 0) { + y += moveY; + h -= moveY; + if (elem.operation?.limitRatio === true) { + x += ((moveY / elem.h) * elem.w) / 2; + w -= (moveY / elem.h) * elem.w; + } + } + } else if (elem.angle !== undefined && (elem.angle > 0 || elem.angle < 0)) { + const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360); + let moveDist = calcMoveDist(moveX, moveY); + let centerX = x + elem.w / 2; + let centerY = y + elem.h / 2; + if (angle < 90) { + moveDist = 0 - changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.sin(radian); + centerY = centerY - centerMoveDist * Math.cos(radian); + } else if (angle < 180) { + moveDist = changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 90); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.cos(radian); + centerY = centerY + centerMoveDist * Math.sin(radian); + } else if (angle < 270) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle - 180); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.sin(radian); + centerY = centerY + centerMoveDist * Math.cos(radian); + } else if (angle < 360) { + moveDist = 0 - changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 270); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.cos(radian); + centerY = centerY - centerMoveDist * Math.sin(radian); + } + if (h + moveDist > 0) { + if (elem.operation?.limitRatio === true) { + w = w + (moveDist / elem.h) * elem.w; + } + h = h + moveDist; + x = centerX - w / 2; + y = centerY - h / 2; + } + } else { + if (h - moveY > 0) { + y += moveY; + h -= moveY; + if (elem.operation?.limitRatio === true) { + x -= moveX / 2; + w += moveX; + } + } + } + break; + } + case 'resize-bottom': { + if (elem.angle === 0) { + if (elem.h + moveY > 0) { + h += moveY; + if (elem.operation?.limitRatio === true) { + x -= ((moveY / elem.h) * elem.w) / 2; + w += (moveY / elem.h) * elem.w; + } + } + } else if (elem.angle !== undefined && (elem.angle > 0 || elem.angle < 0)) { + const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360); + let moveDist = calcMoveDist(moveX, moveY); + let centerX = x + elem.w / 2; + let centerY = y + elem.h / 2; + if (angle < 90) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.sin(radian); + centerY = centerY + centerMoveDist * Math.cos(radian); + } else if (angle < 180) { + moveDist = 0 - changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 90); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.cos(radian); + centerY = centerY - centerMoveDist * Math.sin(radian); + } else if (angle < 270) { + moveDist = changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 180); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.sin(radian); + centerY = centerY - centerMoveDist * Math.cos(radian); + } else if (angle < 360) { + moveDist = changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 270); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.cos(radian); + centerY = centerY + centerMoveDist * Math.sin(radian); + } + if (h + moveDist > 0) { + if (elem.operation?.limitRatio === true) { + w = w + (moveDist / elem.h) * elem.w; + } + h = h + moveDist; + x = centerX - w / 2; + y = centerY - h / 2; + } + } else { + if (elem.h + moveY > 0) { + h += moveY; + if (elem.operation?.limitRatio === true) { + x -= ((moveY / elem.h) * elem.w) / 2; + w += (moveY / elem.h) * elem.w; + } + } + } + break; + } + case 'resize-left': { + if (angle === 0 || !angle) { + if (elem.w - moveX > 0) { + x += moveX; + w -= moveX; + if (elem.operation?.limitRatio === true) { + h -= (moveX / elem.w) * elem.h; + y += ((moveX / elem.w) * elem.h) / 2; + } + } + } else if (elem.angle !== undefined && (elem.angle > 0 || elem.angle < 0)) { + const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360); + let moveDist = calcMoveDist(moveX, moveY); + let centerX = x + elem.w / 2; + let centerY = y + elem.h / 2; + if (angle < 90) { + moveDist = 0 - changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.cos(radian); + centerY = centerY - centerMoveDist * Math.sin(radian); + } else if (angle < 180) { + moveDist = changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 90); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.sin(radian); + centerY = centerY - centerMoveDist * Math.cos(radian); + } else if (angle < 270) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle - 180); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.cos(radian); + centerY = centerY + centerMoveDist * Math.sin(radian); + } else if (angle < 360) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle - 270); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.sin(radian); + centerY = centerY + centerMoveDist * Math.cos(radian); + } + if (w + moveDist > 0) { + if (elem.operation?.limitRatio === true) { + h = h + (moveDist / elem.w) * elem.h; + } + w = w + moveDist; + x = centerX - w / 2; + y = centerY - h / 2; + } + } else { + if (elem.w - moveX > 0) { + x += moveX; + w -= moveX; + if (elem.operation?.limitRatio === true) { + h -= (moveX / elem.w) * elem.h; + y += ((moveX / elem.w) * elem.h) / 2; + } + } + } + break; + } + case 'resize-right': { + if (angle === 0 || !angle) { + if (elem.w + moveX > 0) { + w += moveX; + if (elem.operation?.limitRatio === true) { + y -= (moveX * elem.h) / elem.w / 2; + h += (moveX * elem.h) / elem.w; + } + } + } else if (elem.angle !== undefined && (elem.angle > 0 || elem.angle < 0)) { + const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360); + let moveDist = calcMoveDist(moveX, moveY); + let centerX = x + elem.w / 2; + let centerY = y + elem.h / 2; + if (angle < 90) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.cos(radian); + centerY = centerY + centerMoveDist * Math.sin(radian); + } else if (angle < 180) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle - 90); + const centerMoveDist = moveDist / 2; + centerX = centerX - centerMoveDist * Math.sin(radian); + centerY = centerY + centerMoveDist * Math.cos(radian); + } else if (angle < 270) { + moveDist = changeMoveDistDirect(moveDist, moveY); + const radian = parseRadian(angle - 180); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.cos(radian); + centerY = centerY + centerMoveDist * Math.sin(radian); + moveDist = 0 - moveDist; + } else if (angle < 360) { + moveDist = changeMoveDistDirect(moveDist, moveX); + const radian = parseRadian(angle - 270); + const centerMoveDist = moveDist / 2; + centerX = centerX + centerMoveDist * Math.sin(radian); + centerY = centerY - centerMoveDist * Math.cos(radian); + } + if (w + moveDist > 0) { + if (elem.operation?.limitRatio === true) { + h = h + (moveDist / elem.w) * elem.h; + } + w = w + moveDist; + x = centerX - w / 2; + y = centerY - h / 2; + } + } else { + if (elem.w + moveX > 0) { + w += moveX; + if (elem.operation?.limitRatio === true) { + h += (moveX * elem.h) / elem.w; + y -= (moveX * elem.h) / elem.w / 2; + } + } + } + break; + } + default: { + break; + } + } + + return { x, y, w, h, angle: elem.angle }; +} + +export function getSelectedListArea( + data: Data, + opts: { + start: Point; + end: Point; + scaleInfo: ViewScaleInfo; + viewSize: ViewSizeInfo; + calculator: ViewCalculator; + } +): { indexes: number[]; uuids: string[] } { + const indexes: number[] = []; + const uuids: string[] = []; + const { calculator, scaleInfo, viewSize, start, end } = opts; + + if (!(Array.isArray(data.elements) && start && end)) { + return { indexes, uuids }; + } + const startX = Math.min(start.x, end.x); + const endX = Math.max(start.x, end.x); + const startY = Math.min(start.y, end.y); + const endY = Math.max(start.y, end.y); + + data.elements.forEach((elem, idx) => { + const elemSize = calculator.elementSize(elem, scaleInfo, viewSize); + + const center = calcElementCenter(elemSize); + if (center.x >= startX && center.x <= endX && center.y >= startY && center.y <= endY) { + indexes.push(idx); + uuids.push(elem.uuid); + if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) { + const ves = rotateElementVertexes(elemSize); + if (ves.length === 4) { + const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x]; + const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y]; + elemSize.x = Math.min(...xList); + elemSize.y = Math.min(...yList); + elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList)); + elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList)); + } + } + } + }); + return { indexes, uuids }; +} + +export function calcSelectedElementsArea( + elements: Element[], + opts: { + scaleInfo: ViewScaleInfo; + viewSize: ViewSizeInfo; + calculator: ViewCalculator; + } +): AreaSize | null { + if (!Array.isArray(elements)) { + return null; + } + const area: AreaSize = { x: 0, y: 0, w: 0, h: 0 }; + const { calculator, scaleInfo, viewSize } = opts; + let prevElemSize: ElementSize | null = null; + + elements.forEach((elem) => { + const elemSize = calculator.elementSize(elem, scaleInfo, viewSize); + + if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) { + const ves = rotateElementVertexes(elemSize); + + if (ves.length === 4) { + const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x]; + const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y]; + elemSize.x = Math.min(...xList); + elemSize.y = Math.min(...yList); + elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList)); + elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList)); + } + } + if (prevElemSize) { + const areaStartX = Math.min(elemSize.x, area.x); + const areaStartY = Math.min(elemSize.y, area.y); + + const areaEndX = Math.max(elemSize.x + elemSize.w, area.x + area.w); + const areaEndY = Math.max(elemSize.y + elemSize.h, area.y + area.h); + + area.x = areaStartX; + area.y = areaStartY; + area.w = Math.abs(areaEndX - areaStartX); + area.h = Math.abs(areaEndY - areaStartY); + } else { + area.x = elemSize.x; + area.y = elemSize.y; + area.w = elemSize.w; + area.h = elemSize.h; + } + prevElemSize = elemSize; + }); + return area; +} + +export function isElementInGroup(elem: Element, group: Element<'group'>): boolean { + if (group?.type === 'group' && Array.isArray(group?.desc?.children)) { + for (let i = 0; i < group.desc.children.length; i++) { + const child = group.desc.children[i]; + if (elem.uuid === child.uuid) { + return true; + } + } + } + return false; +} diff --git a/packages/design/dev/data/components/button.ts b/packages/design/dev/data/components/button.ts index 8506ae9..e47e846 100644 --- a/packages/design/dev/data/components/button.ts +++ b/packages/design/dev/data/components/button.ts @@ -1,4 +1,5 @@ import { createUUID } from '@idraw/util'; +import type { ElementSize } from '@idraw/types'; import type { DesignComponent, DesignComponentItem } from '../../../src'; function createButtonItem(variantName: string) { @@ -73,20 +74,21 @@ function createButtonItem(variantName: string) { return componentItem; } -export function createButton(name: string) { +export function createButton(name: string, size?: Partial) { const button: DesignComponent = { uuid: createUUID(), type: 'component', name: `Button ${name}`, x: 50, y: 50, - w: 800, - h: 400, + w: 360, + h: 200, desc: { bgColor: '#aaaaaa54', default: createButtonItem('default'), variants: [createButtonItem('primary'), createButtonItem('secondary')] - } + }, + ...(size || {}) }; return button; } diff --git a/packages/design/dev/data/components/checkbox.ts b/packages/design/dev/data/components/checkbox.ts index b9f1c16..c7350a2 100644 --- a/packages/design/dev/data/components/checkbox.ts +++ b/packages/design/dev/data/components/checkbox.ts @@ -1,4 +1,5 @@ import { createUUID } from '@idraw/util'; +import type { ElementSize } from '@idraw/types'; import type { DesignComponent, DesignComponentItem } from '../../../src'; function createCheckboxItem(variantName: string) { @@ -73,20 +74,21 @@ function createCheckboxItem(variantName: string) { return componentItem; } -export function createCheckbox(name: string) { +export function createCheckbox(name: string, size?: Partial) { const checkbox: DesignComponent = { uuid: createUUID(), type: 'component', name: `Checkbox ${name}`, x: 50, y: 50, - w: 800, - h: 400, + w: 360, + h: 200, desc: { bgColor: '#aaaaaa54', default: createCheckboxItem('default'), variants: [createCheckboxItem('primary'), createCheckboxItem('secondary')] - } + }, + ...(size || {}) }; return checkbox; } diff --git a/packages/design/dev/data/index.ts b/packages/design/dev/data/index.ts index aa4c9c4..c81b9e3 100644 --- a/packages/design/dev/data/index.ts +++ b/packages/design/dev/data/index.ts @@ -3,7 +3,7 @@ import { createButton } from './components/button'; import { createCheckbox } from './components/checkbox'; const data: DesignData = { - components: [createButton('001'), createButton('002'), createCheckbox('001'), createCheckbox('002')], + components: [createButton('001'), createButton('002', { x: 450 }), createCheckbox('001', { x: 50, y: 300 }), createCheckbox('002', { x: 450, y: 300 })], modules: [], pages: [] }; diff --git a/packages/design/src/modules/sketch/index.tsx b/packages/design/src/modules/sketch/index.tsx index 530d4f2..d4b7330 100644 --- a/packages/design/src/modules/sketch/index.tsx +++ b/packages/design/src/modules/sketch/index.tsx @@ -47,7 +47,7 @@ export const Sketch = (props: DashboardProps) => { return; } const core = refCore.current; - const contextSize = calcElementsContextSize(state.viewDrawData.elements, { viewWidth: width, viewHeight: height }); + const contextSize = calcElementsContextSize(state.viewDrawData.elements, { viewWidth: width, viewHeight: height, extend: true }); core.resize({ width, height, diff --git a/packages/lab/README.md b/packages/lab/README.md deleted file mode 100644 index 039dc7d..0000000 --- a/packages/lab/README.md +++ /dev/null @@ -1 +0,0 @@ -# @idraw/studio \ No newline at end of file diff --git a/packages/lab/dev/images/building-001.png b/packages/lab/dev/images/building-001.png deleted file mode 100644 index 4b87e78..0000000 Binary files a/packages/lab/dev/images/building-001.png and /dev/null differ diff --git a/packages/lab/dev/images/building-002.png b/packages/lab/dev/images/building-002.png deleted file mode 100644 index c0699ff..0000000 Binary files a/packages/lab/dev/images/building-002.png and /dev/null differ diff --git a/packages/lab/dev/images/building-003.png b/packages/lab/dev/images/building-003.png deleted file mode 100644 index 5391f28..0000000 Binary files a/packages/lab/dev/images/building-003.png and /dev/null differ diff --git a/packages/lab/dev/images/chart.png b/packages/lab/dev/images/chart.png deleted file mode 100644 index 67efe1f..0000000 Binary files a/packages/lab/dev/images/chart.png and /dev/null differ diff --git a/packages/lab/dev/images/computer.png b/packages/lab/dev/images/computer.png deleted file mode 100644 index 01fde93..0000000 Binary files a/packages/lab/dev/images/computer.png and /dev/null differ diff --git a/packages/lab/dev/images/github.png b/packages/lab/dev/images/github.png deleted file mode 100644 index 3d64592..0000000 Binary files a/packages/lab/dev/images/github.png and /dev/null differ diff --git a/packages/lab/dev/images/lena.png b/packages/lab/dev/images/lena.png deleted file mode 100644 index 08e54c5..0000000 Binary files a/packages/lab/dev/images/lena.png and /dev/null differ diff --git a/packages/lab/dev/images/phone.png b/packages/lab/dev/images/phone.png deleted file mode 100644 index 4ffec7f..0000000 Binary files a/packages/lab/dev/images/phone.png and /dev/null differ diff --git a/packages/lab/dev/index.html b/packages/lab/dev/index.html deleted file mode 100644 index 4a4f633..0000000 --- a/packages/lab/dev/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - -
- - - \ No newline at end of file diff --git a/packages/lab/dev/main.tsx b/packages/lab/dev/main.tsx deleted file mode 100644 index dab0757..0000000 --- a/packages/lab/dev/main.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { Lab } from '../src/index'; - -const dom = document.querySelector('#lab') as HTMLDivElement; -const root = createRoot(dom); - -root.render(); diff --git a/packages/lab/package.json b/packages/lab/package.json deleted file mode 100644 index cf31d21..0000000 --- a/packages/lab/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@idraw/lab", - "version": "0.4.0-alpha.0", - "dependencies": { - "@idraw/core": "^0.4.0-alpha.0", - "@idraw/util": "^0.4.0-alpha.0", - "antd": "^5.5.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@idraw/types": "^0.4.0-alpha.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.1" - } -} diff --git a/packages/lab/src/data.ts b/packages/lab/src/data.ts deleted file mode 100644 index cc71b8a..0000000 --- a/packages/lab/src/data.ts +++ /dev/null @@ -1,521 +0,0 @@ -import type { Data } from '@idraw/types'; -import { deepClone } from '@idraw/util'; - -const data: Data = { - elements: [ - { - uuid: 'xxx-0003', - type: 'image', - x: 100, - y: 100, - w: 100, - h: 100, - angle: 30, - desc: { - src: './images/lena.png' - } - }, - { - uuid: 'xxxx-0001', - x: -50, - y: -40, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#f44336' - } - }, - { - uuid: 'xxx-0002', - type: 'rect', - x: 50, - y: 50, - w: 100, - h: 100, - desc: { - bgColor: '#2196f3' - } - }, - { - uuid: 'xxx-0004', - type: 'image', - x: 250, - y: 250, - w: 100, - h: 100, - desc: { - src: './images/github.png?t=003' - } - }, - { - uuid: 'xxxx-0005', - x: 0, - y: 300, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#009688' - } - }, - { - uuid: 'xxxx-0006', - x: 300, - y: 300, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#673ab7' - } - }, - { - uuid: 'xxxx-0007', - x: 300, - y: 0, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#ffc107' - } - }, - { - uuid: 'xxxx-0008', - x: 150, - y: 150, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#4caf50' - } - }, - { - uuid: 'xxxx-0009', - x: 0, - y: 150, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#ff9800' - } - }, - { - uuid: 'xxxx-0010', - x: 150, - y: 50, - w: 100, - h: 100, - type: 'circle', - desc: { - bgColor: '#cddc39' - } - }, - { - uuid: 'text-0010', - name: 'text-002', - x: 300, - y: 100, - w: 100, - h: 60, - type: 'text', - desc: { - fontSize: 16, - text: [0, 1, 2, 3, 4].map((i) => `Hello Text ${i}`).join('\r\n'), - // text: [0, 1, 2, 3, 4].map(i => `Hello Text ${i}`).join(''), - fontWeight: 'bold', - color: '#666666', - borderRadius: 30, - borderWidth: 2, - borderColor: '#ff5722' - } - }, - { - uuid: 'xxx-0011', - type: 'svg', - x: 400, - y: 100, - w: 100, - h: 100, - desc: { - svg: `` - } - }, - { - uuid: 'xxx-0012', - x: 400, - y: 200, - w: 150, - h: 100, - type: 'html', - angle: 0, - desc: { - html: ` - -
-
- -
-
- -
-
- ` - } - }, - { - uuid: 'group-001', - x: 400, - y: 400, - w: 100, - h: 100, - type: 'group', - desc: { - bgColor: '#1f1f1f', - children: [ - { - uuid: 'group-001-0014', - type: 'circle', - x: -40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#f44336' - } - }, - { - uuid: 'group-001-0015', - type: 'circle', - x: -20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ff9800' - } - }, - { - uuid: 'group-001-0016', - type: 'circle', - x: 0, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ffc106' - } - }, - { - uuid: 'group-001-0017', - type: 'circle', - x: 20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#cddc39' - } - }, - { - uuid: 'group-001-0018', - type: 'circle', - x: 40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#4caf50' - } - } - ] - } - }, - { - uuid: 'group-003', - x: 550, - y: 50, - w: 173.20508075688775, - // w: 100, - h: 100, - angle: 30, - type: 'group', - desc: { - children: [ - { - uuid: 'group-003-014', - type: 'circle', - x: -40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#f44336' - } - }, - { - uuid: 'group-003-0015', - type: 'circle', - x: -20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ff9800' - } - }, - { - uuid: 'group-003-0016', - type: 'circle', - x: 0, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ffc106' - } - }, - { - uuid: 'group-003-0017', - type: 'circle', - x: 20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#cddc39' - } - }, - { - uuid: 'group-003-0018', - type: 'circle', - x: 40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#4caf50' - } - } - ] - } - }, - { - uuid: 'xxxx-0017', - type: 'image', - x: 100, - y: 300, - w: 100, - h: 100, - angle: 30, - desc: { - src: './images/lena.png?v=0017' - } - }, - { - uuid: 'group-004', - x: 550, - y: 250, - w: 375, - h: 400, - type: 'group', - desc: { - bgColor: '#FFFFFF', - children: [ - { - uuid: 'groud-004-001', - type: 'image', - x: 200, - y: 200, - w: 100, - h: 100, - angle: 30, - desc: { - src: './images/lena.png' - } - }, - { - uuid: 'groud-004-002', - type: 'group', - x: 50, - y: 50, - w: 200, - h: 200, - angle: 30, - desc: { - bgColor: '#f0f0f0', - children: [ - { - uuid: 'group-004-002-014', - type: 'circle', - x: -40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#f44336' - } - }, - { - uuid: 'group-004-001-0015', - type: 'circle', - x: -20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ff9800' - } - }, - { - uuid: 'group-004-002-0016', - type: 'circle', - x: 0, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ffc106' - } - }, - { - uuid: 'group-004-002-0017', - type: 'circle', - x: 20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#cddc39' - } - }, - { - uuid: 'group-004-002-0018', - type: 'circle', - x: 40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#4caf50' - } - }, - { - uuid: 'groud-004-002-xxxx', - type: 'group', - x: 50, - y: 100, - w: 200, - h: 100, - angle: 30, - desc: { - bgColor: '#666666', - children: [ - { - uuid: 'group-004-002-xxx-014', - type: 'circle', - x: -40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#f44336' - } - }, - { - uuid: 'group-004-002-xxx-0015', - type: 'circle', - x: -20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ff9800' - } - }, - { - uuid: 'group-004-002-xxx-0016', - type: 'circle', - x: 0, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#ffc106' - } - }, - { - uuid: 'group-004-002-xxx-0017', - type: 'circle', - x: 20, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#cddc39' - } - }, - { - uuid: 'group-004-002-xxx-0018', - type: 'circle', - x: 40, - y: 0, - w: 100, - h: 100, - desc: { - bgColor: '#4caf50' - } - } - ] - } - } - ] - } - } - ] - } - } - ] -}; - -export function getData() { - return deepClone(data); -} diff --git a/packages/lab/src/index.tsx b/packages/lab/src/index.tsx deleted file mode 100644 index 427fabd..0000000 --- a/packages/lab/src/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import { Core, MiddlewareScroller, MiddlewareSelector } from '@idraw/core'; -import { calcElementsContextSize } from '@idraw/util'; -import { getData } from './data'; - -export const Lab = () => { - const ref = useRef(null); - useEffect(() => { - if (ref?.current) { - const data = getData(); - const width = window.innerWidth; - const height = window.innerHeight; - const devicePixelRatio = window.devicePixelRatio; - const options = { - width, - height, - devicePixelRatio - }; - const core = new Core(ref.current, options); - - core.use(MiddlewareScroller); - core.use(MiddlewareSelector); - core.setData(data); - // core.scrollX(0); - // core.scrollY(0); - - window.addEventListener('resize', () => { - const width = window.innerWidth; - const height = window.innerHeight; - const devicePixelRatio = window.devicePixelRatio; - const contextSize = calcElementsContextSize(data.elements, { viewWidth: width, viewHeight: height }); - core.resize({ - width, - height, - devicePixelRatio, - ...contextSize - }); - }); - } - }, []); - return ( -
-
-
- ); -}; diff --git a/packages/lab/src/sketch.tsx b/packages/lab/src/sketch.tsx deleted file mode 100644 index 427fabd..0000000 --- a/packages/lab/src/sketch.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import { Core, MiddlewareScroller, MiddlewareSelector } from '@idraw/core'; -import { calcElementsContextSize } from '@idraw/util'; -import { getData } from './data'; - -export const Lab = () => { - const ref = useRef(null); - useEffect(() => { - if (ref?.current) { - const data = getData(); - const width = window.innerWidth; - const height = window.innerHeight; - const devicePixelRatio = window.devicePixelRatio; - const options = { - width, - height, - devicePixelRatio - }; - const core = new Core(ref.current, options); - - core.use(MiddlewareScroller); - core.use(MiddlewareSelector); - core.setData(data); - // core.scrollX(0); - // core.scrollY(0); - - window.addEventListener('resize', () => { - const width = window.innerWidth; - const height = window.innerHeight; - const devicePixelRatio = window.devicePixelRatio; - const contextSize = calcElementsContextSize(data.elements, { viewWidth: width, viewHeight: height }); - core.resize({ - width, - height, - devicePixelRatio, - ...contextSize - }); - }); - } - }, []); - return ( -
-
-
- ); -}; diff --git a/packages/types/src/lib/board.ts b/packages/types/src/lib/board.ts index f0cc7fa..1f1a146 100644 --- a/packages/types/src/lib/board.ts +++ b/packages/types/src/lib/board.ts @@ -18,8 +18,8 @@ export interface BoardWatherWheelYEvent { point: Point; } -export interface BoardWatherDrawFrameEvent { - snapshot: BoardViewerFrameSnapshot; +export interface BoardWatherDrawFrameEvent> { + snapshot: BoardViewerFrameSnapshot; } export type BoardWatherScaleEvent = ViewScaleInfo; @@ -30,7 +30,7 @@ export type BoardWatherScrollYEvent = ViewScaleInfo; export type BoardWatherResizeEvent = ViewSizeInfo; -export interface BoardWatcherEventMap { +export interface BoardWatcherEventMap> { hover: BoardWatcherPointEvent; pointStart: BoardWatcherPointEvent; pointMove: BoardWatcherPointEvent; @@ -43,52 +43,52 @@ export interface BoardWatcherEventMap { scrollX: BoardWatherScrollXEvent; scrollY: BoardWatherScrollYEvent; resize: BoardWatherResizeEvent; - beforeDrawFrame: BoardWatherDrawFrameEvent; - afterDrawFrame: BoardWatherDrawFrameEvent; + beforeDrawFrame: BoardWatherDrawFrameEvent; + afterDrawFrame: BoardWatherDrawFrameEvent; } export type BoardMode = 'SELECT' | 'SCROLL' | 'RULE' | 'CONNECT' | 'PENCIL' | 'PEN' | string; -export interface BoardMiddlewareObject { +export interface BoardMiddlewareObject> { mode: BoardMode; isDefault?: boolean; created?: () => void; // action - hover?: (e: BoardWatcherEventMap['hover']) => void | boolean; - pointStart?: (e: BoardWatcherEventMap['pointStart']) => void | boolean; - pointMove?: (e: BoardWatcherEventMap['pointMove']) => void | boolean; - pointEnd?: (e: BoardWatcherEventMap['pointEnd']) => void | boolean; - pointLeave?: (e: BoardWatcherEventMap['pointLeave']) => void | boolean; - doubleClick?: (e: BoardWatcherEventMap['doubleClick']) => void | boolean; - wheelX?: (e: BoardWatcherEventMap['wheelX']) => void | boolean; - wheelY?: (e: BoardWatcherEventMap['wheelY']) => void | boolean; + hover?: (e: BoardWatcherEventMap['hover']) => void | boolean; + pointStart?: (e: BoardWatcherEventMap['pointStart']) => void | boolean; + pointMove?: (e: BoardWatcherEventMap['pointMove']) => void | boolean; + pointEnd?: (e: BoardWatcherEventMap['pointEnd']) => void | boolean; + pointLeave?: (e: BoardWatcherEventMap['pointLeave']) => void | boolean; + doubleClick?: (e: BoardWatcherEventMap['doubleClick']) => void | boolean; + wheelX?: (e: BoardWatcherEventMap['wheelX']) => void | boolean; + wheelY?: (e: BoardWatcherEventMap['wheelY']) => void | boolean; - scale?: (e: BoardWatcherEventMap['scale']) => void | boolean; - scrollX?: (e: BoardWatcherEventMap['scrollX']) => void | boolean; - scrollY?: (e: BoardWatcherEventMap['scrollY']) => void | boolean; - resize?: (e: BoardWatcherEventMap['resize']) => void | boolean; + scale?: (e: BoardWatcherEventMap['scale']) => void | boolean; + scrollX?: (e: BoardWatcherEventMap['scrollX']) => void | boolean; + scrollY?: (e: BoardWatcherEventMap['scrollY']) => void | boolean; + resize?: (e: BoardWatcherEventMap['resize']) => void | boolean; // draw - beforeDrawFrame?(e: BoardWatcherEventMap['beforeDrawFrame']): void | boolean; - afterDrawFrame?(e: BoardWatcherEventMap['afterDrawFrame']): void | boolean; + beforeDrawFrame?(e: BoardWatcherEventMap['beforeDrawFrame']): void | boolean; + afterDrawFrame?(e: BoardWatcherEventMap['afterDrawFrame']): void | boolean; } -export interface BoardMiddlewareOptions { +export interface BoardMiddlewareOptions> { viewContent: ViewContent; - sharer: StoreSharer; + sharer: StoreSharer; viewer: BoardViewer; calculator: ViewCalculator; } -export type BoardMiddleware = (opts: BoardMiddlewareOptions) => BoardMiddlewareObject; +export type BoardMiddleware = any> = (opts: BoardMiddlewareOptions) => BoardMiddlewareObject; export interface BoardOptions { viewContent: ViewContent; } -export interface BoardViewerFrameSnapshot { +export interface BoardViewerFrameSnapshot> { activeStore: ActiveStore; - sharedStore: Record; + sharedStore: S; } export interface BoardViewerEventMap { @@ -97,12 +97,12 @@ export interface BoardViewerEventMap { } export interface BoardViewerOptions { - sharer: StoreSharer; + sharer: StoreSharer>; renderer: BoardRenderer; calculator: ViewCalculator; viewContent: ViewContent; - beforeDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot }) => void; - afterDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot }) => void; + beforeDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot> }) => void; + afterDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot> }) => void; } export interface BoardViewer extends UtilEventEmitter { @@ -121,9 +121,10 @@ export interface BoardRenderer extends UtilEventEmitter { export interface BoardWatcherOptions { viewContent: ViewContent; - sharer: StoreSharer; + sharer: StoreSharer>; } export interface BoardWatcherStore { hasPointDown: boolean; + prevClickPoint: Point | null; } diff --git a/packages/types/src/lib/core.ts b/packages/types/src/lib/core.ts index 487369d..4766cfb 100644 --- a/packages/types/src/lib/core.ts +++ b/packages/types/src/lib/core.ts @@ -2,6 +2,7 @@ export interface CoreOptions { width: number; height: number; devicePixelRatio?: number; + padding?: number; // contextWidth?: number; // contextHeight?: number; // onlyRender?: boolean; diff --git a/packages/types/src/lib/store.ts b/packages/types/src/lib/store.ts index 314676c..30fec45 100644 --- a/packages/types/src/lib/store.ts +++ b/packages/types/src/lib/store.ts @@ -8,13 +8,14 @@ export type ActiveStore = ViewSizeInfo & selectedUUIDs: string[]; }; -export interface StoreSharer { +export interface StoreSharer> { getActiveStorage(key: T): ActiveStore[T]; setActiveStorage(key: T, storage: ActiveStore[T]): void; getActiveStoreSnapshot(): ActiveStore; - getSharedStorage(key: string): any; - setSharedStorage(key: string, storage: any): void; + getSharedStorage(key: K): S[K]; + setSharedStorage(key: K, storage: S[K]): void; getSharedStoreSnapshot(): Record; + getActiveScaleInfo(): ViewScaleInfo; setActiveScaleInfo(scaleInfo: ViewScaleInfo): void; setActiveViewSizeInfo(size: ViewSizeInfo): void; diff --git a/packages/util/src/lib/data.ts b/packages/util/src/lib/data.ts index b69f2fd..821b866 100644 --- a/packages/util/src/lib/data.ts +++ b/packages/util/src/lib/data.ts @@ -10,11 +10,15 @@ export function deepClone(target: any): any { }); return arr; } else if (type === 'Object') { - const obj: { [key: string]: any } = {}; + const obj: { [key: string | symbol]: any } = {}; const keys = Object.keys(t); keys.forEach((key) => { obj[key] = _clone(t[key]); }); + const symbolKeys = Object.getOwnPropertySymbols(t); + symbolKeys.forEach((key) => { + obj[key] = _clone(t[key]); + }); return obj; } } diff --git a/packages/util/src/lib/element.ts b/packages/util/src/lib/element.ts index ba65f0b..df5888d 100644 --- a/packages/util/src/lib/element.ts +++ b/packages/util/src/lib/element.ts @@ -124,7 +124,10 @@ export function validateElements(elements: Array>): boolean type AreaSize = ElementSize; -export function calcElementsContextSize(elements: Array>, opts?: { viewWidth: number; viewHeight: number }): ViewContextSize { +export function calcElementsContextSize( + elements: Array>, + opts?: { viewWidth: number; viewHeight: number; extend?: boolean } +): ViewContextSize { const area: AreaSize = { x: 0, y: 0, w: 0, h: 0 }; let prevElemSize: ElementSize | null = null; elements.forEach((elem: Element) => { @@ -167,6 +170,11 @@ export function calcElementsContextSize(elements: Array>, o prevElemSize = elemSize; }); + if (opts?.extend) { + area.x = Math.min(area.x, 0); + area.y = Math.min(area.y, 0); + } + const ctxSize: ViewContextSize = { contextX: area.x, contextY: area.y, @@ -198,11 +206,11 @@ export function calcElementsViewInfo( changeContextTop: number; changeContextBottom: number; } { - const contextSize = calcElementsContextSize(elements, { viewWidth: prevViewSize.width, viewHeight: prevViewSize.height }); + const contextSize = calcElementsContextSize(elements, { viewWidth: prevViewSize.width, viewHeight: prevViewSize.height, extend: options?.extend }); if (options?.extend === true) { - contextSize.contextX = Math.min(contextSize.contextX, prevViewSize.contextX); - contextSize.contextY = Math.min(contextSize.contextY, prevViewSize.contextY); + contextSize.contextX = Math.min(0, contextSize.contextX, prevViewSize.contextX); + contextSize.contextY = Math.min(0, contextSize.contextY, prevViewSize.contextY); contextSize.contextWidth = Math.max(contextSize.contextWidth, prevViewSize.contextWidth); contextSize.contextHeight = Math.max(contextSize.contextHeight, prevViewSize.contextHeight); } diff --git a/packages/util/src/lib/store.ts b/packages/util/src/lib/store.ts index adb1ae1..415f97c 100644 --- a/packages/util/src/lib/store.ts +++ b/packages/util/src/lib/store.ts @@ -1,6 +1,6 @@ import { deepClone } from './data'; -export class Store> { +export class Store> { private _temp: T; private _backUpDefaultStorage: T;