mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
588659ef67
Close [BS-3086](https://linear.app/affine-design/issue/BS-3086/frame里套frame,连一下connector,拖两下,白板损坏) ### What Changes - Fixed `bound.toRelative` may be return `NaN` when `bound.w === 0 || bound.h ===0` - Remove type assertions from `connector-manager.ts` for more type safety
374 lines
7.9 KiB
TypeScript
374 lines
7.9 KiB
TypeScript
import { EPSILON, lineIntersects, polygonPointDistance } from '../math.js';
|
|
import type { SerializedXYWH, XYWH } from '../xywh.js';
|
|
import { deserializeXYWH, serializeXYWH } from '../xywh.js';
|
|
import { type IVec, Vec } from './vec.js';
|
|
|
|
export function getIBoundFromPoints(
|
|
points: IVec[],
|
|
rotation = 0
|
|
): IBound & {
|
|
maxX: number;
|
|
maxY: number;
|
|
minX: number;
|
|
minY: number;
|
|
} {
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
|
|
if (points.length < 1) {
|
|
minX = 0;
|
|
minY = 0;
|
|
maxX = 1;
|
|
maxY = 1;
|
|
} else {
|
|
for (const [x, y] of points) {
|
|
minX = Math.min(x, minX);
|
|
minY = Math.min(y, minY);
|
|
maxX = Math.max(x, maxX);
|
|
maxY = Math.max(y, maxY);
|
|
}
|
|
}
|
|
|
|
if (rotation !== 0) {
|
|
return getIBoundFromPoints(
|
|
points.map(pt =>
|
|
Vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
|
|
)
|
|
);
|
|
}
|
|
|
|
return {
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY,
|
|
x: minX,
|
|
y: minY,
|
|
w: maxX - minX,
|
|
h: maxY - minY,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Represents the x, y, width, and height of a block that can be easily accessed.
|
|
*/
|
|
export interface IBound {
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
rotate?: number;
|
|
}
|
|
|
|
export class Bound implements IBound {
|
|
h: number;
|
|
|
|
w: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
get bl(): IVec {
|
|
return [this.x, this.y + this.h];
|
|
}
|
|
|
|
get br(): IVec {
|
|
return [this.x + this.w, this.y + this.h];
|
|
}
|
|
|
|
get center(): IVec {
|
|
return [this.x + this.w / 2, this.y + this.h / 2];
|
|
}
|
|
|
|
set center([cx, cy]: IVec) {
|
|
const [px, py] = this.center;
|
|
this.x += cx - px;
|
|
this.y += cy - py;
|
|
}
|
|
|
|
get horizontalLine(): IVec[] {
|
|
return [
|
|
[this.x, this.y + this.h / 2],
|
|
[this.x + this.w, this.y + this.h / 2],
|
|
];
|
|
}
|
|
|
|
get leftLine(): IVec[] {
|
|
return [
|
|
[this.x, this.y],
|
|
[this.x, this.y + this.h],
|
|
];
|
|
}
|
|
|
|
get lowerLine(): IVec[] {
|
|
return [
|
|
[this.x, this.y + this.h],
|
|
[this.x + this.w, this.y + this.h],
|
|
];
|
|
}
|
|
|
|
get maxX() {
|
|
return this.x + this.w;
|
|
}
|
|
|
|
get maxY() {
|
|
return this.y + this.h;
|
|
}
|
|
|
|
get midPoints(): IVec[] {
|
|
return [
|
|
[this.x + this.w / 2, this.y],
|
|
[this.x + this.w, this.y + this.h / 2],
|
|
[this.x + this.w / 2, this.y + this.h],
|
|
[this.x, this.y + this.h / 2],
|
|
];
|
|
}
|
|
|
|
get minX() {
|
|
return this.x;
|
|
}
|
|
|
|
get minY() {
|
|
return this.y;
|
|
}
|
|
|
|
get points(): IVec[] {
|
|
return [
|
|
[this.x, this.y],
|
|
[this.x + this.w, this.y],
|
|
[this.x + this.w, this.y + this.h],
|
|
[this.x, this.y + this.h],
|
|
];
|
|
}
|
|
|
|
get rightLine(): IVec[] {
|
|
return [
|
|
[this.x + this.w, this.y],
|
|
[this.x + this.w, this.y + this.h],
|
|
];
|
|
}
|
|
|
|
get tl(): IVec {
|
|
return [this.x, this.y];
|
|
}
|
|
|
|
get tr(): IVec {
|
|
return [this.x + this.w, this.y];
|
|
}
|
|
|
|
get upperLine(): IVec[] {
|
|
return [
|
|
[this.x, this.y],
|
|
[this.x + this.w, this.y],
|
|
];
|
|
}
|
|
|
|
get verticalLine(): IVec[] {
|
|
return [
|
|
[this.x + this.w / 2, this.y],
|
|
[this.x + this.w / 2, this.y + this.h],
|
|
];
|
|
}
|
|
|
|
constructor(x = 0, y = 0, w = 0, h = 0) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.w = w;
|
|
this.h = h;
|
|
}
|
|
|
|
static deserialize(s: string) {
|
|
const [x, y, w, h] = deserializeXYWH(s);
|
|
return new Bound(x, y, w, h);
|
|
}
|
|
|
|
static from(arg1: IBound) {
|
|
return new Bound(arg1.x, arg1.y, arg1.w, arg1.h);
|
|
}
|
|
|
|
static fromCenter(center: IVec, width: number, height: number) {
|
|
const [x, y] = center;
|
|
return new Bound(x - width / 2, y - height / 2, width, height);
|
|
}
|
|
|
|
static fromDOMRect({ left, top, width, height }: DOMRect) {
|
|
return new Bound(left, top, width, height);
|
|
}
|
|
|
|
static fromPoints(points: IVec[]) {
|
|
return Bound.from(getIBoundFromPoints(points));
|
|
}
|
|
|
|
static fromXYWH(xywh: XYWH) {
|
|
return new Bound(xywh[0], xywh[1], xywh[2], xywh[3]);
|
|
}
|
|
|
|
static serialize(bound: IBound) {
|
|
return serializeXYWH(bound.x, bound.y, bound.w, bound.h);
|
|
}
|
|
|
|
clone(): Bound {
|
|
return new Bound(this.x, this.y, this.w, this.h);
|
|
}
|
|
|
|
contains(bound: Bound) {
|
|
return (
|
|
bound.x >= this.x &&
|
|
bound.y >= this.y &&
|
|
bound.maxX <= this.maxX &&
|
|
bound.maxY <= this.maxY
|
|
);
|
|
}
|
|
|
|
containsPoint([x, y]: IVec): boolean {
|
|
const { minX, minY, maxX, maxY } = this;
|
|
return minX <= x && x <= maxX && minY <= y && y <= maxY;
|
|
}
|
|
|
|
expand(margin: [number, number]): Bound;
|
|
expand(left: number, top?: number, right?: number, bottom?: number): Bound;
|
|
expand(
|
|
left: number | [number, number],
|
|
top?: number,
|
|
right?: number,
|
|
bottom?: number
|
|
) {
|
|
if (Array.isArray(left)) {
|
|
const [x, y] = left;
|
|
return new Bound(this.x - x, this.y - y, this.w + x * 2, this.h + y * 2);
|
|
}
|
|
|
|
top ??= left;
|
|
right ??= left;
|
|
bottom ??= top;
|
|
|
|
return new Bound(
|
|
this.x - left,
|
|
this.y - top,
|
|
this.w + left + right,
|
|
this.h + top + bottom
|
|
);
|
|
}
|
|
|
|
getRelativePoint([x, y]: IVec): IVec {
|
|
return [this.x + x * this.w, this.y + y * this.h];
|
|
}
|
|
|
|
getVerticesAndMidpoints() {
|
|
return [...this.points, ...this.midPoints];
|
|
}
|
|
|
|
horizontalDistance(bound: Bound) {
|
|
return Math.min(
|
|
Math.abs(this.minX - bound.maxX),
|
|
Math.abs(this.maxX - bound.minX)
|
|
);
|
|
}
|
|
|
|
include(point: IVec) {
|
|
const x1 = Math.min(this.x, point[0]),
|
|
y1 = Math.min(this.y, point[1]),
|
|
x2 = Math.max(this.maxX, point[0]),
|
|
y2 = Math.max(this.maxY, point[1]);
|
|
return new Bound(x1, y1, x2 - x1, y2 - y1);
|
|
}
|
|
|
|
intersectLine(sp: IVec, ep: IVec, infinite = false) {
|
|
const rst: IVec[] = [];
|
|
(
|
|
[
|
|
[this.tl, this.tr],
|
|
[this.tl, this.bl],
|
|
[this.tr, this.br],
|
|
[this.bl, this.br],
|
|
] as IVec[][]
|
|
).forEach(([p1, p2]) => {
|
|
const p = lineIntersects(sp, ep, p1, p2, infinite);
|
|
if (p) rst.push(p);
|
|
});
|
|
return rst.length === 0 ? null : rst;
|
|
}
|
|
|
|
isHorizontalCross(bound: Bound) {
|
|
return !(this.maxY < bound.minY || this.minY > bound.maxY);
|
|
}
|
|
|
|
isIntersectWithBound(bound: Bound, epsilon = EPSILON) {
|
|
return (
|
|
bound.maxX > this.minX - epsilon &&
|
|
bound.maxY > this.minY - epsilon &&
|
|
bound.minX < this.maxX + epsilon &&
|
|
bound.minY < this.maxY + epsilon &&
|
|
!this.contains(bound) &&
|
|
!bound.contains(this)
|
|
);
|
|
}
|
|
|
|
isOverlapWithBound(bound: Bound, epsilon = EPSILON) {
|
|
return (
|
|
bound.maxX > this.minX - epsilon &&
|
|
bound.maxY > this.minY - epsilon &&
|
|
bound.minX < this.maxX + epsilon &&
|
|
bound.minY < this.maxY + epsilon
|
|
);
|
|
}
|
|
|
|
isPointInBound([x, y]: IVec, tolerance = 0.01) {
|
|
return (
|
|
x > this.minX + tolerance &&
|
|
x < this.maxX - tolerance &&
|
|
y > this.minY + tolerance &&
|
|
y < this.maxY - tolerance
|
|
);
|
|
}
|
|
|
|
isPointNearBound([x, y]: IVec, tolerance = 0.01) {
|
|
return polygonPointDistance(this.points, [x, y]) < tolerance;
|
|
}
|
|
|
|
isVerticalCross(bound: Bound) {
|
|
return !(this.maxX < bound.minX || this.minX > bound.maxX);
|
|
}
|
|
|
|
moveDelta(dx: number, dy: number) {
|
|
return new Bound(this.x + dx, this.y + dy, this.w, this.h);
|
|
}
|
|
|
|
serialize(): SerializedXYWH {
|
|
return serializeXYWH(this.x, this.y, this.w, this.h);
|
|
}
|
|
|
|
/**
|
|
* Convert a point to relative coordinates.
|
|
* @param point - The point to convert.
|
|
* @returns The normalized relative coordinates of the point.
|
|
*/
|
|
toRelative([x, y]: IVec): IVec {
|
|
const normalizedX = this.w === 0 ? 0 : (x - this.x) / this.w;
|
|
const normalizedY = this.h === 0 ? 0 : (y - this.y) / this.h;
|
|
return [normalizedX, normalizedY];
|
|
}
|
|
|
|
toXYWH(): XYWH {
|
|
return [this.x, this.y, this.w, this.h];
|
|
}
|
|
|
|
unite(bound: Bound) {
|
|
const x1 = Math.min(this.x, bound.x),
|
|
y1 = Math.min(this.y, bound.y),
|
|
x2 = Math.max(this.maxX, bound.maxX),
|
|
y2 = Math.max(this.maxY, bound.maxY);
|
|
return new Bound(x1, y1, x2 - x1, y2 - y1);
|
|
}
|
|
|
|
verticalDistance(bound: Bound) {
|
|
return Math.min(
|
|
Math.abs(this.minY - bound.maxY),
|
|
Math.abs(this.maxY - bound.minY)
|
|
);
|
|
}
|
|
}
|