mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +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 {
|
import {
|
||||||
almostEqual,
|
almostEqual,
|
||||||
Bound,
|
Bound,
|
||||||
clamp,
|
|
||||||
getBezierCurveBoundingBox,
|
getBezierCurveBoundingBox,
|
||||||
getBezierParameters,
|
getBezierParameters,
|
||||||
getBoundFromPoints,
|
getBoundFromPoints,
|
||||||
@@ -85,7 +84,7 @@ export function calculateNearestLocation(
|
|||||||
) {
|
) {
|
||||||
const { x, y, w, h } = bounds;
|
const { x, y, w, h } = bounds;
|
||||||
return locations
|
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))
|
.map(point => getPointFromBoundsWithRotation(bounds, point))
|
||||||
.reduce(
|
.reduce(
|
||||||
(prev, curr, index) => {
|
(prev, curr, index) => {
|
||||||
@@ -99,7 +98,7 @@ export function calculateNearestLocation(
|
|||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
[...locations[0]]
|
[...locations[0]]
|
||||||
) as IVec;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rBound(ele: GfxModel, anti = false): IBound {
|
function rBound(ele: GfxModel, anti = false): IBound {
|
||||||
@@ -139,21 +138,19 @@ export function getAnchors(ele: GfxModel) {
|
|||||||
const anchors: { point: PointLocation; coord: IVec }[] = [];
|
const anchors: { point: PointLocation; coord: IVec }[] = [];
|
||||||
const rotate = ele.rotate;
|
const rotate = ele.rotate;
|
||||||
|
|
||||||
[
|
(
|
||||||
[bound.center[0], bound.y - offset],
|
[
|
||||||
[bound.center[0], bound.maxY + offset],
|
[bound.center[0], bound.y - offset],
|
||||||
[bound.x - offset, bound.center[1]],
|
[bound.center[0], bound.maxY + offset],
|
||||||
[bound.maxX + offset, bound.center[1]],
|
[bound.x - offset, bound.center[1]],
|
||||||
]
|
[bound.maxX + offset, bound.center[1]],
|
||||||
.map(vec =>
|
] satisfies IVec[]
|
||||||
getPointFromBoundsWithRotation({ ...bound, rotate }, vec as IVec)
|
)
|
||||||
)
|
.map(vec => getPointFromBoundsWithRotation({ ...bound, rotate }, vec))
|
||||||
.forEach(vec => {
|
.forEach(vec => {
|
||||||
const rst = ele.getLineIntersections(bound.center as IVec, vec as IVec);
|
const rst = ele.getLineIntersections(bound.center, vec);
|
||||||
if (!rst) {
|
if (!rst) return;
|
||||||
console.error(`Failed to get line intersections for ${ele.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const originPoint = getPointFromBoundsWithRotation(
|
const originPoint = getPointFromBoundsWithRotation(
|
||||||
{ ...bound, rotate: -rotate },
|
{ ...bound, rotate: -rotate },
|
||||||
rst[0]
|
rst[0]
|
||||||
@@ -164,7 +161,7 @@ export function getAnchors(ele: GfxModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getConnectableRelativePosition(connectable: GfxModel, position: IVec) {
|
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])))
|
if (isVecZero(Vec.sub(position, [0, 0.5])))
|
||||||
location.tangent = Vec.rot([0, -1], toRadian(connectable.rotate));
|
location.tangent = Vec.rot([0, -1], toRadian(connectable.rotate));
|
||||||
else if (isVecZero(Vec.sub(position, [1, 0.5])))
|
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) }));
|
const rst = points.map(p => ({ p, d: Vec.dist(p, point) }));
|
||||||
rst.sort((a, b) => a.d - b.d);
|
rst.sort((a, b) => a.d - b.d);
|
||||||
return rst[0].p;
|
return rst[0].p;
|
||||||
@@ -245,7 +246,7 @@ function filterConnectablePoints<T extends IVec3 | IVec>(
|
|||||||
): T[] {
|
): T[] {
|
||||||
return points.filter(point => {
|
return points.filter(point => {
|
||||||
if (!bound) return true;
|
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,
|
bound.lowerLine,
|
||||||
bound2.upperLine,
|
bound2.upperLine,
|
||||||
bound2.lowerLine,
|
bound2.lowerLine,
|
||||||
].map(line => {
|
]
|
||||||
return lineIntersects(
|
.map(line => {
|
||||||
point as unknown as IVec,
|
return lineIntersects(
|
||||||
[point[0], point[1] + 1],
|
[point[0], point[1]],
|
||||||
line[0],
|
[point[0], point[1] + 1],
|
||||||
line[1],
|
line[0],
|
||||||
true
|
line[1],
|
||||||
) as IVec;
|
true
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
.filter(p => p !== null);
|
||||||
rst.sort((a, b) => a[1] - b[1]);
|
rst.sort((a, b) => a[1] - b[1]);
|
||||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||||
pushWithPriority(points, [midPoint], 6);
|
pushWithPriority(points, [midPoint], 6);
|
||||||
@@ -399,15 +402,17 @@ function pushGapMidPoint(
|
|||||||
bound.rightLine,
|
bound.rightLine,
|
||||||
bound2.leftLine,
|
bound2.leftLine,
|
||||||
bound2.rightLine,
|
bound2.rightLine,
|
||||||
].map(line => {
|
]
|
||||||
return lineIntersects(
|
.map(line => {
|
||||||
point as unknown as IVec,
|
return lineIntersects(
|
||||||
[point[0] + 1, point[1]],
|
[point[0], point[1]],
|
||||||
line[0],
|
[point[0] + 1, point[1]],
|
||||||
line[1],
|
line[0],
|
||||||
true
|
line[1],
|
||||||
) as IVec;
|
true
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
.filter(p => p !== null);
|
||||||
rst.sort((a, b) => a[0] - b[0]);
|
rst.sort((a, b) => a[0] - b[0]);
|
||||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||||
pushWithPriority(points, [midPoint], 6);
|
pushWithPriority(points, [midPoint], 6);
|
||||||
@@ -480,14 +485,14 @@ function getConnectablePoints(
|
|||||||
expandEndBound: Bound | null
|
expandEndBound: Bound | null
|
||||||
) {
|
) {
|
||||||
const lineBound = Bound.fromPoints([
|
const lineBound = Bound.fromPoints([
|
||||||
startPoint,
|
[startPoint[0], startPoint[1]],
|
||||||
endPoint,
|
[endPoint[0], endPoint[1]],
|
||||||
] as unknown[] as IVec[]);
|
]);
|
||||||
const outerBound =
|
const outerBound =
|
||||||
expandStartBound &&
|
expandStartBound &&
|
||||||
expandEndBound &&
|
expandEndBound &&
|
||||||
expandStartBound.unite(expandEndBound);
|
expandStartBound.unite(expandEndBound);
|
||||||
let points = [nextStartPoint, lastEndPoint] as IVec3[];
|
let points = [nextStartPoint, lastEndPoint];
|
||||||
pushWithPriority(points, lineBound.getVerticesAndMidpoints());
|
pushWithPriority(points, lineBound.getVerticesAndMidpoints());
|
||||||
|
|
||||||
if (!startBound || !endBound) {
|
if (!startBound || !endBound) {
|
||||||
@@ -534,7 +539,7 @@ function getConnectablePoints(
|
|||||||
pushWithPriority(points, expandStartBound.getVerticesAndMidpoints());
|
pushWithPriority(points, expandStartBound.getVerticesAndMidpoints());
|
||||||
pushWithPriority(
|
pushWithPriority(
|
||||||
points,
|
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.getVerticesAndMidpoints());
|
||||||
pushWithPriority(
|
pushWithPriority(
|
||||||
points,
|
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[0], point[0], 0.02) &&
|
||||||
almostEqual(item[1], point[1], 0.02)
|
almostEqual(item[1], point[1], 0.02)
|
||||||
);
|
);
|
||||||
}) as IVec3[];
|
});
|
||||||
if (!startEnds[0] || !startEnds[1]) {
|
if (!startEnds[0] || !startEnds[1]) {
|
||||||
throw new BlockSuiteError(
|
throw new BlockSuiteError(
|
||||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||||
@@ -603,7 +608,9 @@ function mergePath(points: IVec[] | IVec3[]) {
|
|||||||
continue;
|
continue;
|
||||||
result.push([cur[0], cur[1]]);
|
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++) {
|
for (let i = 0; i < result.length - 1; i++) {
|
||||||
const cur = result[i];
|
const cur = result[i];
|
||||||
const next = result[i + 1];
|
const next = result[i + 1];
|
||||||
@@ -687,7 +694,7 @@ function getNextPoint(
|
|||||||
offsetW = 10,
|
offsetW = 10,
|
||||||
offsetH = 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;
|
if (almostEqual(bound.x, result[0])) result[0] -= offsetX;
|
||||||
else if (almostEqual(bound.y, result[1])) result[1] -= offsetY;
|
else if (almostEqual(bound.y, result[1])) result[1] -= offsetY;
|
||||||
else if (almostEqual(bound.maxX, result[0])) result[0] += offsetW;
|
else if (almostEqual(bound.maxX, result[0])) result[0] += offsetW;
|
||||||
@@ -993,7 +1000,7 @@ export class ConnectionOverlay extends Overlay {
|
|||||||
this.highlightPoint = anchor.point;
|
this.highlightPoint = anchor.point;
|
||||||
result = {
|
result = {
|
||||||
id: connectable.id,
|
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 (shortestDistance < 8 && result) break;
|
||||||
|
|
||||||
// if not, check if closes to bound
|
// 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) {
|
if (Vec.dist(nearestPoint, point) < 8) {
|
||||||
this.highlightPoint = nearestPoint;
|
this.highlightPoint = nearestPoint;
|
||||||
@@ -1013,9 +1020,7 @@ export class ConnectionOverlay extends Overlay {
|
|||||||
target.push(connectable);
|
target.push(connectable);
|
||||||
result = {
|
result = {
|
||||||
id: connectable.id,
|
id: connectable.id,
|
||||||
position: bound
|
position: Vec.clampV(bound.toRelative(originPoint), 0, 1),
|
||||||
.toRelative(originPoint)
|
|
||||||
.map(n => clamp(n, 0, 1)) as IVec,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,7 +1053,7 @@ export class ConnectionOverlay extends Overlay {
|
|||||||
// at last, if not, just return the point
|
// at last, if not, just return the point
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
position: point as IVec,
|
position: point,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,7 +1388,7 @@ export class ConnectorPathGenerator extends PathGenerator {
|
|||||||
const eb = Bound.deserialize(end.xywh);
|
const eb = Bound.deserialize(end.xywh);
|
||||||
const startPoint = getNearestConnectableAnchor(start, eb.center);
|
const startPoint = getNearestConnectableAnchor(start, eb.center);
|
||||||
const endPoint = getNearestConnectableAnchor(end, sb.center);
|
const endPoint = getNearestConnectableAnchor(end, sb.center);
|
||||||
return [startPoint, endPoint];
|
return (startPoint && endPoint && [startPoint, endPoint]) ?? [];
|
||||||
} else {
|
} else {
|
||||||
const endPoint = this._getConnectionPoint(connector, 'target');
|
const endPoint = this._getConnectionPoint(connector, 'target');
|
||||||
const startPoint = this._getConnectionPoint(connector, 'source');
|
const startPoint = this._getConnectionPoint(connector, 'source');
|
||||||
|
|||||||
@@ -11,6 +11,24 @@ export function randomSeed(): number {
|
|||||||
return Math.floor(Math.random() * 2 ** 31);
|
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(
|
export function lineIntersects(
|
||||||
sp: IVec,
|
sp: IVec,
|
||||||
ep: IVec,
|
ep: IVec,
|
||||||
@@ -45,10 +63,23 @@ export function lineIntersects(
|
|||||||
return null;
|
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) {
|
export function polygonNearestPoint(points: IVec[], point: IVec) {
|
||||||
const len = points.length;
|
const len = points.length;
|
||||||
let rst: IVec;
|
if (len < 2) {
|
||||||
let dis = Infinity;
|
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++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const p = points[i];
|
const p = points[i];
|
||||||
const p2 = points[(i + 1) % len];
|
const p2 = points[(i + 1) % len];
|
||||||
@@ -59,7 +90,7 @@ export function polygonNearestPoint(points: IVec[], point: IVec) {
|
|||||||
rst = temp;
|
rst = temp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rst!;
|
return rst;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function polygonPointDistance(points: IVec[], point: IVec) {
|
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);
|
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 {
|
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 {
|
toXYWH(): XYWH {
|
||||||
|
|||||||
@@ -565,6 +565,8 @@ export class Vec {
|
|||||||
* @param n
|
* @param n
|
||||||
* @param min
|
* @param min
|
||||||
*/
|
*/
|
||||||
|
static clampV(A: IVec, min: number, max?: number): IVec;
|
||||||
|
|
||||||
static clampV(A: number[], min: number): number[];
|
static clampV(A: number[], min: number): number[];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
// 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.
|
* This property is used to calculate the scale of the editor.
|
||||||
*/
|
*/
|
||||||
get viewScale() {
|
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;
|
return this.boundingClientRect.width / this._cachedOffsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user