import { Schema } from './schema'; import { PositionData, PositionSchema } from './position-schema'; import { SizeData, SizeSchema } from './size-schema'; import { OriginData, OriginSchema } from './origin-schema'; import { ScaleData, ScaleSchema } from './scale-schema'; import { SkewData, SkewSchema } from './skew-schema'; import { RotationData } from './rotation-schema'; import { EntityData } from '../entity-data'; import type { Entity } from '../entity'; import { Angle, Circle, Matrix, PI_2, RAD_TO_DEG, Rectangle, } from '@gedit/math'; import { Bounds } from '../utils/bounds'; import { Disposable, DisposableCollection, TransformSchema, TransformSchemaDecoration, } from '@gedit/utils'; interface Point { x: number; y: number; } interface VData { x: number; y: number; nx: number; ny: number; len: number; ang: number; } const asVec = (p: Point, pp: Point): VData => { const x = pp.x - p.x; const y = pp.y - p.y; const len = Math.sqrt(x * x + y * y) || 0; const nx = x / len || 0; const ny = y / len || 0; const ang = Math.atan2(ny, nx) || 0; return { x, y, len, nx, ny, ang, }; }; export { TransformSchemaDecoration, TransformSchema, asVec }; interface Rect { x: number; y: number; width: number; height: number; } const floatNum = 1000; // 浮点取四位; export class TransformData extends EntityData implements TransformSchema { static type = 'TransformData'; protected _worldTransform: Matrix = new Matrix(); protected _localTransform: Matrix = new Matrix(); protected _children: TransformData[] | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any protected worldCache: Map = new Map(); public sizeToScale: boolean = false; // 标记size转成scale public componentSceneSize?: { width: number; height: number }; // 标记 dom 组件使用大小; public isMask?: boolean; // 是否 mask,如果是 mask 去除 bounds; public maskTransform?: TransformData; // mask 距阵 public visible?: boolean; get children(): TransformData[] { return this._children || []; } /** * 标记为容器,则选择框会动态计算子节点大小 */ isContainer = false; /** * 标记为路径, path 路径移动中心点时不更新 position,, resizable 里使用; */ isPath = false; /** * The X-coordinate value of the normalized local X axis, * the first column of the local transformation matrix without a scale. */ protected _cx: number = 1; /** * The Y-coordinate value of the normalized local X axis, * the first column of the local transformation matrix without a scale. */ protected _sx: number = 0; /** * The X-coordinate value of the normalized local Y axis, * the second column of the local transformation matrix without a scale. */ protected _cy: number = 0; /** * The Y-coordinate value of the normalized local Y axis, * the second column of the local transformation matrix without a scale. */ protected _sy: number = 1; /** * The locally unique ID of the local transform. */ protected _localID: number = 0; /** * The locally unique ID of the local transform * used to calculate the current local transformation matrix. */ protected _currentLocalID: number = 0; /** * The locally unique ID of the world transform. */ protected _worldID = 0; /** * The locally unique ID of the parent's world transform * used to calculate the current world transformation matrix. */ protected _parentID = 0; /** * The parent transform */ protected _parent?: TransformData; constructor(protected entity: Entity) { super(entity); // 默认添加 this.bindChanged(this.entity.addData(PositionData)); this.bindChanged(this.entity.addData(SizeData)); this.bindChanged(this.entity.addData(OriginData)); this.bindChanged(this.entity.addData(ScaleData)); this.bindChanged(this.entity.addData(SkewData), () => this.updateSkew()); this.bindChanged(this.entity.addData(RotationData), () => this.updateSkew() ); } private bindChanged(data: EntityData, fn?: () => void): void { data.onDataChanged(() => { if (fn) fn(); this.fireChanged(); }); } fireChanged(): void { if (this.pauseChanged) return; this._localID++; this.worldCache.clear(); super.fireChanged(); } get localTransform(): Matrix { this.updateLocalTransformMatrix(); return this._localTransform; } get worldTransform(): Matrix { this.updateTransformMatrix(); return this._worldTransform; } getDefaultData(): TransformSchema { return Schema.createDefault(TransformSchemaDecoration); } update(data: Partial): void { if (data.position) { this.entity.updateData(PositionData, data.position); } if (data.size) { this.entity.updateData(SizeData, data.size); } if (data.origin) { this.entity.updateData(OriginData, data.origin); } if (data.scale) { this.entity.updateData(ScaleData, data.scale); } if (data.skew) { this.entity.updateData(SkewData, data.skew); } if (data.rotation !== undefined) { this.entity.updateData(RotationData, data.rotation); } } get position(): PositionSchema { return this.entity.getData(PositionData)!; } set position(position: PositionSchema) { this.entity.updateData(PositionData, position); } get size(): SizeSchema { return this.entity.getData(SizeData)!; } set size(size: SizeSchema) { this.entity.updateData(SizeData, size); } get origin(): OriginSchema { return this.entity.getData(OriginData)!; } set origin(origin: OriginSchema) { this.entity.updateData(OriginData, origin); } get scale(): ScaleSchema { return this.entity.getData(ScaleData)!; } set scale(scale: ScaleSchema) { this.entity.updateData(ScaleData, scale); } get skew(): SkewSchema { return this.entity.getData(SkewData)!; } set skew(skew: SkewSchema) { this.entity.updateData(SkewData, skew); } get rotation(): number { return this.entity.getData(RotationData)!.data; } set rotation(rotation: number) { this.entity.updateData(RotationData, rotation); } get data(): TransformSchema { return TransformSchema.toJSON(this); } /** * Called when the skew or the rotation changes. * * @protected */ protected updateSkew(): void { const rotation = this.rotation; this._cx = Math.cos(rotation + this.skew.y); this._sx = Math.sin(rotation + this.skew.y); this._cy = -Math.sin(rotation - this.skew.x); // cos, added PI/2 this._sy = Math.cos(rotation - this.skew.x); // sin, added PI/2 this._localID++; } /** * Updates the local transformation matrix. */ protected updateLocalTransformMatrix(): void { const lt = this._localTransform; if (this._localID !== this._currentLocalID) { // get the matrix values of the displayobject based on its transform properties.. lt.a = this._cx * this.scale.x; lt.b = this._sx * this.scale.x; lt.c = this._cy * this.scale.y; lt.d = this._sy * this.scale.y; lt.tx = this.position.x; // - (this.origin.x * lt.a + this.origin.y * lt.c); lt.ty = this.position.y; // - (this.origin.x * lt.b + this.origin.y * lt.d); this._currentLocalID = this._localID; // force an update.. this._parentID = -1; } } /** * Updates the local and the world transformation matrices. * */ protected updateTransformMatrix(): void { const lt = this._localTransform; this.updateLocalTransformMatrix(); let parentTransform: Matrix = Matrix.TEMP_MATRIX; let worldId = 0; if (this.parent) { parentTransform = this.parent.worldTransform; worldId = this.parent._worldID; } if (this._parentID !== worldId) { // concat the parent matrix with the objects transform. const pt = parentTransform; const wt = this._worldTransform; wt.a = lt.a * pt.a + lt.b * pt.c; wt.b = lt.a * pt.b + lt.b * pt.d; wt.c = lt.c * pt.a + lt.d * pt.c; wt.d = lt.c * pt.b + lt.d * pt.d; wt.tx = lt.tx * pt.a + lt.ty * pt.c + (pt.tx || 0); wt.ty = lt.tx * pt.b + lt.ty * pt.d + (pt.ty || 0); this._parentID = worldId; // update the id of the transform.. this._worldID++; this.worldCache.clear(); } } /** * Decomposes a matrix and sets the transforms properties based on it. * * matrix - The matrix to decompose */ setFromMatrix(matrix: Matrix): void { // sort out rotation / skew.. const a = matrix.a; const b = matrix.b; const c = matrix.c; const d = matrix.d; const skewX = -Math.atan2(-c, d); const skewY = Math.atan2(b, a); const delta = Math.abs(skewX + skewY); if (delta < 0.00001 || Math.abs(PI_2 - delta) < 0.00001) { this.rotation = skewY; this.skew.x = this.skew.y = 0; } else { this.rotation = 0; this.skew.x = skewX; this.skew.y = skewY; } // next set scale this.scale.x = Math.sqrt(a * a + b * b); this.scale.y = Math.sqrt(c * c + d * d); // next set position this.position.x = matrix.tx; this.position.y = matrix.ty; this.fireChanged(); } protected getFromWorldCache(key: string, fn: () => T): T { this.updateTransformMatrix(); if (this.worldCache.has(key)) return this.worldCache.get(key) as T; const item = fn(); this.worldCache.set(key, item); return item; } protected setPIXIOriginToRect(rect: Rect): void { // pixi 的 container 减掉 pivot if ( this.entity.context.appConfig.engineName === 'pixi' || this.entity.context.appConfig.engineName === 'galacean' ) { const { origin, scale, size, rotation } = this.data; const originX = (origin.x - (origin.initX || 0)) * size.width * scale.x || 0; const originY = (origin.y - (origin.initY || 0)) * size.height * scale.y || 0; const p1 = { x: 0, y: 0 }; const p2 = { x: originX, y: originY }; const vec = asVec(p2, p1); const x = originX + vec.len * Math.cos(vec.ang + rotation); const y = originY + vec.len * Math.sin(vec.ang + rotation); rect.x = rect.x - originX + x; rect.y = rect.y - originY + y; } } /** * bounds; * container 使用带中心点旋转后重新计算的 bounds * * 带父级中心点旋转后重新计算的 bounds; */ get bounds(): Rectangle { return this.getFromWorldCache('bounds', () => { if (this.isContainer && this.children.length) { if (this.componentSceneSize) { const { origin } = this; const size = this.componentSceneSize; const scale = this.worldScale; const bx = -origin.x * size.width; const by = -origin.y * size.height; const rect: Rect = { width: scale.x * size.width, height: scale.y * size.height, x: this.worldTransform.tx + bx * scale.x, y: this.worldTransform.ty + by * scale.y, }; return new Rectangle(rect.x, rect.y, rect.width, rect.height); } const childrenRect = this.children .map(c => { if (c.isMask || !c.visible) { return new Rectangle(0, 0, 0, 0); } if (c.maskTransform) { return c.boundsMask(c.bounds, c.maskTransform?.bounds); } return c.bounds; }) .filter(c => c.width && c.height); if (!childrenRect.length) { return Bounds.getBounds(this, this.worldTransform); } const rect = Rectangle.enlarge(childrenRect); this.setPIXIOriginToRect(rect); return rect; } return Bounds.getBounds(this, this.worldTransform); }); } protected boundsMask(bounds: Rectangle, maskBounds?: Rectangle): Rectangle { if (!maskBounds) { return bounds; } const minX = bounds.x > maskBounds.x ? bounds.x : maskBounds.x; const minY = bounds.y > maskBounds.y ? bounds.y : maskBounds.y; const maxX = bounds.x + bounds.width < maskBounds.x + maskBounds.width ? bounds.x + bounds.width : maskBounds.x + maskBounds.width; const maxY = bounds.y + bounds.height < maskBounds.y + maskBounds.height ? bounds.y + bounds.height : maskBounds.y + maskBounds.height; if (minX <= maxX && minY <= maxY) { return new Rectangle(minX, minY, maxX - minX, maxY - minY); } return new Rectangle(0, 0, 0, 0); } /** * 不旋转的bounds */ get boundsWithoutRotation(): Rectangle { return this.getFromWorldCache('boundsWithoutRotation', () => { const { origin, localBounds } = this; const size = this.localSize; const scale = this.worldScale; const bx = this.isContainer && !this.componentSceneSize ? localBounds.x : -origin.x * size.width; const by = this.isContainer && !this.componentSceneSize ? localBounds.y : -origin.y * size.height; const rect: Rect = { width: scale.x * size.width, height: scale.y * size.height, x: this.worldTransform.tx + bx * scale.x, y: this.worldTransform.ty + by * scale.y, }; if (this.isContainer && !this.componentSceneSize) { this.setPIXIOriginToRect(rect); } // console.log(rect); return new Rectangle(rect.x, rect.y, rect.width, rect.height); }); } /** * 本身的大小 */ get localSize(): SizeSchema { let size; if (this.componentSceneSize) { size = this.componentSceneSize; } else { size = this.isContainer ? this.localBounds : this.size; } return { width: size.width, height: size.height, }; } get worldSize(): SizeSchema { const localSize = this.localSize; const worldScale = this.worldScale; return { width: localSize.width * worldScale.x, height: localSize.height * worldScale.y, }; } /** * 本地bounds */ get localBounds(): Rectangle { // return this.getFromWorldCache('localBounds', () => { if (this.isContainer) { const children = this._children; if (!children || children.length === 0) { return Rectangle.EMPTY; } // 计算 children 的坐标; 去除空组元素; const rects = children .map(c => { if (c.isMask || !c.visible) { return new Rectangle(0, 0, 0, 0); } if (c.maskTransform) { return c.boundsMask( Bounds.getBounds(c, c.localTransform), Bounds.getBounds(c.maskTransform, c.maskTransform.localTransform) ); } return Bounds.getBounds(c, c.localTransform); }) .filter(c => c.width && c.height); // console.log('TTTTTTTTTTTTTTTTTTTTTTTT', rects, Rectangle.enlarge(rects), children); return Rectangle.enlarge(rects); } return Bounds.getBounds(this, this.localTransform); // }); } /** * 判断是否包含点 * @param x * @param y * @param asCircle - 以圆形来算,TODO 目前不支持椭圆形 */ contains(x: number, y: number, asCircle?: boolean): boolean { // Container情况不支持circle if (this.isContainer) { return this.bounds.contains(x, y); } const tempPoint = this.worldTransform.applyInverse({ x, y }); const width = this.size.width; const height = this.size.height; // 不包含空大小 TODO if (width === 0 || height === 0) return false; const x1 = -width * this.origin.x; const y1 = -height * this.origin.y; if (asCircle) { const circle = new Circle( x1 + width / 2, y1 + height / 2, Math.min(width / 2, height / 2) ); return circle.contains(tempPoint.x, tempPoint.y); } if (tempPoint.x >= x1 && tempPoint.x < x1 + width) { if (tempPoint.y >= y1 && tempPoint.y < y1 + height) { return true; } } return false; } get parent(): TransformData | undefined { return this._parent; } isParent(parent: TransformData): boolean { let currentParent = this.parent; while (currentParent) { if (currentParent === parent) return true; currentParent = currentParent.parent; } return false; } private _parentChangedDispose?: DisposableCollection; setParent( parent: TransformData | undefined, listenParentData: boolean = true ): void { if (this._parent !== parent) { if (this._parentChangedDispose) { this._parentChangedDispose.dispose(); this._parentChangedDispose = undefined; } this._parentID = -1; if (parent && listenParentData) { if (!parent._children) parent._children = []; parent._children.push(this); this._parentChangedDispose = new DisposableCollection(); this.toDispose.push(this._parentChangedDispose); this._parentChangedDispose.pushAll([ parent.onDataChanged(() => { this.fireChanged(); }), parent.onDispose(() => { this.setParent(undefined); }), Disposable.create(() => { const index = (parent._children || []).indexOf(this); if (index !== -1) { (parent._children || []).splice(index, 1); if (parent.isContainer) { parent._localID++; parent.worldCache.clear(); } } }), ]); if (parent.isContainer) { this._parentChangedDispose.push( this.onDataChanged(() => { parent._localID++; parent.worldCache.clear(); // TODO 触发fireChange会死循环 }) ); } } this._parent = parent; this.fireChanged(); } } /** * 判断矩形碰撞 */ intersects(rect: Rectangle): boolean { if ( !this.isContainer && (this.size.width === 0 || this.size.height === 0) ) { return false; } return Rectangle.intersectsWithRotation( this.boundsWithoutRotation, this.worldRotation, rect, 0 ); } /** * 全局的scale */ get worldScale(): ScaleSchema { return this.getFromWorldCache('worldScale', () => { const parent = this.parent; const parentScale = parent ? parent.worldScale : { x: 1, y: 1 }; return { x: this.scale.x * parentScale.x, y: this.scale.y * parentScale.y, }; }); } /** * 全局的rotation */ get worldRotation(): number { return this.getFromWorldCache('worldRotation', () => { const parent = this.parent; if (parent) { return Angle.wrap(this.rotation + parent.worldRotation); } else { return Angle.wrap(this.rotation); } }); } /** * 全局的角度 */ get worldDegree(): number { return this.getFromWorldCache('worldDegree', () => Math.round(this.worldRotation * RAD_TO_DEG) ); } /** * 全局的原点位置 */ get worldOrigin(): PositionSchema { return this.getFromWorldCache('worldOrigin', () => { const matrix = this.worldTransform; return matrix.apply({ x: this.origin.x * this.size.width, y: this.origin.y * this.size.height, }); }); } isParentTransform(parent?: TransformData): boolean { let currentParent = this.parent; while (currentParent) { if (currentParent === parent) return true; currentParent = currentParent.parent; } return false; } /** * 宽转换成scale,用于图片等无法修改大小的场景 * @param width * @param isWorldSize 是否为绝对大小 */ widthToScaleX(width: number, isWorldSize?: boolean): number { const parentScaleX = isWorldSize && this.parent ? this.parent.worldScale.x : 1; return ( Math.round((width / parentScaleX / this.localSize.width) * floatNum) / floatNum ); } /** * 绝对高转换成scale,用于图片等无法修改大小的场景 * @param worldHeight * @param isWorldSize 是否为绝对大小 */ heightToScaleY(height: number, isWorldSize?: boolean): number { const parentScaleY = isWorldSize && this.parent ? this.parent.worldScale.y : 1; return ( Math.round((height / parentScaleY / this.localSize.height) * floatNum) / floatNum ); } sizeToScaleValue( size: { width: number; height: number }, isWorldSize?: boolean ): { x: number; y: number } { return { x: this.widthToScaleX(size.width, isWorldSize), y: this.heightToScaleY(size.height, isWorldSize), }; } } export namespace TransformData { /** * @param dragableEntities * @param target */ export function isParentOrChildrenTransform( dragableEntities: Entity[], target: Entity ): boolean { const targetTransform = target.getData(TransformData); if (!targetTransform) return false; for (const dragger of dragableEntities.values()) { const draggerTransform = dragger.getData(TransformData); if (!draggerTransform) continue; if ( draggerTransform.isParent(targetTransform) || targetTransform.isParent(draggerTransform) ) { return true; } } return false; } }