fix(snaps): keep winning point snaplines complete
This commit is contained in:
@@ -817,32 +817,7 @@ const filterPointSnapsToNearestCluster = (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offsetSnaps.length < 2) {
|
||||
offsetSnaps.forEach((snap) => keptPointSnaps.add(snap));
|
||||
continue;
|
||||
}
|
||||
|
||||
const sortedSnaps = offsetSnaps
|
||||
.slice()
|
||||
.sort((a, b) => a.visualDistance - b.visualDistance);
|
||||
|
||||
let keepUntil = sortedSnaps.length;
|
||||
|
||||
// This runs after the winning snap offset is known. A big visual-distance
|
||||
// gap means references after it belong to a separate, farther cluster.
|
||||
for (let i = 1; i < sortedSnaps.length; i++) {
|
||||
const distanceGap =
|
||||
sortedSnaps[i].visualDistance - sortedSnaps[i - 1].visualDistance;
|
||||
|
||||
if (distanceGap > clusterBreakDistance) {
|
||||
keepUntil = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < keepUntil; i++) {
|
||||
keptPointSnaps.add(sortedSnaps[i]);
|
||||
}
|
||||
offsetSnaps.forEach((snap) => keptPointSnaps.add(snap));
|
||||
}
|
||||
|
||||
let writeIndex = 0;
|
||||
@@ -1127,24 +1102,20 @@ type OuterSnapLineCoordinateRange = {
|
||||
max: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Only center-to-center snaps count as center snaplines for redundancy. A
|
||||
* center-to-outer snap still explains an outer edge or midpoint alignment.
|
||||
*/
|
||||
const isCenterToCenterSnap = (snap: PointSnap) => {
|
||||
return snap.pointTypes.every((pointType) => pointType === "center");
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes which snap source pair produced a rendered point snapline, so later
|
||||
* filtering can reason about redundancy without mixing unrelated references.
|
||||
* Only center-to-center snaps count as center snaplines; a center-to-outer snap
|
||||
* still explains an outer edge or midpoint alignment.
|
||||
*/
|
||||
const getPointSnapLineSourcePair = (
|
||||
snap: PointSnap,
|
||||
): PointSnapLineSourcePair => {
|
||||
return {
|
||||
snapSourcePairKey: snap.snapSourceIds.join("->"),
|
||||
pointType: isCenterToCenterSnap(snap) ? "center" : "outer",
|
||||
pointType: snap.pointTypes.every((pointType) => pointType === "center")
|
||||
? "center"
|
||||
: "outer",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
|
||||
import { API } from "./helpers/api";
|
||||
|
||||
import type { AppClassProperties, AppState } from "../types";
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
NormalizedZoomValue,
|
||||
} from "../types";
|
||||
|
||||
type ReferenceSnapPoints = NonNullable<
|
||||
ReturnType<typeof SnapCache.getReferenceSnapPoints>
|
||||
@@ -96,6 +100,27 @@ const getHorizontalPointSnapLineMaxX = (
|
||||
return horizontalSnapLine.points[horizontalSnapLine.points.length - 1][0];
|
||||
};
|
||||
|
||||
const getHorizontalPointSnapLineXRange = (
|
||||
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||
) => {
|
||||
const horizontalSnapLine = snapLines
|
||||
.filter((snapLine) => snapLine.type === "points")
|
||||
.find((snapLine) => {
|
||||
const [firstPoint, lastPoint] = snapLine.points;
|
||||
|
||||
return firstPoint[1] === lastPoint[1];
|
||||
});
|
||||
|
||||
if (!horizontalSnapLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
horizontalSnapLine.points[0][0],
|
||||
horizontalSnapLine.points[horizontalSnapLine.points.length - 1][0],
|
||||
] as const;
|
||||
};
|
||||
|
||||
const getHorizontalGapSnapLines = (
|
||||
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||
) => {
|
||||
@@ -561,7 +586,7 @@ describe("snapping", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers the nearest visual cluster for same-offset point snaps", () => {
|
||||
it("keeps same-offset point snaps even across distant references", () => {
|
||||
const selected = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "selected",
|
||||
@@ -596,7 +621,7 @@ describe("snapping", () => {
|
||||
arrayToMap(elements),
|
||||
);
|
||||
|
||||
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(220);
|
||||
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(900);
|
||||
});
|
||||
|
||||
it("prefers a nearby point snap over a slightly better far offset", () => {
|
||||
@@ -681,6 +706,46 @@ describe("snapping", () => {
|
||||
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(500);
|
||||
});
|
||||
|
||||
it("keeps same-source same-offset point snaps across zoom-scaled cluster breaks", () => {
|
||||
const reference = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "reference",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 140.1015625,
|
||||
height: 140.1015625,
|
||||
});
|
||||
const selected = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "selected",
|
||||
x: 338.608871112217,
|
||||
y: 0,
|
||||
width: 140.1015625,
|
||||
height: 140.1015625,
|
||||
});
|
||||
const elements = [reference, selected];
|
||||
const app = createSnappingApp({
|
||||
selectedElementIds: { [selected.id]: true },
|
||||
zoom: { value: 1.5 as NormalizedZoomValue },
|
||||
});
|
||||
|
||||
primeReferenceSnapPoints(elements, [selected]);
|
||||
|
||||
const { snapLines } = snapDraggedElements(
|
||||
elements,
|
||||
{ x: 0, y: 0 },
|
||||
app,
|
||||
NO_MODIFIER_KEYS,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
|
||||
const range = getHorizontalPointSnapLineXRange(snapLines);
|
||||
|
||||
expect(range).not.toBe(null);
|
||||
expect(range![0]).toBeCloseTo(reference.x, 6);
|
||||
expect(range![1]).toBeCloseTo(selected.x + selected.width, 6);
|
||||
});
|
||||
|
||||
it("renders gap snaplines when rounded bounds touch the reference gap overlap", () => {
|
||||
const selected = API.createElement({
|
||||
type: "rectangle",
|
||||
|
||||
Reference in New Issue
Block a user