Files
AFFiNE-Mirror/blocksuite/framework/global/src/gfx/model/bound.ts
T
L-Sun 588659ef67 fix(editor): connector target position NaN (#11606)
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
2025-04-10 12:33:24 +00:00

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)
);
}
}