import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ErrorCode } from "./error"; export const PAINT_PBVH_CAPABILITY_SCHEMA = 1 as const; export const PAINT_PBVH_WASM_ENTRYPOINT = "_web_engine_apply_pbvh_stroke" as const; export type PaintPBVHDomain = "SCULPT" | "VERTEX_COLOR" | "WEIGHT" | "TEXTURE"; const BRUSH_INVENTORY = { SCULPT: [ "DRAW", "SMOOTH", "PINCH", "INFLATE", "GRAB", "LAYER", "CLAY", "NUDGE", "THUMB", "SNAKE_HOOK", "ROTATE", "SIMPLIFY", "CREASE", "BLOB", "CLAY_STRIPS", "MASK", "DRAW_SHARP", "ELASTIC_DEFORM", "POSE", "MULTIPLANE_SCRAPE", "SLIDE_RELAX", "CLAY_THUMB", "CLOTH", "DRAW_FACE_SETS", "PAINT", "SMEAR", "BOUNDARY", "DISPLACEMENT_ERASER", "DISPLACEMENT_SMEAR", "PLANE", "BLUR", "SCENE_PROJECT", ], VERTEX_COLOR: ["DRAW", "BLUR", "AVERAGE", "SMEAR"], WEIGHT: ["DRAW", "BLUR", "AVERAGE", "SMEAR"], TEXTURE: ["DRAW", "SOFTEN", "SMEAR", "CLONE", "FILL", "MASK"], } as const satisfies Record; export interface PaintPBVHBrushInventoryEntry { domain: PaintPBVHDomain; brush: string; source: "blender-5.2.0/source/blender/makesdna/DNA_brush_enums.h"; } export interface PaintPBVHCapabilityRequest { schemaVersion: typeof PAINT_PBVH_CAPABILITY_SCHEMA; operation: "PBVH_BRUSH"; domain: PaintPBVHDomain; brush: string; objectId: string; meshId: string; baseRevision: number; } export interface PaintPBVHCapabilityContext { nativeEntrypointPresent: boolean; sessionContextReady: boolean; verifiedBrushes: ReadonlySet; currentRevision?: number; currentObjectId?: string; currentMeshId?: string; } export class PaintPBVHCapabilityError extends Error { readonly code: ErrorCode; readonly path?: string; constructor(code: ErrorCode, message: string, path?: string) { super(message); this.name = "PaintPBVHCapabilityError"; this.code = code; this.path = path; } } function fail(code: ErrorCode, message: string, path?: string): never { throw new PaintPBVHCapabilityError(code, message, path); } function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function stableId(value: unknown, prefix: "object:" | "mesh:", path: string): string { if (typeof value !== "string" || !value.startsWith(prefix) || value.length <= prefix.length || value.length > 512) { fail("PAINT_SCHEMA_INVALID", `${path} must be a bounded ${prefix.slice(0, -1)} stable ID`, path); } return value; } export function paintPBVHBrushInventory(): PaintPBVHBrushInventoryEntry[] { return (Object.entries(BRUSH_INVENTORY) as Array<[PaintPBVHDomain, readonly string[]]>).flatMap(([domain, brushes]) => brushes.map((brush) => ({ domain, brush, source: "blender-5.2.0/source/blender/makesdna/DNA_brush_enums.h" as const, })), ); } export function parsePaintPBVHCapabilityRequest(value: unknown): PaintPBVHCapabilityRequest { if (!record(value)) fail("PAINT_SCHEMA_INVALID", "PBVH capability request must be an object"); const allowed = ["schemaVersion", "operation", "domain", "brush", "objectId", "meshId", "baseRevision"]; const extra = Object.keys(value).find((key) => !allowed.includes(key)); if (extra) fail("PAINT_SCHEMA_INVALID", `PBVH capability request contains unsupported field ${extra}`, extra); if (value.schemaVersion !== PAINT_PBVH_CAPABILITY_SCHEMA) fail("PROTOCOL_MISMATCH", "Unsupported PBVH capability schema", "schemaVersion"); if (value.operation !== "PBVH_BRUSH") fail("PAINT_SCHEMA_INVALID", "operation must be PBVH_BRUSH", "operation"); if (typeof value.domain !== "string" || !(value.domain in BRUSH_INVENTORY)) { fail("PAINT_SCHEMA_INVALID", "domain is not a PBVH paint domain", "domain"); } const domain = value.domain as PaintPBVHDomain; if (typeof value.brush !== "string" || !(BRUSH_INVENTORY[domain] as readonly string[]).includes(value.brush)) { fail("PAINT_PBVH_BRUSH_UNVERIFIED", `Brush ${String(value.brush)} is not in the Blender 5.2 ${domain} inventory`, "brush"); } if (!Number.isSafeInteger(value.baseRevision) || (value.baseRevision as number) < 0) { fail("PAINT_SCHEMA_INVALID", "baseRevision must be a non-negative safe integer", "baseRevision"); } return { schemaVersion: PAINT_PBVH_CAPABILITY_SCHEMA, operation: "PBVH_BRUSH", domain, brush: value.brush, objectId: stableId(value.objectId, "object:", "objectId"), meshId: stableId(value.meshId, "mesh:", "meshId"), baseRevision: value.baseRevision as number, }; } export function gatePaintPBVHCapability( value: unknown, context: PaintPBVHCapabilityContext, ): CapabilityGateResult { const request = parsePaintPBVHCapabilityRequest(value); const capability = `PBVH_${request.domain}_${request.brush}`; if (context.currentRevision !== undefined && request.baseRevision !== context.currentRevision) { return blockedGate("N-017", capability, [ capabilityIssue("REVISION_CONFLICT", "PBVH capability request is stale", "baseRevision"), ]); } if ((context.currentObjectId !== undefined && request.objectId !== context.currentObjectId) || (context.currentMeshId !== undefined && request.meshId !== context.currentMeshId)) { return blockedGate("N-017", capability, [ capabilityIssue("PAINT_SCHEMA_INVALID", "PBVH request does not target the current Mesh object", "objectId"), ]); } if (!context.nativeEntrypointPresent) { return blockedGate("N-017", capability, [ capabilityIssue( "PAINT_PBVH_UNAVAILABLE", `${PAINT_PBVH_WASM_ENTRYPOINT} is not present in the WebEngine WASM build`, "operation", false, ), ]); } if (!context.sessionContextReady) { return blockedGate("N-017", capability, [ capabilityIssue("PAINT_PBVH_CONTEXT_UNAVAILABLE", "The Blender PBVH paint session context is not initialized", "operation"), ]); } if (!context.verifiedBrushes.has(`${request.domain}:${request.brush}`)) { return blockedGate("N-017", capability, [ capabilityIssue("PAINT_PBVH_BRUSH_UNVERIFIED", "This PBVH brush has no desktop/WASM golden", "brush"), ]); } return readyGate("N-017", capability); }