Files
workinf_Blender_Wasm/web/protocol/paint-depth-visibility.ts
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

158 lines
7.0 KiB
TypeScript

import { PAINT_BUDGET } from "./paint";
export const PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION = 1 as const;
export const PAINT_DEPTH_VISIBILITY_BUDGET = {
maxVertexSamples: PAINT_BUDGET.maxSamples,
maxReadbackBytes: PAINT_BUDGET.maxStrokeBytes,
maxDimension: 4096,
} as const;
export type PaintDepthVisibilityBackend = "MAIN_THREAD_WEBGL2" | "OFFSCREEN_WEBGL2";
export interface PaintDepthVisibilityRequestIR {
schemaVersion: typeof PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION;
objectId: string;
meshId: string;
revision: number;
vertexIndices: number[];
}
export interface PaintDepthVisibilityResultIR extends PaintDepthVisibilityRequestIR {
backend: PaintDepthVisibilityBackend;
source: "GPU_RGBA_DEPTH_READBACK";
width: number;
height: number;
depthReadbackBytes: number;
occluderPixelCount: number;
visibleVertexIndices: number[];
}
export type PaintDepthVisibilityErrorCode =
| "PAINT_SCHEMA_INVALID"
| "PAINT_BUDGET_EXCEEDED"
| "PAINT_DEPTH_UNAVAILABLE"
| "REVISION_CONFLICT";
export class PaintDepthVisibilityError extends Error {
constructor(readonly code: PaintDepthVisibilityErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "PaintDepthVisibilityError";
}
}
const encoder = new TextEncoder();
const REQUEST_FIELDS = new Set(["schemaVersion", "objectId", "meshId", "revision", "vertexIndices"]);
const RESULT_FIELDS = new Set([
...REQUEST_FIELDS,
"backend",
"source",
"width",
"height",
"depthReadbackBytes",
"occluderPixelCount",
"visibleVertexIndices",
]);
function fail(code: PaintDepthVisibilityErrorCode, message: string): never {
throw new PaintDepthVisibilityError(code, message);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) fail("PAINT_SCHEMA_INVALID", `${label} must be an object`);
return value as Record<string, unknown>;
}
function exact(value: Record<string, unknown>, fields: ReadonlySet<string>, label: string): void {
if (Object.keys(value).some((field) => !fields.has(field))) fail("PAINT_SCHEMA_INVALID", `${label} contains undeclared fields`);
}
function identity(value: unknown, prefix: "object:" | "mesh:", label: string): string {
if (typeof value !== "string" || !value.startsWith(prefix) || value.length <= prefix.length || encoder.encode(value).byteLength > 256) {
fail("PAINT_SCHEMA_INVALID", `${label} is not a bounded ${prefix} identity`);
}
return value;
}
function nonNegativeInteger(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("PAINT_SCHEMA_INVALID", `${label} must be a non-negative safe integer`);
return value;
}
function vertexIndices(value: unknown, label: string): number[] {
if (!Array.isArray(value) || value.length === 0) fail("PAINT_SCHEMA_INVALID", `${label} must contain at least one vertex`);
if (value.length > PAINT_DEPTH_VISIBILITY_BUDGET.maxVertexSamples) fail("PAINT_BUDGET_EXCEEDED", `${label} exceeds the depth sample budget`);
const parsed = value.map((item, index) => nonNegativeInteger(item, `${label}[${index}]`));
const unique = new Set(parsed);
if (unique.size !== parsed.length) fail("PAINT_SCHEMA_INVALID", `${label} contains duplicate vertex identities`);
return [...unique].sort((left, right) => left - right);
}
function resultVertexIndices(value: unknown): number[] {
if (!Array.isArray(value)) fail("PAINT_SCHEMA_INVALID", "visibleVertexIndices must be an array");
if (value.length === 0) return [];
return vertexIndices(value, "visibleVertexIndices");
}
export function validatePaintDepthVisibilityRequest(value: unknown, currentRevision: number): PaintDepthVisibilityRequestIR {
const request = record(value, "Paint depth visibility request");
exact(request, REQUEST_FIELDS, "Paint depth visibility request");
if (request.schemaVersion !== PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION) fail("PAINT_SCHEMA_INVALID", "Paint depth visibility schema is unsupported");
const revision = nonNegativeInteger(request.revision, "revision");
if (!Number.isSafeInteger(currentRevision) || currentRevision < 0) fail("PAINT_SCHEMA_INVALID", "current revision is invalid");
if (revision !== currentRevision) fail("REVISION_CONFLICT", "Paint depth visibility request is stale");
return {
schemaVersion: PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION,
objectId: identity(request.objectId, "object:", "objectId"),
meshId: identity(request.meshId, "mesh:", "meshId"),
revision,
vertexIndices: vertexIndices(request.vertexIndices, "vertexIndices"),
};
}
export function validatePaintDepthVisibilityResult(
value: unknown,
requestValue: PaintDepthVisibilityRequestIR,
): PaintDepthVisibilityResultIR {
const result = record(value, "Paint depth visibility result");
exact(result, RESULT_FIELDS, "Paint depth visibility result");
const request = validatePaintDepthVisibilityRequest({
schemaVersion: result.schemaVersion,
objectId: result.objectId,
meshId: result.meshId,
revision: result.revision,
vertexIndices: result.vertexIndices,
}, requestValue.revision);
if (request.objectId !== requestValue.objectId || request.meshId !== requestValue.meshId ||
request.vertexIndices.length !== requestValue.vertexIndices.length ||
request.vertexIndices.some((index, offset) => index !== requestValue.vertexIndices[offset])) {
fail("PAINT_SCHEMA_INVALID", "Paint depth visibility result does not match its request");
}
const backend = result.backend;
if (backend !== "MAIN_THREAD_WEBGL2" && backend !== "OFFSCREEN_WEBGL2") fail("PAINT_SCHEMA_INVALID", "Paint depth backend is invalid");
if (result.source !== "GPU_RGBA_DEPTH_READBACK") fail("PAINT_SCHEMA_INVALID", "Paint depth source is invalid");
const width = nonNegativeInteger(result.width, "width");
const height = nonNegativeInteger(result.height, "height");
if (width < 1 || height < 1 || width > PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension || height > PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension) {
fail("PAINT_BUDGET_EXCEEDED", "Paint depth dimensions exceed the readback budget");
}
const depthReadbackBytes = nonNegativeInteger(result.depthReadbackBytes, "depthReadbackBytes");
if (depthReadbackBytes !== width * height * 4 || depthReadbackBytes > PAINT_DEPTH_VISIBILITY_BUDGET.maxReadbackBytes) {
fail("PAINT_BUDGET_EXCEEDED", "Paint depth readback byte length is invalid");
}
const occluderPixelCount = nonNegativeInteger(result.occluderPixelCount, "occluderPixelCount");
if (occluderPixelCount > width * height) fail("PAINT_SCHEMA_INVALID", "Paint depth occluder count exceeds the target");
const visibleVertexIndices = resultVertexIndices(result.visibleVertexIndices);
const requested = new Set(request.vertexIndices);
if (visibleVertexIndices.some((index) => !requested.has(index))) fail("PAINT_SCHEMA_INVALID", "Paint depth result contains an unrequested vertex");
return {
...request,
backend,
source: "GPU_RGBA_DEPTH_READBACK",
width,
height,
depthReadbackBytes,
occluderPixelCount,
visibleVertexIndices,
};
}