Files
workinf_Blender_Wasm/web/protocol/budget.ts
2026-08-12 04:47:48 -04:00

178 lines
7.3 KiB
TypeScript

export type BudgetMetric = "triangles" | "vertices" | "indexBytes" | "materialSlots" | "textureBytes" | "gpuBytes";
export interface LightweightBudget {
schemaVersion: 1;
triangles?: number;
vertices?: number;
indexBytes?: number;
materialSlots?: number;
textureBytes?: number;
gpuBytes?: number;
}
export interface LightweightUsage {
triangles: number;
vertices: number;
indexBytes: number;
materialSlots: number;
textureBytes: number;
gpuBytes: number;
}
export interface BudgetViolation {
metric: BudgetMetric;
limit: number;
actual: number;
excess: number;
}
export interface LightweightBudgetReport {
withinBudget: boolean;
budget: LightweightBudget;
usage: LightweightUsage;
violations: BudgetViolation[];
}
export interface LightweightObjectUsage {
objectId: string;
collectionIds: string[];
usage: LightweightUsage;
}
export interface LightweightLODUsage {
lodId: string;
objectId: string;
usage: LightweightUsage;
}
export interface LightweightUsageAggregation {
project: LightweightUsage;
collections: Record<string, LightweightUsage>;
objects: Record<string, LightweightUsage>;
lod: Record<string, LightweightUsage>;
}
export interface LightweightBudgetScopes {
project?: LightweightBudget;
collections?: Record<string, LightweightBudget>;
objects?: Record<string, LightweightBudget>;
lod?: Record<string, LightweightBudget>;
}
export interface LightweightBudgetAggregationReport {
usage: LightweightUsageAggregation;
project?: LightweightBudgetReport;
collections: Record<string, LightweightBudgetReport>;
objects: Record<string, LightweightBudgetReport>;
lod: Record<string, LightweightBudgetReport>;
}
export class BudgetValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "BudgetValidationError";
}
}
const metrics: BudgetMetric[] = ["triangles", "vertices", "indexBytes", "materialSlots", "textureBytes", "gpuBytes"];
const emptyUsage = (): LightweightUsage => ({ triangles: 0, vertices: 0, indexBytes: 0, materialSlots: 0, textureBytes: 0, gpuBytes: 0 });
function validateUsage(value: LightweightUsage, field: string): LightweightUsage {
for (const metric of metrics) nonNegativeInteger(value[metric], `${field}.${metric}`);
return { ...value };
}
function addUsage(target: LightweightUsage, source: LightweightUsage, field: string): void {
for (const metric of metrics) {
const value = target[metric] + source[metric];
if (!Number.isSafeInteger(value)) throw new BudgetValidationError(`${field}.${metric} exceeds safe integer range`);
target[metric] = value;
}
}
function requireId(value: string, field: string): string {
if (typeof value !== "string" || value.length === 0 || value.length > 256) throw new BudgetValidationError(`${field} must be a non-empty id`);
return value;
}
function nonNegativeInteger(value: unknown, field: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new BudgetValidationError(`${field} must be a non-negative safe integer`);
return value;
}
export function parseLightweightBudget(value: unknown): LightweightBudget {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new BudgetValidationError("budget must be an object");
const input = value as Record<string, unknown>;
if (input.schemaVersion !== 1) throw new BudgetValidationError("Unsupported budget schema");
const budget: LightweightBudget = { schemaVersion: 1 };
for (const metric of metrics) {
if (input[metric] !== undefined) budget[metric] = nonNegativeInteger(input[metric], metric);
}
if (metrics.every((metric) => budget[metric] === undefined)) throw new BudgetValidationError("budget must define at least one metric");
return budget;
}
export function evaluateLightweightBudget(budgetValue: unknown, usageValue: LightweightUsage): LightweightBudgetReport {
const budget = parseLightweightBudget(budgetValue);
validateUsage(usageValue, "usage");
const violations = metrics.flatMap((metric) => {
const limit = budget[metric];
if (limit === undefined || usageValue[metric] <= limit) return [];
return [{ metric, limit, actual: usageValue[metric], excess: usageValue[metric] - limit }];
});
return { withinBudget: violations.length === 0, budget, usage: { ...usageValue }, violations };
}
/** Aggregates source object usage once, collection membership independently, and LOD usage separately. */
export function aggregateLightweightUsage(objects: readonly LightweightObjectUsage[], lod: readonly LightweightLODUsage[] = []): LightweightUsageAggregation {
const project = emptyUsage();
const collections: Record<string, LightweightUsage> = {};
const objectUsage: Record<string, LightweightUsage> = {};
const lodUsage: Record<string, LightweightUsage> = {};
for (const object of objects) {
const objectId = requireId(object.objectId, "object.objectId");
if (objectUsage[objectId]) throw new BudgetValidationError(`duplicate object id: ${objectId}`);
if (!Array.isArray(object.collectionIds)) throw new BudgetValidationError(`object.${objectId}.collectionIds must be an array`);
const usage = validateUsage(object.usage, `object.${objectId}.usage`);
objectUsage[objectId] = usage;
addUsage(project, usage, "project");
const collectionIds = new Set<string>();
for (const collectionIdValue of object.collectionIds) {
const collectionId = requireId(collectionIdValue, "object.collectionIds[]");
if (collectionIds.has(collectionId)) throw new BudgetValidationError(`duplicate collection membership: ${objectId}:${collectionId}`);
collectionIds.add(collectionId);
const collection = collections[collectionId] ?? (collections[collectionId] = emptyUsage());
addUsage(collection, usage, `collection.${collectionId}`);
}
}
for (const item of lod) {
const lodId = requireId(item.lodId, "lod.lodId");
const objectId = requireId(item.objectId, "lod.objectId");
if (!objectUsage[objectId]) throw new BudgetValidationError(`LOD references unknown object: ${objectId}`);
if (lodUsage[lodId]) throw new BudgetValidationError(`duplicate lod id: ${lodId}`);
lodUsage[lodId] = validateUsage(item.usage, `lod.${lodId}.usage`);
}
return { project, collections, objects: objectUsage, lod: lodUsage };
}
export function evaluateLightweightBudgets(scopes: LightweightBudgetScopes, aggregation: LightweightUsageAggregation): LightweightBudgetAggregationReport {
const project = scopes.project ? evaluateLightweightBudget(scopes.project, aggregation.project) : undefined;
const collections: Record<string, LightweightBudgetReport> = {};
for (const [id, budget] of Object.entries(scopes.collections ?? {})) {
const usage = aggregation.collections[id] ?? emptyUsage();
collections[id] = evaluateLightweightBudget(budget, usage);
}
const objects: Record<string, LightweightBudgetReport> = {};
for (const [id, budget] of Object.entries(scopes.objects ?? {})) {
const usage = aggregation.objects[id] ?? emptyUsage();
objects[id] = evaluateLightweightBudget(budget, usage);
}
const lod: Record<string, LightweightBudgetReport> = {};
for (const [id, budget] of Object.entries(scopes.lod ?? {})) {
const usage = aggregation.lod[id] ?? emptyUsage();
lod[id] = evaluateLightweightBudget(budget, usage);
}
return { usage: aggregation, project, collections, objects, lod };
}