Round out the canvas editing experience. - Dragging one card of a multi-selection now moves and persists the whole selection (React Flow hands the full dragged set to onNodeDragStop). - A category band is draggable by its header, which moves every technology in that band; the band body stays click-through. - A "Snap: On/Off" toolbar toggle (default on, remembered per browser) snaps dragged cards to a 46px grid that divides the column and row steps, so snapped cards line up with the automatic layout. - Connection lines are adjustable: select an edge to reveal "+" handles at each segment's midpoint (click to add a bend), drag bends to reroute, and double-click a bend to remove it. Routes persist per browser. - Edges route around technologies they are not connected to, and corners are rounded, so lines no longer cut through unrelated cards. - Ctrl/Cmd-clicking to multi-select no longer pops the side panel over the canvas.
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { buildRoutePath, ROUTE_CORNER_RADIUS } from "@/components/admin/tech-tree/edgeRoutes";
|
|
import {
|
|
routeAroundObstacles,
|
|
segmentCrossesRect,
|
|
type RouteRect,
|
|
} from "@/components/admin/tech-tree/edgeRouting";
|
|
|
|
describe("tech tree edge route path", () => {
|
|
it("returns nothing for fewer than two points", () => {
|
|
expect(buildRoutePath([])).toBe("");
|
|
expect(buildRoutePath([{ x: 0, y: 0 }])).toBe("");
|
|
});
|
|
|
|
it("draws a straight line for two points", () => {
|
|
expect(buildRoutePath([{ x: 0, y: 0 }, { x: 100, y: 0 }])).toBe("M 0,0 L 100,0");
|
|
});
|
|
|
|
it("rounds each interior corner with a quadratic curve", () => {
|
|
const d = buildRoutePath([
|
|
{ x: 0, y: 0 },
|
|
{ x: 100, y: 0 },
|
|
{ x: 100, y: 100 },
|
|
]);
|
|
expect(d.startsWith("M 0,0")).toBe(true);
|
|
expect(d).toContain("Q 100,0");
|
|
expect(d.endsWith("L 100,100")).toBe(true);
|
|
// The straight run stops short of the corner by the radius.
|
|
expect(d).toContain(`L ${100 - ROUTE_CORNER_RADIUS},0`);
|
|
});
|
|
|
|
it("shrinks the radius on short segments so it never overshoots", () => {
|
|
const d = buildRoutePath([
|
|
{ x: 0, y: 0 },
|
|
{ x: 10, y: 0 },
|
|
{ x: 10, y: 10 },
|
|
]);
|
|
// Half of the 10px segment, not the full default radius.
|
|
expect(d).toContain("L 5,0");
|
|
expect(d).toContain("Q 10,0 10,5");
|
|
});
|
|
});
|
|
|
|
describe("tech tree edge obstacle routing", () => {
|
|
const box = (id: number, x: number, y: number, width: number, height: number): RouteRect => ({
|
|
id,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
});
|
|
|
|
it("leaves a clear path unchanged", () => {
|
|
const points = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 200, y: 0 },
|
|
];
|
|
// The box sits well below the line.
|
|
expect(routeAroundObstacles(points, [box(1, 0, 100, 60, 40)])).toEqual(points);
|
|
});
|
|
|
|
it("returns the input untouched when there are no obstacles", () => {
|
|
const points = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 100, y: 50 },
|
|
];
|
|
expect(routeAroundObstacles(points, [])).toBe(points);
|
|
});
|
|
|
|
it("detours around a box that blocks the segment", () => {
|
|
const obstacle = box(1, 80, -20, 40, 40);
|
|
const points = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 200, y: 0 },
|
|
];
|
|
const routed = routeAroundObstacles(points, [obstacle]);
|
|
expect(routed.length).toBeGreaterThan(2);
|
|
expect(routed[0]).toEqual({ x: 0, y: 0 });
|
|
expect(routed[routed.length - 1]).toEqual({ x: 200, y: 0 });
|
|
// Every resulting segment clears the box.
|
|
for (let i = 0; i < routed.length - 1; i += 1) {
|
|
expect(segmentCrossesRect(routed[i], routed[i + 1], obstacle)).toBe(false);
|
|
}
|
|
});
|
|
});
|