154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
});
|