Manager Module Architecture
This document describes the manager-based architecture that now underpins the app. The single monolithic App has been split into focused managers, dramatically improving modularity, testability, and extensibility.
Overview
The manager-based architecture splits the monolithic App class into focused, testable managers:
App (orchestrator/facade)
├── ModelManager # Model storage and metadata
├── FileLoader # Multi-format file loading (Gaussian + FBX)
├── GaussianLoader # High-level GaussianModel creation service
├── ONNXManager # ONNX model lifecycle and inference
├── FBXLoaderManager # FBX mesh file loading
├── CameraManager # Camera and controller management
├── AnimationManager # Dynamic model updates
└── RenderLoop # Frame cycle and rendering coordination
Each manager owns a specific responsibility while App wires them together via dependency injection. Managers communicate through well-defined interfaces and callbacks.
Core managers
ModelManager
Responsibilities: maintain all loaded models and expose a unified interface.
Highlights: - Stores metadata and references. - Tracks visibility, transforms, and capacity limits. - Supports both PLY and ONNX entries. - Generates unique names and accelerates lookups.
Key methods:
class ModelManager {
addModel(model: Omit<ModelEntry, 'id'>): ModelEntry;
removeModel(id: string): boolean;
getModels(): ModelInfo[];
getModelWithPointCloud(type: 'ply' | 'onnx', id?: string): ModelEntry | null;
getFullModels(): ModelEntry[];
getModelsByType(modelType: 'ply' | 'onnx'): ModelEntry[];
getVisibleModels(): ModelEntry[];
getDynamicModels(): ModelEntry[];
setModelVisibility(id: string, visible: boolean): boolean;
getModelCount(): number;
getTotalPoints(): number;
getTotalVisiblePoints(): number;
isAtCapacity(): boolean;
getRemainingCapacity(): number;
clearAllModels(): void;
findModelByName(name: string): ModelEntry | null;
generateUniqueName(baseName: string): string;
hasModelWithName(name: string): boolean;
// Legacy transform helpers
setModelPosition(id: string, x: number, y: number, z: number): boolean;
setModelRotation(id: string, x: number, y: number, z: number): boolean;
setModelScale(id: string, scale: number | [number, number, number]): boolean;
setModelTransform(id: string, transform: Float32Array | number[]): boolean;
getModelPosition(id: string): [number, number, number] | null;
getModelTransform(id: string): Float32Array | null;
}
FileLoader
Responsibilities: load and parse multiple Gaussian formats and FBX files.
Features:
- Uses defaultLoader from IO module internally for unified format handling
- Supports all Gaussian formats: PLY, SPZ, KSplat, SPLAT, SOG, Compressed PLY
- Supports FBX mesh files via FBXLoaderManager
- Automatic format detection via detectGaussianFormat() and isGaussianFormat()
- Supports file and URL based loading with progress callbacks
- Handles blob URLs with format detection
- Validates capacity limits before loading
class FileLoader {
loadFile(file: File, device: GPUDevice): Promise<ModelEntry | null>;
loadSample(filename: string, device: GPUDevice, expectedType?: string): Promise<ModelEntry | null>;
isFileTypeSupported(filename: string): boolean;
getSupportedExtensions(): string[];
getFileType(filename: string): string | null;
getGaussianFormat(filename: string): string | null;
}
Loading Flow:
1. Check model capacity
2. Detect file type (Gaussian, ONNX, FBX)
3. Route to appropriate loader (defaultLoader for Gaussian, FBXLoaderManager for FBX)
4. Convert loaded data to ModelEntry via createGaussianModel()
5. Register with ModelManager
GaussianLoader
Responsibilities: high-level convenience service for creating GaussianModel instances.
Features:
- Wraps FileLoader and ONNXManager for unified API
- Supports all Gaussian formats (PLY, SPZ, KSplat, SPLAT, SOG, Compressed PLY)
- Supports ONNX models
- Auto-detects file format
- Returns GaussianModel instances (Three.js Object3D wrappers)
class GaussianLoader {
createFromGaussian(renderer, modelPath, options?, formatHint?): Promise<GaussianModel>;
createFromPLY(renderer, modelPath, options?): Promise<GaussianModel>;
createFromSPZ(renderer, modelPath, options?): Promise<GaussianModel>;
createFromKSplat(renderer, modelPath, options?): Promise<GaussianModel>;
createFromSplat(renderer, modelPath, options?): Promise<GaussianModel>;
createFromSOG(renderer, modelPath, options?): Promise<GaussianModel>;
createFromONNX(renderer, modelPath, camMat, projMat, options?): Promise<GaussianModel>;
createFromFile(renderer, modelPath, cameraMatrices?, options?, fileType?): Promise<GaussianModel>;
createFromEntry(entry: ModelEntry): GaussianModel;
isFormatSupported(filename: string): boolean;
getSupportedFormats(): string[];
detectFormat(filename: string): string | null;
}
Use Case: Simplifies model loading when working with Three.js integration, as it returns GaussianModel instances that can be added directly to Three.js scenes.
ONNXManager
Responsibilities: manage ONNX model loading and inference.
Features:
- Creates dedicated ONNXGenerator + DynamicPointCloud pairs.
- Detects colour mode/dimension (SH vs RGB).
- Supports static and dynamic inference.
- Manages GPU-only pipelines and resource cleanup.
class ONNXManager {
loadONNXModel(
device: GPUDevice,
modelPath: string,
cameraMatrix: Float32Array,
projectionMatrix: Float32Array,
name?: string,
options?: ONNXLoadOptions,
): Promise<ModelEntry>;
loadONNXFromFile(
device: GPUDevice,
file: File,
cameraMatrix?: Float32Array | null,
projectionMatrix?: Float32Array | null,
): Promise<ModelEntry>;
updateCameraMatrices(
modelName: string,
cameraMatrix: Float32Array,
projectionMatrix: Float32Array,
): Promise<void>;
disposeModel(modelId: string): void;
dispose(): void;
getGenerator(id: string): ONNXGenerator | undefined;
getPointCloud(id: string): DynamicPointCloud | undefined;
hasONNXModels(): boolean;
getONNXModels(): string[];
getONNXPerformanceStats(): { modelCount: number; totalGenerators: number; totalPointClouds: number };
}
CameraManager
Responsibilities: manage camera state and controller switching.
Highlights: - Initialises the camera and updates it each frame. - Toggles orbit/FPS controllers and handles resize events. - Computes orbit centres based on model distributions. - Exposes matrices, viewport, frustum data, and debug snapshots.
class CameraManager {
initCamera(canvas: HTMLCanvasElement): void;
resetCamera(): void;
setupCameraForPointCloud(pc: PointCloud): void;
resize(canvas: HTMLCanvasElement): void;
update(dt: number): void;
switchController(type: 'orbit' | 'fps'): void;
getControllerType(): ControllerType;
getController(): IController;
getCamera(): PerspectiveCamera | null;
getCameraMatrix(): mat4;
getProjectionMatrix(): mat4;
getCameraPosition(): vec3 | null;
getCameraRotation(): quat | null;
setCameraPosition(position: vec3): void;
setCameraRotation(rotation: quat): void;
setOrbitCenter(center: vec3): void;
getOrbitCenter(): vec3 | null;
getViewportInfo(): { width: number; height: number; aspect: number } | null;
getFrustumInfo(): { fov: number; near: number; far: number } | null;
isInitialized(): boolean;
getDebugInfo(): any;
}
AnimationManager
Responsibilities: update dynamic models (typically ONNX-driven).
class AnimationManager {
updateDynamicPointClouds(view: mat4, proj: mat4, time: number): void;
controlDynamicAnimation(action: 'start' | 'pause' | 'resume' | 'stop', speed?: number): void;
setDynamicAnimationTime(time: number): void;
getDynamicPerformanceStats(): Array<{ modelName: string; stats: any }>;
}
RenderLoop
Responsibilities: orchestrate the frame loop and render coordination.
Features:
- Owns requestAnimationFrame lifecycle
- Calculates delta time and tracks FPS
- Manages render state (background color, Gaussian scale)
- Coordinates per-frame updates (camera, animation, rendering)
- Provides performance callbacks (FPS, point count)
class RenderLoop {
init(gpu: WebGPUContext, renderer: GaussianRenderer, canvas: HTMLCanvasElement): void;
start(): void;
stop(): void;
setGaussianScale(scale: number): void;
getGaussianScale(): number;
setBackgroundColor(color: [number, number, number, number]): void;
getBackgroundColor(): [number, number, number, number];
setCallbacks(callbacks: FPSCallbacks): void;
getState(): AppState;
getPerformanceInfo(): object;
getFPS(): number;
getFrameCount(): number;
getLastFrameTime(): number;
getAverageFrameTime(): number;
isRunning(): boolean;
setTargetFPS(fps: number): void;
getTargetFPS(): number;
getDebugInfo(): object;
}
Frame Cycle:
1. Calculate delta time
2. CameraManager.update(dt)
3. AnimationManager.updateDynamicPointClouds(view, proj, time)
4. Update FPS counters
5. GaussianRenderer.prepareMulti(...) / renderMulti(...)
6. Invoke callbacks (FPS, point count)
FBXLoaderManager
Responsibilities: load FBX mesh files and register them with ModelManager.
Features: - Wraps Three.js FBXLoader - Provides progress and error callbacks - Integrates with ModelManager for unified model tracking
class FBXLoaderManager {
loadFromFile(file: File, options?): Promise<ModelEntry>;
loadFromUrl(url: string, options?): Promise<ModelEntry>;
dispose(): void;
}
Note: FBX + WebGPU compatibility is limited; GLTF/GLB is recommended for better WebGPU support.
Data flow
Initialisation
1. DOM / canvas validation (DOMElements)
2. WebGPU init (`initWebGPU_onnx`)
3. GaussianRenderer construction
4. CameraManager initialisation
5. RenderLoop initialisation + start
Event/data flow
Example chains: - File drop →onFileLoad → FileLoader.loadFile → ModelManager.addModel → RenderLoop picks up the new model.
- ONNX load → ONNXManager.loadONNXModel → DynamicPointCloud outputs → preprocess → sort → render.
- Controller switch → CameraManager.switchController → UI updates the active control scheme.
Manager communication
Dependency injection
class App {
constructor() {
this.modelManager = new ModelManager(MAX_MODELS);
this.cameraManager = new CameraManager();
this.animationManager = new AnimationManager(this.modelManager);
this.renderLoop = new RenderLoop(this.modelManager, this.animationManager, this.cameraManager);
this.fileLoader = new FileLoader(this.modelManager, loadingCallbacks);
this.onnxManager = new ONNXManager(this.modelManager);
}
}
Callback contracts
interface LoadingCallbacks {
onProgress: (show: boolean, text?: string, pct?: number) => void;
onError: (msg: string) => void;
}
interface UICallbacks {
onFileLoad(file: File): Promise<void>;
onSampleLoad(filename: string): void;
onResetCamera(): void;
onGaussianScaleChange(scale: number): void;
onBackgroundColorChange(color: [number, number, number, number]): void;
onControllerSwitch(type: 'orbit' | 'fps'): void;
}
Advantages
- Modularity – clear separation of concerns.
- Extensibility – new capabilities can be added by introducing new managers.
- Testability – managers are independently testable with mocked dependencies.
- Performance – responsibilities can be optimised in isolation, reducing contention.
Manager Communication Patterns
Dependency Injection
Managers receive dependencies via constructors:
- FileLoader requires ModelManager and LoadingCallbacks
- AnimationManager requires ModelManager
- RenderLoop requires ModelManager, AnimationManager, and CameraManager
- GaussianLoader requires FileLoader and ONNXManager
Callback Contracts
Managers use callbacks for cross-cutting concerns:
LoadingCallbacks:
interface LoadingCallbacks {
onProgress?: (show: boolean, text?: string, pct?: number) => void;
onError?: (message: string) => void;
}
FPSCallbacks:
interface FPSCallbacks {
onFPSUpdate?: (fps: number) => void;
onPointCountUpdate?: (count: number) => void;
}
Data Flow
- Loading:
FileLoader→ModelManager.addModel()→RenderLooppicks up new model - Dynamic Updates:
ONNXManager→DynamicPointCloud→AnimationManager.update()→RenderLoop - Camera:
CameraManager→RenderLoopuses camera matrices for rendering