Files

154 lines
5.1 KiB
TypeScript
Raw Permalink Normal View History

/**
* SpatialIndex Tests rbush-based spatial search
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { SpatialIndex } from '../src/canvas/SpatialIndex';
import type { CADElement } from '../src/types/cad.types';
function makeElement(id: string, x: number, y: number, width: number, height: number): CADElement {
return {
id,
type: 'rect',
layerId: 'layer-1',
x,
y,
width,
height,
properties: {},
};
}
describe('SpatialIndex', () => {
let index: SpatialIndex;
beforeEach(() => {
index = new SpatialIndex();
});
describe('insert & search', () => {
it('should find an inserted element within its bounding box', () => {
const el = makeElement('el-1', 50, 50, 20, 20);
index.insert(el);
const results = index.search({ minX: 40, minY: 40, maxX: 60, maxY: 60 });
expect(results.length).toBe(1);
expect(results[0].id).toBe('el-1');
});
it('should not find an element outside the search viewport', () => {
const el = makeElement('el-1', 100, 100, 20, 20);
index.insert(el);
const results = index.search({ minX: 0, minY: 0, maxX: 50, maxY: 50 });
expect(results.length).toBe(0);
});
it('should find multiple elements within viewport', () => {
index.insert(makeElement('el-1', 10, 10, 10, 10));
index.insert(makeElement('el-2', 20, 20, 10, 10));
index.insert(makeElement('el-3', 100, 100, 10, 10));
const results = index.search({ minX: 0, minY: 0, maxX: 30, maxY: 30 });
expect(results.length).toBe(2);
const ids = results.map(e => e.id).sort();
expect(ids).toEqual(['el-1', 'el-2']);
});
it('should handle elements at origin', () => {
index.insert(makeElement('el-origin', 0, 0, 10, 10));
const results = index.search({ minX: -5, minY: -5, maxX: 5, maxY: 5 });
expect(results.length).toBe(1);
expect(results[0].id).toBe('el-origin');
});
});
describe('bulkInsert', () => {
it('should bulk insert and find all elements', () => {
const elements = [
makeElement('bulk-1', 10, 10, 5, 5),
makeElement('bulk-2', 20, 20, 5, 5),
makeElement('bulk-3', 30, 30, 5, 5),
makeElement('bulk-4', 200, 200, 5, 5),
];
index.bulkInsert(elements);
const results = index.search({ minX: 0, minY: 0, maxX: 50, maxY: 50 });
expect(results.length).toBe(3);
});
it('should work with empty array', () => {
index.bulkInsert([]);
const results = index.search({ minX: 0, minY: 0, maxX: 100, maxY: 100 });
expect(results.length).toBe(0);
});
});
describe('remove', () => {
it('should remove an element so it is no longer found', () => {
const el = makeElement('el-rm', 50, 50, 10, 10);
index.insert(el);
let results = index.search({ minX: 40, minY: 40, maxX: 60, maxY: 60 });
expect(results.length).toBe(1);
index.remove(el);
results = index.search({ minX: 40, minY: 40, maxX: 60, maxY: 60 });
expect(results.length).toBe(0);
});
it('should remove only the specified element', () => {
const el1 = makeElement('el-keep', 50, 50, 10, 10);
const el2 = makeElement('el-rm', 50, 50, 10, 10);
index.insert(el1);
index.insert(el2);
index.remove(el2);
const results = index.search({ minX: 40, minY: 40, maxX: 60, maxY: 60 });
expect(results.length).toBe(1);
expect(results[0].id).toBe('el-keep');
});
});
describe('clear', () => {
it('should clear all elements from the index', () => {
index.insert(makeElement('el-1', 10, 10, 5, 5));
index.insert(makeElement('el-2', 20, 20, 5, 5));
index.insert(makeElement('el-3', 30, 30, 5, 5));
index.clear();
const results = index.search({ minX: 0, minY: 0, maxX: 100, maxY: 100 });
expect(results.length).toBe(0);
});
});
describe('edge cases', () => {
it('should handle overlapping bounding boxes correctly', () => {
index.insert(makeElement('el-1', 25, 25, 30, 30)); // bbox: 10,10 to 40,40
index.insert(makeElement('el-2', 30, 30, 30, 30)); // bbox: 15,15 to 45,45
const results = index.search({ minX: 10, minY: 10, maxX: 40, maxY: 40 });
expect(results.length).toBe(2);
});
it('should handle zero-size elements', () => {
index.insert(makeElement('el-zero', 50, 50, 0, 0));
const results = index.search({ minX: 49, minY: 49, maxX: 51, maxY: 51 });
expect(results.length).toBe(1);
});
it('should handle negative coordinates', () => {
index.insert(makeElement('el-neg', -50, -50, 20, 20));
const results = index.search({ minX: -60, minY: -60, maxX: -40, maxY: -40 });
expect(results.length).toBe(1);
expect(results[0].id).toBe('el-neg');
});
it('should search with a large viewport containing all elements', () => {
index.insert(makeElement('el-1', 10, 10, 5, 5));
index.insert(makeElement('el-2', 1000, 1000, 5, 5));
const results = index.search({ minX: -10000, minY: -10000, maxX: 10000, maxY: 10000 });
expect(results.length).toBe(2);
});
});
});