1
0
Fork 0
polaris-task-force/tests/int/tech-tree-edge-routes.int.spec.ts
Z8MB1E d43b2ef658 feat(tech-tree): drag groups, snap to grid, and route edges around nodes
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.
2026-10-03 03:28:27 -04:00

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);
}
});
});