Files
OpenFrontIO/tests/SegmentTrailRaster.test.ts
2026-03-16 00:56:22 +01:00

171 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
locateSegment,
positionAtStep,
stepAtTick,
strokeStepInterval,
} from "../src/client/graphics/layers/SegmentTrailRaster";
function makeGame() {
return {
x(ref: number): number {
return ref % 10;
},
y(ref: number): number {
return Math.floor(ref / 10);
},
};
}
function makePlan() {
return {
startTick: 10,
ticksPerStep: 2,
points: Uint32Array.from([0, 3, 33]), // (0,0)->(3,0)->(3,3)
segmentSteps: Uint32Array.from([3, 3]),
segCumSteps: Uint32Array.from([0, 3, 6]),
};
}
function makeMockCtx() {
const ops: Array<{ op: string; x?: number; y?: number }> = [];
const ctx = {
beginPath() {
ops.push({ op: "beginPath" });
},
moveTo(x: number, y: number) {
ops.push({ op: "moveTo", x, y });
},
lineTo(x: number, y: number) {
ops.push({ op: "lineTo", x, y });
},
stroke() {
ops.push({ op: "stroke" });
},
} as unknown as CanvasRenderingContext2D;
return { ctx, ops };
}
describe("SegmentTrailRaster", () => {
it("stepAtTick clamps before start and after end", () => {
const plan = makePlan();
expect(stepAtTick(plan, 8)).toBe(0);
expect(stepAtTick(plan, 10)).toBe(0);
expect(stepAtTick(plan, 12)).toBe(1);
expect(stepAtTick(plan, 100)).toBe(6);
});
it("locateSegment handles boundaries with end-exclusive segments", () => {
const plan = makePlan();
expect(locateSegment(plan.segCumSteps, 2, 0)).toBe(0);
expect(locateSegment(plan.segCumSteps, 2, 2)).toBe(0);
expect(locateSegment(plan.segCumSteps, 2, 3)).toBe(1);
expect(locateSegment(plan.segCumSteps, 2, 6)).toBe(1);
});
it("positionAtStep matches expected piecewise interpolation", () => {
const plan = makePlan();
const game = makeGame();
expect(positionAtStep(game, plan, 2)).toEqual({ x: 2, y: 0 });
expect(positionAtStep(game, plan, 4)).toEqual({ x: 3, y: 1 });
expect(positionAtStep(game, plan, 6)).toEqual({ x: 3, y: 3 });
});
it("strokeStepInterval draws same-segment interval including first step", () => {
const { ctx, ops } = makeMockCtx();
const plan = makePlan();
const game = makeGame();
const drew = strokeStepInterval(ctx, game, plan, 0, 1);
expect(drew).toBe(true);
expect(ops).toEqual([
{ op: "beginPath" },
{ op: "moveTo", x: 0.5, y: 0.5 },
{ op: "lineTo", x: 1.5, y: 0.5 },
{ op: "stroke" },
]);
});
it("strokeStepInterval crosses corners without skipping boundaries", () => {
const { ctx, ops } = makeMockCtx();
const plan = makePlan();
const game = makeGame();
const drew = strokeStepInterval(ctx, game, plan, 2, 5);
expect(drew).toBe(true);
expect(ops).toEqual([
{ op: "beginPath" },
{ op: "moveTo", x: 2.5, y: 0.5 },
{ op: "lineTo", x: 3.5, y: 0.5 },
{ op: "lineTo", x: 3.5, y: 2.5 },
{ op: "stroke" },
]);
});
it("strokeStepInterval no-ops for empty deltas", () => {
const { ctx, ops } = makeMockCtx();
const plan = makePlan();
const game = makeGame();
expect(strokeStepInterval(ctx, game, plan, 4, 4)).toBe(false);
expect(ops).toEqual([]);
});
it("keeps +0.5 offset even when rounding is disabled", () => {
const { ctx, ops } = makeMockCtx();
const game = {
x(ref: number): number {
return ref === 0 ? 0.2 : 1.7;
},
y(): number {
return 0.4;
},
};
const plan = {
startTick: 0,
ticksPerStep: 1,
points: Uint32Array.from([0, 1]),
segmentSteps: Uint32Array.from([1]),
segCumSteps: Uint32Array.from([0, 1]),
};
expect(strokeStepInterval(ctx, game, plan, 0, 1, false)).toBe(true);
expect(ops).toEqual([
{ op: "beginPath" },
{ op: "moveTo", x: 0.7, y: 0.9 },
{ op: "lineTo", x: 2.2, y: 0.9 },
{ op: "stroke" },
]);
});
it("supports replan-style epoch replay by drawing multiple intervals", () => {
const { ctx, ops } = makeMockCtx();
const game = makeGame();
const epochA = {
startTick: 0,
ticksPerStep: 1,
points: Uint32Array.from([0, 3]),
segmentSteps: Uint32Array.from([3]),
segCumSteps: Uint32Array.from([0, 3]),
};
const epochB = {
startTick: 3,
ticksPerStep: 1,
points: Uint32Array.from([3, 33]),
segmentSteps: Uint32Array.from([3]),
segCumSteps: Uint32Array.from([0, 3]),
};
expect(strokeStepInterval(ctx, game, epochA, 0, 3)).toBe(true);
expect(strokeStepInterval(ctx, game, epochB, 0, 2)).toBe(true);
expect(ops).toEqual([
{ op: "beginPath" },
{ op: "moveTo", x: 0.5, y: 0.5 },
{ op: "lineTo", x: 3.5, y: 0.5 },
{ op: "stroke" },
{ op: "beginPath" },
{ op: "moveTo", x: 3.5, y: 0.5 },
{ op: "lineTo", x: 3.5, y: 2.5 },
{ op: "stroke" },
]);
});
});