mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 15:16:28 +08:00
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
This commit is contained in:
@@ -12,7 +12,6 @@ import type { IBound, IVec, IVec3 } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
almostEqual,
|
||||
Bound,
|
||||
clamp,
|
||||
getBezierCurveBoundingBox,
|
||||
getBezierParameters,
|
||||
getBoundFromPoints,
|
||||
@@ -85,7 +84,7 @@ export function calculateNearestLocation(
|
||||
) {
|
||||
const { x, y, w, h } = bounds;
|
||||
return locations
|
||||
.map(offset => [x + offset[0] * w, y + offset[1] * h] as IVec)
|
||||
.map<IVec>(offset => [x + offset[0] * w, y + offset[1] * h])
|
||||
.map(point => getPointFromBoundsWithRotation(bounds, point))
|
||||
.reduce(
|
||||
(prev, curr, index) => {
|
||||
@@ -99,7 +98,7 @@ export function calculateNearestLocation(
|
||||
return prev;
|
||||
},
|
||||
[...locations[0]]
|
||||
) as IVec;
|
||||
);
|
||||
}
|
||||
|
||||
function rBound(ele: GfxModel, anti = false): IBound {
|
||||
@@ -139,21 +138,19 @@ export function getAnchors(ele: GfxModel) {
|
||||
const anchors: { point: PointLocation; coord: IVec }[] = [];
|
||||
const rotate = ele.rotate;
|
||||
|
||||
[
|
||||
[bound.center[0], bound.y - offset],
|
||||
[bound.center[0], bound.maxY + offset],
|
||||
[bound.x - offset, bound.center[1]],
|
||||
[bound.maxX + offset, bound.center[1]],
|
||||
]
|
||||
.map(vec =>
|
||||
getPointFromBoundsWithRotation({ ...bound, rotate }, vec as IVec)
|
||||
)
|
||||
(
|
||||
[
|
||||
[bound.center[0], bound.y - offset],
|
||||
[bound.center[0], bound.maxY + offset],
|
||||
[bound.x - offset, bound.center[1]],
|
||||
[bound.maxX + offset, bound.center[1]],
|
||||
] satisfies IVec[]
|
||||
)
|
||||
.map(vec => getPointFromBoundsWithRotation({ ...bound, rotate }, vec))
|
||||
.forEach(vec => {
|
||||
const rst = ele.getLineIntersections(bound.center as IVec, vec as IVec);
|
||||
if (!rst) {
|
||||
console.error(`Failed to get line intersections for ${ele.id}`);
|
||||
return;
|
||||
}
|
||||
const rst = ele.getLineIntersections(bound.center, vec);
|
||||
if (!rst) return;
|
||||
|
||||
const originPoint = getPointFromBoundsWithRotation(
|
||||
{ ...bound, rotate: -rotate },
|
||||
rst[0]
|
||||
@@ -164,7 +161,7 @@ export function getAnchors(ele: GfxModel) {
|
||||
}
|
||||
|
||||
function getConnectableRelativePosition(connectable: GfxModel, position: IVec) {
|
||||
const location = connectable.getRelativePointLocation(position as IVec);
|
||||
const location = connectable.getRelativePointLocation(position);
|
||||
if (isVecZero(Vec.sub(position, [0, 0.5])))
|
||||
location.tangent = Vec.rot([0, -1], toRadian(connectable.rotate));
|
||||
else if (isVecZero(Vec.sub(position, [1, 0.5])))
|
||||
@@ -184,7 +181,11 @@ export function getNearestConnectableAnchor(ele: Connectable, point: IVec) {
|
||||
);
|
||||
}
|
||||
|
||||
function closestPoint(points: PointLocation[], point: IVec) {
|
||||
function closestPoint(
|
||||
points: PointLocation[],
|
||||
point: IVec
|
||||
): PointLocation | null {
|
||||
if (points.length === 0) return null;
|
||||
const rst = points.map(p => ({ p, d: Vec.dist(p, point) }));
|
||||
rst.sort((a, b) => a.d - b.d);
|
||||
return rst[0].p;
|
||||
@@ -245,7 +246,7 @@ function filterConnectablePoints<T extends IVec3 | IVec>(
|
||||
): T[] {
|
||||
return points.filter(point => {
|
||||
if (!bound) return true;
|
||||
return !bound.isPointInBound(point as IVec);
|
||||
return !bound.isPointInBound([point[0], point[1]]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -368,15 +369,17 @@ function pushGapMidPoint(
|
||||
bound.lowerLine,
|
||||
bound2.upperLine,
|
||||
bound2.lowerLine,
|
||||
].map(line => {
|
||||
return lineIntersects(
|
||||
point as unknown as IVec,
|
||||
[point[0], point[1] + 1],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
) as IVec;
|
||||
});
|
||||
]
|
||||
.map(line => {
|
||||
return lineIntersects(
|
||||
[point[0], point[1]],
|
||||
[point[0], point[1] + 1],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
);
|
||||
})
|
||||
.filter(p => p !== null);
|
||||
rst.sort((a, b) => a[1] - b[1]);
|
||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||
pushWithPriority(points, [midPoint], 6);
|
||||
@@ -399,15 +402,17 @@ function pushGapMidPoint(
|
||||
bound.rightLine,
|
||||
bound2.leftLine,
|
||||
bound2.rightLine,
|
||||
].map(line => {
|
||||
return lineIntersects(
|
||||
point as unknown as IVec,
|
||||
[point[0] + 1, point[1]],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
) as IVec;
|
||||
});
|
||||
]
|
||||
.map(line => {
|
||||
return lineIntersects(
|
||||
[point[0], point[1]],
|
||||
[point[0] + 1, point[1]],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
);
|
||||
})
|
||||
.filter(p => p !== null);
|
||||
rst.sort((a, b) => a[0] - b[0]);
|
||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||
pushWithPriority(points, [midPoint], 6);
|
||||
@@ -480,14 +485,14 @@ function getConnectablePoints(
|
||||
expandEndBound: Bound | null
|
||||
) {
|
||||
const lineBound = Bound.fromPoints([
|
||||
startPoint,
|
||||
endPoint,
|
||||
] as unknown[] as IVec[]);
|
||||
[startPoint[0], startPoint[1]],
|
||||
[endPoint[0], endPoint[1]],
|
||||
]);
|
||||
const outerBound =
|
||||
expandStartBound &&
|
||||
expandEndBound &&
|
||||
expandStartBound.unite(expandEndBound);
|
||||
let points = [nextStartPoint, lastEndPoint] as IVec3[];
|
||||
let points = [nextStartPoint, lastEndPoint];
|
||||
pushWithPriority(points, lineBound.getVerticesAndMidpoints());
|
||||
|
||||
if (!startBound || !endBound) {
|
||||
@@ -534,7 +539,7 @@ function getConnectablePoints(
|
||||
pushWithPriority(points, expandStartBound.getVerticesAndMidpoints());
|
||||
pushWithPriority(
|
||||
points,
|
||||
expandStartBound.include(lastEndPoint as unknown as IVec).points
|
||||
expandStartBound.include([lastEndPoint[0], lastEndPoint[1]]).points
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,7 +547,7 @@ function getConnectablePoints(
|
||||
pushWithPriority(points, expandEndBound.getVerticesAndMidpoints());
|
||||
pushWithPriority(
|
||||
points,
|
||||
expandEndBound.include(nextStartPoint as unknown as IVec).points
|
||||
expandEndBound.include([nextStartPoint[0], nextStartPoint[1]]).points
|
||||
);
|
||||
}
|
||||
|
||||
@@ -561,7 +566,7 @@ function getConnectablePoints(
|
||||
almostEqual(item[0], point[0], 0.02) &&
|
||||
almostEqual(item[1], point[1], 0.02)
|
||||
);
|
||||
}) as IVec3[];
|
||||
});
|
||||
if (!startEnds[0] || !startEnds[1]) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||
@@ -603,7 +608,9 @@ function mergePath(points: IVec[] | IVec3[]) {
|
||||
continue;
|
||||
result.push([cur[0], cur[1]]);
|
||||
}
|
||||
result.push(last(points as IVec[]) as IVec);
|
||||
if (points.length !== 0) {
|
||||
result.push([points[points.length - 1][0], points[points.length - 1][1]]);
|
||||
}
|
||||
for (let i = 0; i < result.length - 1; i++) {
|
||||
const cur = result[i];
|
||||
const next = result[i + 1];
|
||||
@@ -687,7 +694,7 @@ function getNextPoint(
|
||||
offsetW = 10,
|
||||
offsetH = 10
|
||||
) {
|
||||
const result: IVec = Array.from(point) as IVec;
|
||||
const result: IVec = [point[0], point[1]];
|
||||
if (almostEqual(bound.x, result[0])) result[0] -= offsetX;
|
||||
else if (almostEqual(bound.y, result[1])) result[1] -= offsetY;
|
||||
else if (almostEqual(bound.maxX, result[0])) result[0] += offsetW;
|
||||
@@ -993,7 +1000,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
this.highlightPoint = anchor.point;
|
||||
result = {
|
||||
id: connectable.id,
|
||||
position: anchor.coord as IVec,
|
||||
position: anchor.coord,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1001,7 +1008,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
if (shortestDistance < 8 && result) break;
|
||||
|
||||
// if not, check if closes to bound
|
||||
const nearestPoint = connectable.getNearestPoint(point as IVec) as IVec;
|
||||
const nearestPoint = connectable.getNearestPoint(point);
|
||||
|
||||
if (Vec.dist(nearestPoint, point) < 8) {
|
||||
this.highlightPoint = nearestPoint;
|
||||
@@ -1013,9 +1020,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
target.push(connectable);
|
||||
result = {
|
||||
id: connectable.id,
|
||||
position: bound
|
||||
.toRelative(originPoint)
|
||||
.map(n => clamp(n, 0, 1)) as IVec,
|
||||
position: Vec.clampV(bound.toRelative(originPoint), 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1048,7 +1053,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
// at last, if not, just return the point
|
||||
if (!result) {
|
||||
result = {
|
||||
position: point as IVec,
|
||||
position: point,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1383,7 +1388,7 @@ export class ConnectorPathGenerator extends PathGenerator {
|
||||
const eb = Bound.deserialize(end.xywh);
|
||||
const startPoint = getNearestConnectableAnchor(start, eb.center);
|
||||
const endPoint = getNearestConnectableAnchor(end, sb.center);
|
||||
return [startPoint, endPoint];
|
||||
return (startPoint && endPoint && [startPoint, endPoint]) ?? [];
|
||||
} else {
|
||||
const endPoint = this._getConnectionPoint(connector, 'target');
|
||||
const startPoint = this._getConnectionPoint(connector, 'source');
|
||||
|
||||
@@ -11,6 +11,24 @@ export function randomSeed(): number {
|
||||
return Math.floor(Math.random() * 2 ** 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection point of two line segments.
|
||||
*
|
||||
* @param sp - Start point of the first line segment [x, y]
|
||||
* @param ep - End point of the first line segment [x, y]
|
||||
* @param sp2 - Start point of the second line segment [x, y]
|
||||
* @param ep2 - End point of the second line segment [x, y]
|
||||
* @param infinite - If true, treats the lines as infinite lines rather than line segments
|
||||
* @returns The intersection point [x, y] if the lines intersect, null if they are parallel or coincident
|
||||
*
|
||||
* @example
|
||||
* const intersection = lineIntersects([0, 0], [2, 2], [0, 2], [2, 0]);
|
||||
* // Returns [1, 1] - the intersection point of the two line segments
|
||||
*
|
||||
* @example
|
||||
* const parallel = lineIntersects([0, 0], [2, 2], [0, 1], [2, 3], true);
|
||||
* // Returns null - the lines are parallel
|
||||
*/
|
||||
export function lineIntersects(
|
||||
sp: IVec,
|
||||
ep: IVec,
|
||||
@@ -45,10 +63,23 @@ export function lineIntersects(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest point on a polygon to a given point.
|
||||
*
|
||||
* @param points - Array of points defining the polygon vertices [x, y][]
|
||||
* @param point - The point to find the nearest point to [x, y]
|
||||
* @returns The nearest point on the polygon to the given point
|
||||
* @throws Error if points array is empty or has less than 2 points
|
||||
*/
|
||||
export function polygonNearestPoint(points: IVec[], point: IVec) {
|
||||
const len = points.length;
|
||||
let rst: IVec;
|
||||
let dis = Infinity;
|
||||
if (len < 2) {
|
||||
throw new Error('Polygon must have at least 2 points');
|
||||
}
|
||||
|
||||
let rst: IVec = points[0]; // Initialize with first point as fallback
|
||||
let dis = Vec.dist(points[0], point);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const p = points[i];
|
||||
const p2 = points[(i + 1) % len];
|
||||
@@ -59,7 +90,7 @@ export function polygonNearestPoint(points: IVec[], point: IVec) {
|
||||
rst = temp;
|
||||
}
|
||||
}
|
||||
return rst!;
|
||||
return rst;
|
||||
}
|
||||
|
||||
export function polygonPointDistance(points: IVec[], point: IVec) {
|
||||
|
||||
@@ -341,8 +341,15 @@ export class Bound implements IBound {
|
||||
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 {
|
||||
return [(x - this.x) / this.w, (y - this.y) / this.h];
|
||||
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 {
|
||||
|
||||
@@ -565,6 +565,8 @@ export class Vec {
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clampV(A: IVec, min: number, max?: number): IVec;
|
||||
|
||||
static clampV(A: number[], min: number): number[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
|
||||
@@ -214,7 +214,12 @@ export class Viewport {
|
||||
* This property is used to calculate the scale of the editor.
|
||||
*/
|
||||
get viewScale() {
|
||||
if (!this._shell || this._cachedOffsetWidth === null) return 1;
|
||||
if (
|
||||
!this._shell ||
|
||||
this._cachedOffsetWidth === null ||
|
||||
this._cachedOffsetWidth === 0
|
||||
)
|
||||
return 1;
|
||||
return this.boundingClientRect.width / this._cachedOffsetWidth;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user