Files
2026-07-12 21:26:08 +08:00

116 lines
4.4 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
graphEdgeHoverAnchor,
graphNodeHoverAnchor,
resolveGraphHoverPreviewPosition,
worldPointToScreenPoint
} from "../src/render";
import type { RendererViewport, RendererViewportSize } from "../src/render";
const VIEWPORT_SIZE: RendererViewportSize = { width: 1000, height: 680 };
function assertNear(actual: number, expected: number, message?: string): void {
assert.ok(Math.abs(actual - expected) < 0.001, `${message ?? "number"} expected ${expected}, got ${actual}`);
}
function assertPointNear(actual: { x: number; y: number }, expected: { x: number; y: number }): void {
assertNear(actual.x, expected.x, "x");
assertNear(actual.y, expected.y, "y");
}
describe("graph overlay anchors", () => {
it("positions node hover anchors from projected screen points", () => {
const viewport: RendererViewport = { x: -140, y: 82, scale: 2.25 };
const node = { point: { x: 620, y: 260 } };
assertPointNear(
graphNodeHoverAnchor(node, viewport, VIEWPORT_SIZE),
worldPointToScreenPoint(node.point, viewport, VIEWPORT_SIZE)
);
});
it("keeps node hover anchors tied to expanded world bounds", () => {
const viewport: RendererViewport = { x: 84, y: -36, scale: 1.7 };
const viewportSize: RendererViewportSize = { width: 760, height: 520 };
const worldBounds = { minX: -240, minY: -120, maxX: 1260, maxY: 900, width: 1500, height: 1020 };
const node = { point: { x: -80, y: 820 } };
assertPointNear(
graphNodeHoverAnchor(node, viewport, viewportSize, worldBounds),
worldPointToScreenPoint(node.point, viewport, viewportSize, worldBounds)
);
});
it("reprojects hover anchors when drawer resize changes the graph viewport", () => {
const viewport: RendererViewport = { x: -320, y: -180, scale: 2.4 };
const node = { point: { x: 460, y: 220 } };
const wideSize: RendererViewportSize = { width: 1000, height: 680 };
const drawerSize: RendererViewportSize = { width: 640, height: 680 };
const wideAnchor = graphNodeHoverAnchor(node, viewport, wideSize);
const drawerAnchor = graphNodeHoverAnchor(node, viewport, drawerSize);
assert.notDeepEqual(drawerAnchor, wideAnchor);
assertPointNear(
drawerAnchor,
worldPointToScreenPoint(node.point, viewport, drawerSize)
);
});
it("positions edge hover anchors from the projected midpoint of both endpoints", () => {
const viewport: RendererViewport = { x: 120, y: -64, scale: 1.8 };
const source = { point: { x: 200, y: 190 } };
const target = { point: { x: 760, y: 430 } };
const sourceScreen = worldPointToScreenPoint(source.point, viewport, VIEWPORT_SIZE);
const targetScreen = worldPointToScreenPoint(target.point, viewport, VIEWPORT_SIZE);
assertPointNear(
graphEdgeHoverAnchor({ source, target }, viewport, VIEWPORT_SIZE),
{
x: (sourceScreen.x + targetScreen.x) / 2,
y: (sourceScreen.y + targetScreen.y) / 2
}
);
});
it("positions edge hover anchors through expanded bounds", () => {
const viewport: RendererViewport = { x: 40, y: -88, scale: 1.25 };
const viewportSize: RendererViewportSize = { width: 900, height: 620 };
const worldBounds = { minX: -400, minY: -200, maxX: 1400, maxY: 1040, width: 1800, height: 1240 };
const source = { point: { x: -200, y: 920 } };
const target = { point: { x: 1180, y: -40 } };
const sourceScreen = worldPointToScreenPoint(source.point, viewport, viewportSize, worldBounds);
const targetScreen = worldPointToScreenPoint(target.point, viewport, viewportSize, worldBounds);
assertPointNear(
graphEdgeHoverAnchor({ source, target }, viewport, viewportSize, worldBounds),
{
x: (sourceScreen.x + targetScreen.x) / 2,
y: (sourceScreen.y + targetScreen.y) / 2
}
);
});
it("uses the viewport center when an edge endpoint is unavailable", () => {
assert.deepEqual(
graphEdgeHoverAnchor({ source: null, target: { point: { x: 760, y: 430 } } }, { x: 0, y: 0, scale: 1 }, VIEWPORT_SIZE),
{ x: 500, y: 340 }
);
});
it("keeps preview cards inside the graph viewport", () => {
assert.deepEqual(
resolveGraphHoverPreviewPosition({
anchorScreenPoint: { x: 990, y: 12 },
previewSize: { width: 240, height: 150 },
viewportSize: VIEWPORT_SIZE,
offset: { x: 18, y: -174 },
margin: 12
}),
{ x: 748, y: 12 }
);
});
});