mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[feat] Add QuadTree spatial data structure for node indexing
- Implements efficient O(log n) spatial queries for large node graphs - Supports dynamic insertion, removal, and updates - Configurable depth and capacity parameters - Comprehensive test suite covering performance scenarios - Benchmark tools for performance validation - Designed for viewport culling optimization in 100+ node workflows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
195
src/utils/spatial/QuadTree.test.ts
Normal file
195
src/utils/spatial/QuadTree.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { QuadTree, type Bounds } from './QuadTree'
|
||||
|
||||
describe('QuadTree', () => {
|
||||
let quadTree: QuadTree<string>
|
||||
const worldBounds: Bounds = { x: 0, y: 0, width: 1000, height: 1000 }
|
||||
|
||||
beforeEach(() => {
|
||||
quadTree = new QuadTree<string>(worldBounds, {
|
||||
maxDepth: 4,
|
||||
maxItemsPerNode: 4
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertion', () => {
|
||||
it('should insert items within bounds', () => {
|
||||
const success = quadTree.insert('node1',
|
||||
{ x: 100, y: 100, width: 50, height: 50 },
|
||||
'node1'
|
||||
)
|
||||
expect(success).toBe(true)
|
||||
expect(quadTree.size).toBe(1)
|
||||
})
|
||||
|
||||
it('should reject items outside bounds', () => {
|
||||
const success = quadTree.insert('node1',
|
||||
{ x: -100, y: -100, width: 50, height: 50 },
|
||||
'node1'
|
||||
)
|
||||
expect(success).toBe(false)
|
||||
expect(quadTree.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle duplicate IDs by replacing', () => {
|
||||
quadTree.insert('node1', { x: 100, y: 100, width: 50, height: 50 }, 'data1')
|
||||
quadTree.insert('node1', { x: 200, y: 200, width: 50, height: 50 }, 'data2')
|
||||
|
||||
expect(quadTree.size).toBe(1)
|
||||
const results = quadTree.query({ x: 150, y: 150, width: 100, height: 100 })
|
||||
expect(results).toContain('data2')
|
||||
expect(results).not.toContain('data1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('querying', () => {
|
||||
beforeEach(() => {
|
||||
// Insert test nodes in a grid pattern
|
||||
for (let x = 0; x < 10; x++) {
|
||||
for (let y = 0; y < 10; y++) {
|
||||
const id = `node_${x}_${y}`
|
||||
quadTree.insert(id, {
|
||||
x: x * 100,
|
||||
y: y * 100,
|
||||
width: 50,
|
||||
height: 50
|
||||
}, id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should find nodes within query bounds', () => {
|
||||
const results = quadTree.query({ x: 0, y: 0, width: 250, height: 250 })
|
||||
expect(results.length).toBe(9) // 3x3 grid
|
||||
})
|
||||
|
||||
it('should return empty array for out-of-bounds query', () => {
|
||||
const results = quadTree.query({ x: 2000, y: 2000, width: 100, height: 100 })
|
||||
expect(results.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle partial overlaps', () => {
|
||||
const results = quadTree.query({ x: 25, y: 25, width: 100, height: 100 })
|
||||
expect(results.length).toBe(4) // 2x2 grid due to overlap
|
||||
})
|
||||
|
||||
it('should handle large query areas efficiently', () => {
|
||||
const startTime = performance.now()
|
||||
const results = quadTree.query({ x: 0, y: 0, width: 1000, height: 1000 })
|
||||
const queryTime = performance.now() - startTime
|
||||
|
||||
expect(results.length).toBe(100) // All nodes
|
||||
expect(queryTime).toBeLessThan(5) // Should be fast
|
||||
})
|
||||
})
|
||||
|
||||
describe('removal', () => {
|
||||
it('should remove existing items', () => {
|
||||
quadTree.insert('node1', { x: 100, y: 100, width: 50, height: 50 }, 'node1')
|
||||
expect(quadTree.size).toBe(1)
|
||||
|
||||
const success = quadTree.remove('node1')
|
||||
expect(success).toBe(true)
|
||||
expect(quadTree.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle removal of non-existent items', () => {
|
||||
const success = quadTree.remove('nonexistent')
|
||||
expect(success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updating', () => {
|
||||
it('should update item position', () => {
|
||||
quadTree.insert('node1', { x: 100, y: 100, width: 50, height: 50 }, 'node1')
|
||||
|
||||
const success = quadTree.update('node1', { x: 200, y: 200, width: 50, height: 50 })
|
||||
expect(success).toBe(true)
|
||||
|
||||
// Should not find at old position
|
||||
const oldResults = quadTree.query({ x: 75, y: 75, width: 100, height: 100 })
|
||||
expect(oldResults).not.toContain('node1')
|
||||
|
||||
// Should find at new position
|
||||
const newResults = quadTree.query({ x: 175, y: 175, width: 100, height: 100 })
|
||||
expect(newResults).toContain('node1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('subdivision', () => {
|
||||
it('should subdivide when exceeding max items', () => {
|
||||
// Insert 5 items (max is 4) to trigger subdivision
|
||||
for (let i = 0; i < 5; i++) {
|
||||
quadTree.insert(`node${i}`, {
|
||||
x: i * 10,
|
||||
y: i * 10,
|
||||
width: 5,
|
||||
height: 5
|
||||
}, `node${i}`)
|
||||
}
|
||||
|
||||
expect(quadTree.size).toBe(5)
|
||||
|
||||
// Verify all items can still be found
|
||||
const allResults = quadTree.query(worldBounds)
|
||||
expect(allResults.length).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('performance', () => {
|
||||
it('should handle 1000 nodes efficiently', () => {
|
||||
const insertStart = performance.now()
|
||||
|
||||
// Insert 1000 nodes
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const x = Math.random() * 900
|
||||
const y = Math.random() * 900
|
||||
quadTree.insert(`node${i}`, {
|
||||
x,
|
||||
y,
|
||||
width: 50,
|
||||
height: 50
|
||||
}, `node${i}`)
|
||||
}
|
||||
|
||||
const insertTime = performance.now() - insertStart
|
||||
expect(insertTime).toBeLessThan(50) // Should be fast
|
||||
|
||||
// Query performance
|
||||
const queryStart = performance.now()
|
||||
const results = quadTree.query({ x: 400, y: 400, width: 200, height: 200 })
|
||||
const queryTime = performance.now() - queryStart
|
||||
|
||||
expect(queryTime).toBeLessThan(2) // Queries should be very fast
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results.length).toBeLessThan(1000) // Should cull most nodes
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle zero-sized bounds', () => {
|
||||
const success = quadTree.insert('point', { x: 100, y: 100, width: 0, height: 0 }, 'point')
|
||||
expect(success).toBe(true)
|
||||
|
||||
const results = quadTree.query({ x: 99, y: 99, width: 2, height: 2 })
|
||||
expect(results).toContain('point')
|
||||
})
|
||||
|
||||
it('should handle items spanning multiple quadrants', () => {
|
||||
const success = quadTree.insert('large', {
|
||||
x: 400,
|
||||
y: 400,
|
||||
width: 200,
|
||||
height: 200
|
||||
}, 'large')
|
||||
expect(success).toBe(true)
|
||||
|
||||
// Should be found when querying any overlapping quadrant
|
||||
const topLeft = quadTree.query({ x: 0, y: 0, width: 500, height: 500 })
|
||||
const bottomRight = quadTree.query({ x: 500, y: 500, width: 500, height: 500 })
|
||||
|
||||
expect(topLeft).toContain('large')
|
||||
expect(bottomRight).toContain('large')
|
||||
})
|
||||
})
|
||||
})
|
||||
290
src/utils/spatial/QuadTree.ts
Normal file
290
src/utils/spatial/QuadTree.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* QuadTree implementation for spatial indexing of nodes
|
||||
* Optimized for viewport culling in large node graphs
|
||||
*/
|
||||
|
||||
export interface Bounds {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface QuadTreeItem<T> {
|
||||
id: string
|
||||
bounds: Bounds
|
||||
data: T
|
||||
}
|
||||
|
||||
interface QuadTreeOptions {
|
||||
maxDepth?: number
|
||||
maxItemsPerNode?: number
|
||||
minNodeSize?: number
|
||||
}
|
||||
|
||||
class QuadNode<T> {
|
||||
private bounds: Bounds
|
||||
private depth: number
|
||||
private maxDepth: number
|
||||
private maxItems: number
|
||||
private items: QuadTreeItem<T>[] = []
|
||||
private children: QuadNode<T>[] | null = null
|
||||
private divided = false
|
||||
|
||||
constructor(
|
||||
bounds: Bounds,
|
||||
depth: number = 0,
|
||||
maxDepth: number = 5,
|
||||
maxItems: number = 4
|
||||
) {
|
||||
this.bounds = bounds
|
||||
this.depth = depth
|
||||
this.maxDepth = maxDepth
|
||||
this.maxItems = maxItems
|
||||
}
|
||||
|
||||
insert(item: QuadTreeItem<T>): boolean {
|
||||
// Check if item is within bounds
|
||||
if (!this.contains(item.bounds)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we have space and haven't divided, add to this node
|
||||
if (this.items.length < this.maxItems && !this.divided) {
|
||||
this.items.push(item)
|
||||
return true
|
||||
}
|
||||
|
||||
// If we haven't reached max depth, subdivide
|
||||
if (!this.divided && this.depth < this.maxDepth) {
|
||||
this.subdivide()
|
||||
}
|
||||
|
||||
// If divided, insert into children
|
||||
if (this.divided && this.children) {
|
||||
for (const child of this.children) {
|
||||
if (child.insert(item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't subdivide further, add to this node anyway
|
||||
this.items.push(item)
|
||||
return true
|
||||
}
|
||||
|
||||
remove(item: QuadTreeItem<T>): boolean {
|
||||
const index = this.items.findIndex(i => i.id === item.id)
|
||||
if (index !== -1) {
|
||||
this.items.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.divided && this.children) {
|
||||
for (const child of this.children) {
|
||||
if (child.remove(item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
query(searchBounds: Bounds, found: QuadTreeItem<T>[] = []): QuadTreeItem<T>[] {
|
||||
// Check if search area intersects with this node
|
||||
if (!this.intersects(searchBounds)) {
|
||||
return found
|
||||
}
|
||||
|
||||
// Add items in this node that intersect with search bounds
|
||||
for (const item of this.items) {
|
||||
if (this.boundsIntersect(item.bounds, searchBounds)) {
|
||||
found.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search children
|
||||
if (this.divided && this.children) {
|
||||
for (const child of this.children) {
|
||||
child.query(searchBounds, found)
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
private subdivide() {
|
||||
const { x, y, width, height } = this.bounds
|
||||
const halfWidth = width / 2
|
||||
const halfHeight = height / 2
|
||||
|
||||
this.children = [
|
||||
// Top-left
|
||||
new QuadNode<T>(
|
||||
{ x, y, width: halfWidth, height: halfHeight },
|
||||
this.depth + 1,
|
||||
this.maxDepth,
|
||||
this.maxItems
|
||||
),
|
||||
// Top-right
|
||||
new QuadNode<T>(
|
||||
{ x: x + halfWidth, y, width: halfWidth, height: halfHeight },
|
||||
this.depth + 1,
|
||||
this.maxDepth,
|
||||
this.maxItems
|
||||
),
|
||||
// Bottom-left
|
||||
new QuadNode<T>(
|
||||
{ x, y: y + halfHeight, width: halfWidth, height: halfHeight },
|
||||
this.depth + 1,
|
||||
this.maxDepth,
|
||||
this.maxItems
|
||||
),
|
||||
// Bottom-right
|
||||
new QuadNode<T>(
|
||||
{ x: x + halfWidth, y: y + halfHeight, width: halfWidth, height: halfHeight },
|
||||
this.depth + 1,
|
||||
this.maxDepth,
|
||||
this.maxItems
|
||||
)
|
||||
]
|
||||
|
||||
this.divided = true
|
||||
|
||||
// Redistribute existing items to children
|
||||
const itemsToRedistribute = [...this.items]
|
||||
this.items = []
|
||||
|
||||
for (const item of itemsToRedistribute) {
|
||||
let inserted = false
|
||||
for (const child of this.children) {
|
||||
if (child.insert(item)) {
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Keep in parent if it doesn't fit in any child
|
||||
if (!inserted) {
|
||||
this.items.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private contains(itemBounds: Bounds): boolean {
|
||||
return (
|
||||
itemBounds.x >= this.bounds.x &&
|
||||
itemBounds.y >= this.bounds.y &&
|
||||
itemBounds.x + itemBounds.width <= this.bounds.x + this.bounds.width &&
|
||||
itemBounds.y + itemBounds.height <= this.bounds.y + this.bounds.height
|
||||
)
|
||||
}
|
||||
|
||||
private intersects(searchBounds: Bounds): boolean {
|
||||
return this.boundsIntersect(this.bounds, searchBounds)
|
||||
}
|
||||
|
||||
private boundsIntersect(a: Bounds, b: Bounds): boolean {
|
||||
return !(
|
||||
a.x + a.width < b.x ||
|
||||
b.x + b.width < a.x ||
|
||||
a.y + a.height < b.y ||
|
||||
b.y + b.height < a.y
|
||||
)
|
||||
}
|
||||
|
||||
// Debug helper to get tree structure
|
||||
getDebugInfo(): any {
|
||||
return {
|
||||
bounds: this.bounds,
|
||||
depth: this.depth,
|
||||
itemCount: this.items.length,
|
||||
divided: this.divided,
|
||||
children: this.children?.map(child => child.getDebugInfo())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QuadTree<T> {
|
||||
private root: QuadNode<T>
|
||||
private itemMap: Map<string, QuadTreeItem<T>> = new Map()
|
||||
private options: Required<QuadTreeOptions>
|
||||
|
||||
constructor(bounds: Bounds, options: QuadTreeOptions = {}) {
|
||||
this.options = {
|
||||
maxDepth: options.maxDepth ?? 5,
|
||||
maxItemsPerNode: options.maxItemsPerNode ?? 4,
|
||||
minNodeSize: options.minNodeSize ?? 50
|
||||
}
|
||||
|
||||
this.root = new QuadNode<T>(
|
||||
bounds,
|
||||
0,
|
||||
this.options.maxDepth,
|
||||
this.options.maxItemsPerNode
|
||||
)
|
||||
}
|
||||
|
||||
insert(id: string, bounds: Bounds, data: T): boolean {
|
||||
const item: QuadTreeItem<T> = { id, bounds, data }
|
||||
|
||||
// Remove old item if it exists
|
||||
if (this.itemMap.has(id)) {
|
||||
this.remove(id)
|
||||
}
|
||||
|
||||
const success = this.root.insert(item)
|
||||
if (success) {
|
||||
this.itemMap.set(id, item)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
remove(id: string): boolean {
|
||||
const item = this.itemMap.get(id)
|
||||
if (!item) return false
|
||||
|
||||
const success = this.root.remove(item)
|
||||
if (success) {
|
||||
this.itemMap.delete(id)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
update(id: string, newBounds: Bounds): boolean {
|
||||
const item = this.itemMap.get(id)
|
||||
if (!item) return false
|
||||
|
||||
// Remove and re-insert with new bounds
|
||||
const data = item.data
|
||||
this.remove(id)
|
||||
return this.insert(id, newBounds, data)
|
||||
}
|
||||
|
||||
query(searchBounds: Bounds): T[] {
|
||||
const items = this.root.query(searchBounds)
|
||||
return items.map(item => item.data)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.root = new QuadNode<T>(
|
||||
this.root['bounds'],
|
||||
0,
|
||||
this.options.maxDepth,
|
||||
this.options.maxItemsPerNode
|
||||
)
|
||||
this.itemMap.clear()
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.itemMap.size
|
||||
}
|
||||
|
||||
getDebugInfo() {
|
||||
return {
|
||||
size: this.size,
|
||||
tree: this.root.getDebugInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
218
src/utils/spatial/QuadTreeBenchmark.ts
Normal file
218
src/utils/spatial/QuadTreeBenchmark.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Performance benchmark for QuadTree vs linear culling
|
||||
* Measures query performance at different node counts and zoom levels
|
||||
*/
|
||||
import { QuadTree, type Bounds } from './QuadTree'
|
||||
|
||||
export interface BenchmarkResult {
|
||||
nodeCount: number
|
||||
queryCount: number
|
||||
linearTime: number
|
||||
quadTreeTime: number
|
||||
speedup: number
|
||||
culledPercentage: number
|
||||
}
|
||||
|
||||
export interface NodeData {
|
||||
id: string
|
||||
bounds: Bounds
|
||||
}
|
||||
|
||||
export class QuadTreeBenchmark {
|
||||
private worldBounds: Bounds = { x: -5000, y: -5000, width: 10000, height: 10000 }
|
||||
|
||||
// Generate random nodes with realistic clustering
|
||||
generateNodes(count: number): NodeData[] {
|
||||
const nodes: NodeData[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 70% clustered, 30% scattered
|
||||
const isClustered = Math.random() < 0.7
|
||||
|
||||
let x: number, y: number
|
||||
|
||||
if (isClustered) {
|
||||
// Pick a cluster center
|
||||
const clusterX = (Math.random() - 0.5) * 8000
|
||||
const clusterY = (Math.random() - 0.5) * 8000
|
||||
|
||||
// Add node near cluster with gaussian distribution
|
||||
x = clusterX + (Math.random() - 0.5) * 500
|
||||
y = clusterY + (Math.random() - 0.5) * 500
|
||||
} else {
|
||||
// Scattered randomly
|
||||
x = (Math.random() - 0.5) * 9000
|
||||
y = (Math.random() - 0.5) * 9000
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: `node_${i}`,
|
||||
bounds: {
|
||||
x,
|
||||
y,
|
||||
width: 200 + Math.random() * 100,
|
||||
height: 100 + Math.random() * 50
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Linear viewport culling (baseline)
|
||||
linearCulling(nodes: NodeData[], viewport: Bounds): string[] {
|
||||
const visible: string[] = []
|
||||
|
||||
for (const node of nodes) {
|
||||
if (this.boundsIntersect(node.bounds, viewport)) {
|
||||
visible.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return visible
|
||||
}
|
||||
|
||||
// QuadTree viewport culling
|
||||
quadTreeCulling(quadTree: QuadTree<string>, viewport: Bounds): string[] {
|
||||
return quadTree.query(viewport)
|
||||
}
|
||||
|
||||
// Check if two bounds intersect
|
||||
private boundsIntersect(a: Bounds, b: Bounds): boolean {
|
||||
return !(
|
||||
a.x + a.width < b.x ||
|
||||
b.x + b.width < a.x ||
|
||||
a.y + a.height < b.y ||
|
||||
b.y + b.height < a.y
|
||||
)
|
||||
}
|
||||
|
||||
// Run benchmark for specific configuration
|
||||
runBenchmark(
|
||||
nodeCount: number,
|
||||
viewportSize: { width: number; height: number },
|
||||
queryCount: number = 100
|
||||
): BenchmarkResult {
|
||||
// Generate nodes
|
||||
const nodes = this.generateNodes(nodeCount)
|
||||
|
||||
// Build QuadTree
|
||||
const quadTree = new QuadTree<string>(this.worldBounds, {
|
||||
maxDepth: Math.ceil(Math.log2(nodeCount / 4)),
|
||||
maxItemsPerNode: 4
|
||||
})
|
||||
|
||||
for (const node of nodes) {
|
||||
quadTree.insert(node.id, node.bounds, node.id)
|
||||
}
|
||||
|
||||
// Generate random viewports for testing
|
||||
const viewports: Bounds[] = []
|
||||
for (let i = 0; i < queryCount; i++) {
|
||||
const x = (Math.random() - 0.5) * (this.worldBounds.width - viewportSize.width)
|
||||
const y = (Math.random() - 0.5) * (this.worldBounds.height - viewportSize.height)
|
||||
viewports.push({
|
||||
x,
|
||||
y,
|
||||
width: viewportSize.width,
|
||||
height: viewportSize.height
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark linear culling
|
||||
const linearStart = performance.now()
|
||||
let linearVisibleTotal = 0
|
||||
for (const viewport of viewports) {
|
||||
const visible = this.linearCulling(nodes, viewport)
|
||||
linearVisibleTotal += visible.length
|
||||
}
|
||||
const linearTime = performance.now() - linearStart
|
||||
|
||||
// Benchmark QuadTree culling
|
||||
const quadTreeStart = performance.now()
|
||||
let quadTreeVisibleTotal = 0
|
||||
for (const viewport of viewports) {
|
||||
const visible = this.quadTreeCulling(quadTree, viewport)
|
||||
quadTreeVisibleTotal += visible.length
|
||||
}
|
||||
const quadTreeTime = performance.now() - quadTreeStart
|
||||
|
||||
// Calculate metrics
|
||||
const avgVisible = linearVisibleTotal / queryCount
|
||||
const culledPercentage = ((nodeCount - avgVisible) / nodeCount) * 100
|
||||
|
||||
return {
|
||||
nodeCount,
|
||||
queryCount,
|
||||
linearTime,
|
||||
quadTreeTime,
|
||||
speedup: linearTime / quadTreeTime,
|
||||
culledPercentage
|
||||
}
|
||||
}
|
||||
|
||||
// Run comprehensive benchmark suite
|
||||
runBenchmarkSuite(): BenchmarkResult[] {
|
||||
const nodeCounts = [50, 100, 200, 500, 1000, 2000, 5000]
|
||||
const viewportSizes = [
|
||||
{ width: 1920, height: 1080 }, // Full HD
|
||||
{ width: 800, height: 600 }, // Zoomed in
|
||||
{ width: 4000, height: 3000 } // Zoomed out
|
||||
]
|
||||
|
||||
const results: BenchmarkResult[] = []
|
||||
|
||||
for (const nodeCount of nodeCounts) {
|
||||
for (const viewportSize of viewportSizes) {
|
||||
const result = this.runBenchmark(nodeCount, viewportSize)
|
||||
results.push(result)
|
||||
|
||||
console.log(
|
||||
`Nodes: ${nodeCount}, ` +
|
||||
`Viewport: ${viewportSize.width}x${viewportSize.height}, ` +
|
||||
`Linear: ${result.linearTime.toFixed(2)}ms, ` +
|
||||
`QuadTree: ${result.quadTreeTime.toFixed(2)}ms, ` +
|
||||
`Speedup: ${result.speedup.toFixed(2)}x, ` +
|
||||
`Culled: ${result.culledPercentage.toFixed(1)}%`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Find optimal maxDepth for given node count
|
||||
findOptimalDepth(nodeCount: number): number {
|
||||
const nodes = this.generateNodes(nodeCount)
|
||||
const viewport = { x: 0, y: 0, width: 1920, height: 1080 }
|
||||
|
||||
let bestDepth = 1
|
||||
let bestTime = Infinity
|
||||
|
||||
for (let depth = 1; depth <= 10; depth++) {
|
||||
const quadTree = new QuadTree<string>(this.worldBounds, {
|
||||
maxDepth: depth,
|
||||
maxItemsPerNode: 4
|
||||
})
|
||||
|
||||
// Build tree
|
||||
for (const node of nodes) {
|
||||
quadTree.insert(node.id, node.bounds, node.id)
|
||||
}
|
||||
|
||||
// Measure query time
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < 100; i++) {
|
||||
quadTree.query(viewport)
|
||||
}
|
||||
const time = performance.now() - start
|
||||
|
||||
if (time < bestTime) {
|
||||
bestTime = time
|
||||
bestDepth = depth
|
||||
}
|
||||
}
|
||||
|
||||
return bestDepth
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user