Files
refboard-ayon/frontend/src/canvas/SpatialGrid.ts
T
Hiren Kangad de418b2ba5 feat: media processing pipeline, spatial indexing, canvas-based video rendering
- Background media worker: polls media_jobs table, runs ffprobe+ffmpeg
  with concurrency limit, generates video posters, emits socket events
- Non-blocking video upload: stores file + enqueues job, returns immediately
- Poster hydration on board load: GET /boards/:id injects poster/dimensions
  from DB into canvas_state video objects
- SpatialGrid: fixed-cell (512px) spatial hash for O(nearby) culling instead
  of O(all) item scanning, eliminates setTimeout violations
- Canvas-based video rendering: draws video frames to offscreen canvas then
  uploads to GPU, completely eliminates GL_INVALID_OPERATION errors from
  PixiJS VideoSource auto-update mechanism
- Server poster upgrade path: culling ticker and applyProcessedMedia() both
  upgrade client-captured posters to server posters when available
- Pause restores server poster (paused video behaves like an image)
- Selection drag-end persistence: onObjectDragEnd broadcasts + saves + undo
- Live media:job:update socket handler patches scene data + VideoSprite
  dimensions without broadcast fanout
2026-03-10 14:30:01 +05:30

141 lines
3.6 KiB
TypeScript

/**
* SpatialGrid — fixed-cell spatial index for fast region queries.
*
* Each item occupies one or more grid cells based on its axis-aligned bounds.
* Query returns all items whose cells overlap the query rectangle.
*
* Cell size should be ~1-2x the median item size for best performance.
* Too small = many cells per item. Too large = too many items per cell.
*/
export interface SpatialEntry<T> {
id: string;
data: T;
x: number;
y: number;
w: number;
h: number;
}
export class SpatialGrid<T> {
private cellSize: number;
private cells: Map<string, Set<string>> = new Map();
private entries: Map<string, SpatialEntry<T>> = new Map();
constructor(cellSize = 512) {
this.cellSize = cellSize;
}
/** Insert or update an entry. */
upsert(id: string, data: T, x: number, y: number, w: number, h: number): void {
// Remove old cells if updating
if (this.entries.has(id)) {
this._removeCells(id);
}
const entry: SpatialEntry<T> = { id, data, x, y, w, h };
this.entries.set(id, entry);
this._insertCells(id, x, y, w, h);
}
/** Remove an entry. */
remove(id: string): void {
if (!this.entries.has(id)) return;
this._removeCells(id);
this.entries.delete(id);
}
/** Query all entries that overlap the given rectangle. */
query(rx: number, ry: number, rw: number, rh: number): SpatialEntry<T>[] {
const seen = new Set<string>();
const results: SpatialEntry<T>[] = [];
const c0 = Math.floor(rx / this.cellSize);
const r0 = Math.floor(ry / this.cellSize);
const c1 = Math.floor((rx + rw) / this.cellSize);
const r1 = Math.floor((ry + rh) / this.cellSize);
for (let r = r0; r <= r1; r++) {
for (let c = c0; c <= c1; c++) {
const key = `${c},${r}`;
const cell = this.cells.get(key);
if (!cell) continue;
for (const id of cell) {
if (seen.has(id)) continue;
seen.add(id);
const entry = this.entries.get(id)!;
// AABB overlap check
if (
entry.x < rx + rw &&
entry.x + entry.w > rx &&
entry.y < ry + rh &&
entry.y + entry.h > ry
) {
results.push(entry);
}
}
}
}
return results;
}
/** Get a specific entry by ID. */
get(id: string): SpatialEntry<T> | undefined {
return this.entries.get(id);
}
/** Number of entries in the index. */
get size(): number {
return this.entries.size;
}
/** Clear the entire index. */
clear(): void {
this.cells.clear();
this.entries.clear();
}
// ---- Internal ----
private _cellKeys(x: number, y: number, w: number, h: number): string[] {
const c0 = Math.floor(x / this.cellSize);
const r0 = Math.floor(y / this.cellSize);
const c1 = Math.floor((x + w) / this.cellSize);
const r1 = Math.floor((y + h) / this.cellSize);
const keys: string[] = [];
for (let r = r0; r <= r1; r++) {
for (let c = c0; c <= c1; c++) {
keys.push(`${c},${r}`);
}
}
return keys;
}
private _insertCells(id: string, x: number, y: number, w: number, h: number): void {
for (const key of this._cellKeys(x, y, w, h)) {
let cell = this.cells.get(key);
if (!cell) {
cell = new Set();
this.cells.set(key, cell);
}
cell.add(id);
}
}
private _removeCells(id: string): void {
const entry = this.entries.get(id);
if (!entry) return;
for (const key of this._cellKeys(entry.x, entry.y, entry.w, entry.h)) {
const cell = this.cells.get(key);
if (cell) {
cell.delete(id);
if (cell.size === 0) this.cells.delete(key);
}
}
}
}