fix(snaps): keep winning point snaplines complete
This commit is contained in:
@@ -817,32 +817,7 @@ const filterPointSnapsToNearestCluster = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetSnaps.length < 2) {
|
offsetSnaps.forEach((snap) => keptPointSnaps.add(snap));
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let writeIndex = 0;
|
let writeIndex = 0;
|
||||||
@@ -1127,24 +1102,20 @@ type OuterSnapLineCoordinateRange = {
|
|||||||
max: number;
|
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
|
* Describes which snap source pair produced a rendered point snapline, so later
|
||||||
* filtering can reason about redundancy without mixing unrelated references.
|
* 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 = (
|
const getPointSnapLineSourcePair = (
|
||||||
snap: PointSnap,
|
snap: PointSnap,
|
||||||
): PointSnapLineSourcePair => {
|
): PointSnapLineSourcePair => {
|
||||||
return {
|
return {
|
||||||
snapSourcePairKey: snap.snapSourceIds.join("->"),
|
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 { API } from "./helpers/api";
|
||||||
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import type {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
NormalizedZoomValue,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
type ReferenceSnapPoints = NonNullable<
|
type ReferenceSnapPoints = NonNullable<
|
||||||
ReturnType<typeof SnapCache.getReferenceSnapPoints>
|
ReturnType<typeof SnapCache.getReferenceSnapPoints>
|
||||||
@@ -96,6 +100,27 @@ const getHorizontalPointSnapLineMaxX = (
|
|||||||
return horizontalSnapLine.points[horizontalSnapLine.points.length - 1][0];
|
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 = (
|
const getHorizontalGapSnapLines = (
|
||||||
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
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({
|
const selected = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
id: "selected",
|
id: "selected",
|
||||||
@@ -596,7 +621,7 @@ describe("snapping", () => {
|
|||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(220);
|
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(900);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers a nearby point snap over a slightly better far offset", () => {
|
it("prefers a nearby point snap over a slightly better far offset", () => {
|
||||||
@@ -681,6 +706,46 @@ describe("snapping", () => {
|
|||||||
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(500);
|
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", () => {
|
it("renders gap snaplines when rounded bounds touch the reference gap overlap", () => {
|
||||||
const selected = API.createElement({
|
const selected = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
|
|||||||
Reference in New Issue
Block a user