wip
This commit is contained in:
commit
d8f2407ddf
44 changed files with 10377 additions and 0 deletions
173
tests/geometry.test.ts
Normal file
173
tests/geometry.test.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { buildGeometry, insetPolygon, signedArea } from '../src/lib/geometry';
|
||||
import type { Point, Room } from '../src/lib/types';
|
||||
|
||||
const rectangle = (last = 3000): Room => ({
|
||||
id: 'r1',
|
||||
name: 'Room',
|
||||
direction: 0,
|
||||
walls: [4000, 3000, 4000, last].map((length, index) => ({
|
||||
id: `w${index}`,
|
||||
length,
|
||||
turn: 90,
|
||||
doors: [],
|
||||
})),
|
||||
});
|
||||
|
||||
function roomFromPoints(vertices: Point[]): Room {
|
||||
const headings = vertices.map((point, index) => {
|
||||
const next = vertices[(index + 1) % vertices.length];
|
||||
return (Math.atan2(next.y - point.y, next.x - point.x) * 180) / Math.PI;
|
||||
});
|
||||
return {
|
||||
id: 'shape',
|
||||
name: 'Shape',
|
||||
direction: 0,
|
||||
walls: vertices.map((point, index) => {
|
||||
const next = vertices[(index + 1) % vertices.length];
|
||||
const turn = ((headings[(index + 1) % headings.length] - headings[index] + 540) % 360) - 180;
|
||||
return {
|
||||
id: `w${index}`,
|
||||
length: Math.hypot(next.x - point.x, next.y - point.y),
|
||||
turn,
|
||||
doors: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('measured room geometry', () => {
|
||||
it('closes a rectangle and ignores the unused final turn', () => {
|
||||
const room = rectangle();
|
||||
room.walls[3].turn = 0;
|
||||
const result = buildGeometry(room, 5);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.vertices).toHaveLength(4);
|
||||
expect(result.area).toBeCloseTo(12_000_000);
|
||||
expect(result.perimeter).toBeCloseTo(14_000);
|
||||
expect(result.finalLengthError).toBeCloseTo(0);
|
||||
expect(result.finalAngleError).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it('predicts the next closing length and signed turn for an unfinished room', () => {
|
||||
const room = rectangle();
|
||||
room.walls.pop();
|
||||
const result = buildGeometry(room, 5);
|
||||
expect(result.closed).toBe(false);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.vertices).toHaveLength(4);
|
||||
expect(result.closingLength).toBeCloseTo(3000);
|
||||
expect(result.closingTurn).toBeCloseTo(90);
|
||||
});
|
||||
|
||||
it('reports the measured final wall mismatch, accepting only residuals inside tolerance', () => {
|
||||
const result = buildGeometry(rectangle(3003), 5);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.gap).toBeCloseTo(3);
|
||||
expect(result.finalLengthError).toBeCloseTo(3);
|
||||
expect(result.edges[3].length).toBeCloseTo(3000);
|
||||
expect(
|
||||
result.issues.some(
|
||||
(issue) => issue.severity === 'warning' && issue.message.includes('remeasure'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(buildGeometry(rectangle(3020), 5).valid).toBe(false);
|
||||
expect(buildGeometry(rectangle(3020), 1e308).closed).toBe(false);
|
||||
expect(buildGeometry(rectangle(), 51).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('reports direction mismatch rather than assigning the residual to a bowed wall', () => {
|
||||
const room = rectangle();
|
||||
room.walls[2].turn = 88;
|
||||
const result = buildGeometry(room, 5);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(Math.abs(result.finalAngleError)).toBeCloseTo(2);
|
||||
expect(result.gap).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('supports a concave fireplace and a slanted outer wall', () => {
|
||||
const room = roomFromPoints([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 5000, y: 0 },
|
||||
{ x: 5000, y: 4000 },
|
||||
{ x: 3000, y: 4000 },
|
||||
{ x: 3000, y: 3400 },
|
||||
{ x: 2000, y: 3400 },
|
||||
{ x: 2000, y: 4000 },
|
||||
{ x: 0, y: 3500 },
|
||||
]);
|
||||
const result = buildGeometry(room, 0.1);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.vertices).toHaveLength(8);
|
||||
expect(result.area).toBeCloseTo(18_900_000);
|
||||
const inset = insetPolygon(result.vertices, 10);
|
||||
expect(inset).toHaveLength(1);
|
||||
expect(Math.abs(signedArea(inset[0]))).toBeLessThan(result.area);
|
||||
});
|
||||
|
||||
it('rejects crossing outlines, invalid lengths and impossible turns', () => {
|
||||
const crossed = roomFromPoints([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 4000, y: 0 },
|
||||
{ x: 0, y: 3000 },
|
||||
{ x: 4000, y: 3000 },
|
||||
]);
|
||||
expect(buildGeometry(crossed, 5).issues.some((issue) => issue.message.includes('cross'))).toBe(
|
||||
true,
|
||||
);
|
||||
const room = rectangle();
|
||||
room.walls[0].length = -1;
|
||||
room.walls[1].turn = 180;
|
||||
expect(buildGeometry(room, 5).valid).toBe(false);
|
||||
room.walls[0].length = 1e308;
|
||||
expect(Number.isFinite(buildGeometry(room, 5).area)).toBe(true);
|
||||
});
|
||||
|
||||
it('validates door boundaries and overlapping openings', () => {
|
||||
const room = rectangle();
|
||||
room.walls[0].doors = [
|
||||
{ id: 'd1', offset: 300, width: 900 },
|
||||
{ id: 'd2', offset: 1000, width: 900 },
|
||||
];
|
||||
expect(buildGeometry(room, 5).issues.some((issue) => issue.message.includes('overlap'))).toBe(
|
||||
true,
|
||||
);
|
||||
room.walls[0].doors = [{ id: 'd1', offset: 3500, width: 900 }];
|
||||
expect(buildGeometry(room, 5).valid).toBe(false);
|
||||
room.walls[0].doors = [{ id: 'd1', offset: 0, width: 900 }];
|
||||
expect(buildGeometry(room, 5).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('insets clockwise and anticlockwise rectangles and detects an erased room', () => {
|
||||
const vertices = buildGeometry(rectangle(), 5).vertices;
|
||||
for (const points of [vertices, [...vertices].reverse()]) {
|
||||
const inset = insetPolygon(points, 10);
|
||||
expect(inset).toHaveLength(1);
|
||||
expect(Math.abs(signedArea(inset[0]))).toBeCloseTo(3980 * 2980);
|
||||
}
|
||||
expect(insetPolygon(vertices, 1600)).toEqual([]);
|
||||
expect(insetPolygon(vertices, 1e308)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns separate floor regions when a narrow connecting passage disappears', () => {
|
||||
const inset = insetPolygon(
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 2000, y: 0 },
|
||||
{ x: 2000, y: 900 },
|
||||
{ x: 3000, y: 900 },
|
||||
{ x: 3000, y: 0 },
|
||||
{ x: 5000, y: 0 },
|
||||
{ x: 5000, y: 2000 },
|
||||
{ x: 3000, y: 2000 },
|
||||
{ x: 3000, y: 1100 },
|
||||
{ x: 2000, y: 1100 },
|
||||
{ x: 2000, y: 2000 },
|
||||
{ x: 0, y: 2000 },
|
||||
],
|
||||
150,
|
||||
);
|
||||
expect(inset).toHaveLength(2);
|
||||
expect(inset.every((polygon) => signedArea(polygon) > 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue