Files
PBandDev 7afb5b351b feat: ship v2 organizer rewrite (#5)
* Add comfy skill

* feat: squash v2 branch
2026-03-23 16:57:00 -05:00

189 lines
6.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { createGridAlgorithm } from "../../src/layout/algorithms/grid";
import type { LayoutNode, LayoutInput } from "../../src/layout/types";
function node(id: string, width = 100, height = 60): LayoutNode {
return { id, width, height };
}
function input(nodes: LayoutNode[]): LayoutInput {
return { nodes, edges: [] };
}
// ---------------------------------------------------------------------------
// Grid layout: row-based
// ---------------------------------------------------------------------------
describe("grid layout (row-based)", () => {
it("[2ROW] with 4 nodes produces 2x2 grid", () => {
const algo = createGridAlgorithm({ rows: 2, gap: 40 });
const nodes = [node("A"), node("B"), node("C"), node("D")];
const result = algo.layout(input(nodes));
// cols = ceil(4/2) = 2
// Row 0: A(0,0), B(0,1)
// Row 1: C(1,0), D(1,1)
const posA = result.positions.get("A")!;
const posB = result.positions.get("B")!;
const posC = result.positions.get("C")!;
const posD = result.positions.get("D")!;
// cellWidth = 100+40 = 140, cellHeight = 60+40 = 100
expect(posA).toEqual({ x: 0, y: 0 });
expect(posB).toEqual({ x: 140, y: 0 });
expect(posC).toEqual({ x: 0, y: 100 });
expect(posD).toEqual({ x: 140, y: 100 });
});
it("[2ROW] with 1 node places at origin", () => {
const algo = createGridAlgorithm({ rows: 2, gap: 40 });
const result = algo.layout(input([node("A")]));
expect(result.positions.get("A")).toEqual({ x: 0, y: 0 });
});
it("[3ROW] with 7 nodes distributes correctly", () => {
const algo = createGridAlgorithm({ rows: 3, gap: 20 });
const nodes = Array.from({ length: 7 }, (_, i) => node(`N${i}`, 80, 50));
const result = algo.layout(input(nodes));
// cols = ceil(7/3) = 3
// Row 0: N0, N1, N2
// Row 1: N3, N4, N5
// Row 2: N6
expect(result.positions.size).toBe(7);
const posN0 = result.positions.get("N0")!;
const posN3 = result.positions.get("N3")!;
const posN6 = result.positions.get("N6")!;
// cellWidth = 80+20 = 100, cellHeight = 50+20 = 70
expect(posN0).toEqual({ x: 0, y: 0 });
expect(posN3).toEqual({ x: 0, y: 70 });
expect(posN6).toEqual({ x: 0, y: 140 });
});
});
// ---------------------------------------------------------------------------
// Grid layout: column-based
// ---------------------------------------------------------------------------
describe("grid layout (column-based)", () => {
it("[3COL] with 9 nodes produces 3x3 grid", () => {
const algo = createGridAlgorithm({ columns: 3, gap: 40 });
const nodes = Array.from({ length: 9 }, (_, i) => node(`N${i}`));
const result = algo.layout(input(nodes));
// rows = ceil(9/3) = 3
// Row 0: N0, N1, N2
// Row 1: N3, N4, N5
// Row 2: N6, N7, N8
expect(result.positions.size).toBe(9);
const posN0 = result.positions.get("N0")!;
const posN4 = result.positions.get("N4")!;
const posN8 = result.positions.get("N8")!;
expect(posN0).toEqual({ x: 0, y: 0 });
expect(posN4).toEqual({ x: 140, y: 100 }); // (row=1, col=1)
expect(posN8).toEqual({ x: 280, y: 200 }); // (row=2, col=2)
});
it("[2COL] with 5 nodes: 3 rows x 2 columns", () => {
const algo = createGridAlgorithm({ columns: 2, gap: 40 });
const nodes = Array.from({ length: 5 }, (_, i) => node(`N${i}`));
const result = algo.layout(input(nodes));
// rows = ceil(5/2) = 3
// Row 0: N0, N1
// Row 1: N2, N3
// Row 2: N4
expect(result.positions.size).toBe(5);
const posN4 = result.positions.get("N4")!;
expect(posN4).toEqual({ x: 0, y: 200 }); // (row=2, col=0)
});
});
// ---------------------------------------------------------------------------
// Grid layout: mixed node sizes
// ---------------------------------------------------------------------------
describe("grid layout with different node sizes", () => {
it("uses max dimensions for uniform cell sizing", () => {
const algo = createGridAlgorithm({ columns: 2, gap: 20 });
const nodes = [
node("small", 60, 30),
node("wide", 150, 30),
node("tall", 60, 100),
node("big", 150, 100),
];
const result = algo.layout(input(nodes));
// maxWidth = 150, maxHeight = 100
// cellWidth = 150+20 = 170, cellHeight = 100+20 = 120
const posSmall = result.positions.get("small")!;
const posWide = result.positions.get("wide")!;
const posTall = result.positions.get("tall")!;
const posBig = result.positions.get("big")!;
// "small" at (0,0): centered in cell → x = (150-60)/2 = 45, y = (100-30)/2 = 35
expect(posSmall).toEqual({ x: 45, y: 35 });
// "wide" at (0,1): x = 170 + (150-150)/2 = 170, y = (100-30)/2 = 35
expect(posWide).toEqual({ x: 170, y: 35 });
// "tall" at (1,0): x = (150-60)/2 = 45, y = 120 + (100-100)/2 = 120
expect(posTall).toEqual({ x: 45, y: 120 });
// "big" at (1,1): x = 170 + 0 = 170, y = 120 + 0 = 120
expect(posBig).toEqual({ x: 170, y: 120 });
});
});
// ---------------------------------------------------------------------------
// Grid layout: determinism
// ---------------------------------------------------------------------------
describe("grid layout determinism", () => {
it("produces identical output for identical input", () => {
const algo = createGridAlgorithm({ rows: 2, gap: 40 });
const nodes = [node("A"), node("B"), node("C"), node("D"), node("E")];
const result1 = algo.layout(input(nodes));
const result2 = algo.layout(input(nodes));
expect(result1.positions.size).toBe(result2.positions.size);
for (const [id, pos1] of result1.positions) {
const pos2 = result2.positions.get(id)!;
expect(pos1.x).toBe(pos2.x);
expect(pos1.y).toBe(pos2.y);
}
});
});
// ---------------------------------------------------------------------------
// Grid layout: default (no rows/columns specified)
// ---------------------------------------------------------------------------
describe("grid layout default (sqrt columns)", () => {
it("uses sqrt(n) columns when neither rows nor columns given", () => {
const algo = createGridAlgorithm({ gap: 40 });
const nodes = Array.from({ length: 9 }, (_, i) => node(`N${i}`));
const result = algo.layout(input(nodes));
// sqrt(9) = 3 cols, 3 rows
expect(result.positions.size).toBe(9);
// N0 at (0,0), N3 at (1,0), N6 at (2,0)
const posN0 = result.positions.get("N0")!;
const posN3 = result.positions.get("N3")!;
const posN6 = result.positions.get("N6")!;
expect(posN0.y).toBe(0);
expect(posN3.y).toBe(100); // row 1
expect(posN6.y).toBe(200); // row 2
});
});