import { Able, Bounds, params, payload, PositionSchema, TransformData, TransformSchema } from '../../common'; import { Matrix } from '@gedit/math'; import { Dragable, DragablePayload } from './dragable'; import { asVec } from '../../common'; export enum ResizeType { LEFT = 'left', RIGHT = 'right', BOTTOM = 'bottom', TOP = 'top', LEFT_TOP = 'leftTop', LEFT_BOTTOM = 'leftBottom', RIGHT_TOP = 'rightTop', RIGHT_BOTTOM = 'rightBottom', ORIGIN = 'origin' } export const ResizePayload = Symbol('ResizePayload'); export interface ResizePayload extends DragablePayload { type: ResizeType } export const MIN_SIZE = 10; interface ResizableCache { data: TransformSchema, localMatrix: Matrix, worldMatrix: Matrix, startInverse: PositionSchema, bounds: { bottomCenter: PositionSchema, bottomLeft: PositionSchema, bottomRight: PositionSchema, topCenter: PositionSchema, topLeft: PositionSchema, topRight: PositionSchema, leftCenter: PositionSchema, rightCenter: PositionSchema, origin: PositionSchema, } boundsInverse: { bottomCenter: PositionSchema, bottomLeft: PositionSchema, bottomRight: PositionSchema, topCenter: PositionSchema, topLeft: PositionSchema, topRight: PositionSchema, leftCenter: PositionSchema, rightCenter: PositionSchema, origin: PositionSchema, } } type ResizeControlKey = 'bottomCenter' | 'bottomLeft' | 'bottomRight' | 'topCenter' | 'topLeft' | 'topRight' | 'leftCenter' | 'rightCenter' | 'origin'; interface ResizeControl { movingKey: ResizeControlKey, // 移动的点 staticKey: ResizeControlKey, // 静止的点 changeWidth?: boolean changeHeight?: boolean } function positionInverse(key: ResizeControlKey, transform: TransformData, matrix: Matrix): PositionSchema { switch (key) { case 'bottomCenter': return Bounds.getBottomCenter(transform.data, matrix); case 'bottomLeft': return Bounds.getBottomLeft(transform.data, matrix); case 'bottomRight': return Bounds.getBottomRight(transform.data, matrix); case 'leftCenter': return Bounds.getLeftCenter(transform.data, matrix); case 'rightCenter': return Bounds.getRightCenter(transform.data, matrix); case 'topCenter': return Bounds.getTopCenter(transform.data, matrix); case 'topLeft': return Bounds.getTopLeft(transform.data, matrix); case 'topRight': return Bounds.getTopRight(transform.data, matrix); case 'origin': return Bounds.getCenter(transform.data, matrix); } } function resizeTransform(control: ResizeControl, transform: TransformData, cache: ResizableCache, delta: PositionSchema, ratio: number): void { const { movingKey, staticKey, changeWidth, changeHeight } = control; transform.pauseChanged = true; const matrix = cache.localMatrix; const { bounds, boundsInverse } = cache; const baseData = cache.data; const { origin } = transform; const movingPos = bounds[movingKey]; const staticPosInverse = boundsInverse[staticKey]; // 非原点模式 const originXNormal = origin.x <= 0 || origin.x >= 1; const originYNormal = origin.y <= 0 || origin.y >= 1; const staticPos = { x: originXNormal ? bounds[staticKey].x : 0, y: originYNormal ? bounds[staticKey].y : 0, }; const newMovingPos = { x: changeWidth ? movingPos.x + delta.x : movingPos.x, y: changeHeight ? movingPos.y + delta.y : movingPos.y, }; let width: number | undefined; let height: number | undefined; // changeWidth, changeHeight => customWidth, customHeight, 自定义了宽高 if (typeof changeWidth !== 'undefined') { transform.size.changeWidth = changeWidth; } if (typeof changeHeight !== 'undefined') { transform.size.changeHeight = changeHeight; } if (changeWidth) { if (originXNormal) { width = movingKey.match(/left/i) ? staticPos.x - newMovingPos.x : newMovingPos.x - staticPos.x; } else { // 1 - (origin.x || 0) || 1: 0 设回 1 const s = movingKey.match(/right/ig) ? 1 - (origin.x || 0) || 1 : (origin.x || 1); width = Math.abs(newMovingPos.x - staticPos.x) / s; } if (width < MIN_SIZE) width = MIN_SIZE; if (ratio) height = width * ratio; } if (changeHeight) { if (originYNormal) { height = movingKey.match(/top/i) ? staticPos.y - newMovingPos.y : newMovingPos.y - staticPos.y; } else { const s = movingKey.match(/bottom/ig) ? 1 - (origin.y || 0) || 1 : (origin.y || 1); height = Math.abs(newMovingPos.y - staticPos.y) / s; } if (height < MIN_SIZE) height = MIN_SIZE; if (ratio) width = height / ratio; } if (width !== undefined) transform.size.width = width; if (height !== undefined) transform.size.height = height; if (transform.sizeToScale) { if (width !== undefined) { transform.size.width = baseData.size.width; transform.scale.x = baseData.scale.x ? width / baseData.size.width * baseData.scale.x : 0; } if (height !== undefined) { transform.size.height = baseData.size.height; transform.scale.y = height / baseData.size.height * baseData.scale.y; } } if (originXNormal || originYNormal) { const newStaticPosInverse = positionInverse(staticKey, transform, matrix); const posDelta = { x: originXNormal ? staticPosInverse.x - newStaticPosInverse.x : 0, y: originYNormal ? staticPosInverse.y - newStaticPosInverse.y : 0, }; transform.update({ position: { x: baseData.position.x + posDelta.x, y: baseData.position.y + posDelta.y, } }); } transform.pauseChanged = false; transform.fireChanged(); } export class Resizable extends Able { // 用于吸附计算 static globalBefore = Dragable.globalBefore; static type = 'Resizable'; protected supportTypes: Set = new Set(['all']); protected startCacheMap: Map = new Map(); @payload(ResizePayload) payload: ResizePayload; @params(TransformData) handle(transform: TransformData): void { const { type, scale = 1, startPos, endPos, isStart, isMoving } = this.payload; if (!this.supportTypes.has('all') && !this.supportTypes.has(type)) return; if (isStart && !isMoving) { const data = transform.data; const localMatrix = transform.localTransform; const bounds = { topCenter: Bounds.getTopCenter(data), topRight: Bounds.getTopRight(data), topLeft: Bounds.getTopLeft(data), bottomCenter: Bounds.getBottomCenter(data), bottomRight: Bounds.getBottomRight(data), bottomLeft: Bounds.getBottomLeft(data), leftCenter: Bounds.getLeftCenter(data), rightCenter: Bounds.getRightCenter(data), origin: Bounds.getCenter(data) }; const boundsInverse = { topCenter: localMatrix.apply(bounds.topCenter), topRight: localMatrix.apply(bounds.topRight), topLeft: localMatrix.apply(bounds.topLeft), bottomCenter: localMatrix.apply(bounds.bottomCenter), bottomRight: localMatrix.apply(bounds.bottomRight), bottomLeft: localMatrix.apply(bounds.bottomLeft), leftCenter: localMatrix.apply(bounds.leftCenter), rightCenter: localMatrix.apply(bounds.rightCenter), origin: localMatrix.apply(bounds.origin) }; this.startCacheMap.set(transform, { data, localMatrix: localMatrix.clone(), worldMatrix: transform.worldTransform.clone(), startInverse: transform.worldTransform.applyInverse(startPos), bounds, boundsInverse, }); } else if (isMoving) { const locked = !!transform.size.locked; const start = this.startCacheMap.get(transform); if (!start) return; const ratio = (locked && start.data.size.width) ? (start.data.size.height / start.data.size.width) : 0; const startInverse = start.startInverse; const endInverse = start.worldMatrix.applyInverse(endPos); // 把位移投影到矩阵上 const delta = { x: (endInverse.x - startInverse.x) / scale, y: (endInverse.y - startInverse.y) / scale, }; switch (type) { case ResizeType.TOP: resizeTransform({ movingKey: 'topCenter', staticKey: 'bottomCenter', changeHeight: true, }, transform, start, delta, ratio); break; case ResizeType.BOTTOM: resizeTransform({ movingKey: 'bottomCenter', staticKey: 'topCenter', changeHeight: true }, transform, start, delta, ratio); break; case ResizeType.LEFT: resizeTransform({ movingKey: 'leftCenter', staticKey: 'rightCenter', changeWidth: true }, transform, start, delta, ratio); break; case ResizeType.RIGHT: resizeTransform({ movingKey: 'rightCenter', staticKey: 'leftCenter', changeWidth: true }, transform, start, delta, ratio); break; case ResizeType.LEFT_TOP: resizeTransform({ movingKey: 'topLeft', staticKey: 'bottomRight', changeWidth: true, changeHeight: true }, transform, start, delta, ratio); break; case ResizeType.RIGHT_BOTTOM: resizeTransform({ movingKey: 'bottomRight', staticKey: 'topLeft', changeWidth: true, changeHeight: true }, transform, start, delta, ratio); break; case ResizeType.RIGHT_TOP: resizeTransform({ movingKey: 'topRight', staticKey: 'bottomLeft', changeWidth: true, changeHeight: true }, transform, start, delta, ratio); break; case ResizeType.LEFT_BOTTOM: resizeTransform({ movingKey: 'bottomLeft', staticKey: 'topRight', changeWidth: true, changeHeight: true }, transform, start, delta, ratio); break; // case ResizeType.CENTER: // transform.update({ // position: { // x: start.position.x + delta.x, // y: start.position.y + delta.y, // } // }); case ResizeType.ORIGIN: const { size, scale: dScale, origin, position, rotation } = start.data; const dx = delta.x / size.width; const dy = delta.y / size.height; // 求移动点的角度 const moveVec = asVec(position, { x: position.x + delta.x * dScale.x, y: position.y + delta.y * dScale.y }); const moveRotate = moveVec.ang + rotation; const x = position.x + moveVec.len * Math.cos(moveRotate); const y = position.y + moveVec.len * Math.sin(moveRotate); const ox = dx + origin.x; const oy = dy + origin.y; if (transform.isPath) { // @ts-ignore const gameObject = this.entity.gameObject as any; if ((ox || oy) && !gameObject.data.path.originChangeBeforeSize) { gameObject.data.path.originChangeBeforeSize = { width: size.width, height: size.height }; } else if (!ox && !oy && gameObject.data.path.originChangeBeforeSize) { gameObject.data.path.originChangeBeforeSize = undefined; } transform.update({ origin: { x: ox, y: oy } }); break; } // 中心点移多少加多少; transform.update({ origin: { x: ox, y: oy }, position : { x, y } }); break; } } else { this.startCacheMap.clear(); } } support(...types: ('all' | ResizeType)[]): void { this.supportTypes = new Set(types); } }