import { GEOMETRY_NODE_FIELD_BUDGET, parseGeometryNodeDomainCardinality, type GeometryNodeDomainCardinalityIR, } from "./geometry-nodes"; export interface DepsgraphMeshEvaluationIR { objectId: string; meshId: string; sourceMeshId: string; vertexCount: number; triangleCount: number; modifierCount: number; modifiers: DepsgraphModifierEvaluationIR[]; worldMatrix: number[]; positions: number[]; indices: number[]; domainCardinality?: GeometryNodeDomainCardinalityIR; fieldMaterializations?: Array<{ schemaVersion: 1; fieldId: string; domain: "POINT"; dataType: "FLOAT"; elementCount: number; scalarValueCount: number; materializedByteLength: number; transport: "JSON" | "BINARY_REQUIRED"; errorCode?: "GN_FIELD_JSON_BUDGET_EXCEEDED"; }>; attributes?: Record; } export interface DepsgraphModifierEvaluationIR { uuid: string; index: number; persistentUid: number; typeCode: number; type: string; name: string; showViewport: boolean; showRender: boolean; showEditmode: boolean; showOnCage: boolean; status: "EVALUATED" | "DISABLED" | "BLOCKED"; reason?: string; error?: string; errorCode?: "UNSUPPORTED_MODIFIER_TYPE" | "MODIFIER_TARGET_MISSING" | "BLENDER_MODIFIER_ERROR" | "GEOMETRY_NODES_SIMULATION_UNAVAILABLE" | "GEOMETRY_NODES_EVALUATOR_UNSUPPORTED"; suggestion?: string; targetObjectIds?: string[]; dependsOn?: string[]; } export interface DepsgraphObjectEvaluationIR { objectId: string; type: "MESH" | "CURVE" | "SURFACE" | "FONT" | "METABALL" | "CURVES" | "POINT_CLOUD" | "VOLUME" | "LATTICE" | "ARMATURE" | "GREASE_PENCIL" | "EMPTY" | "OTHER"; modifierCount: number; modifiers: DepsgraphModifierEvaluationIR[]; } export interface DepsgraphConstraintEvaluationIR { name: string; typeCode: number; influence: number; flag: number; targetObjectId?: string; } export interface DepsgraphBoneEvaluationIR { id: string; name: string; poseMatrix: number[]; constraints: DepsgraphConstraintEvaluationIR[]; } export interface DepsgraphArmatureEvaluationIR { id: string; objectId: string; bones: DepsgraphBoneEvaluationIR[]; } export interface DepsgraphNonMeshGeometryIR { objectId: string; sourceDataId: string; sourceType: "CURVE" | "SURFACE" | "FONT" | "METABALL"; meshId: string; status: "EVALUATED" | "BLOCKED"; errorCode?: "NON_MESH_DATA_BUDGET_EXCEEDED"; vertexCount: number; edgeCount: number; triangleCount: number; worldMatrix: number[]; positions?: number[]; normals?: number[]; edgeVertexIndices?: number[]; indices?: number[]; uvs?: number[]; triangleMaterialIndices?: number[]; sourceElementIndices?: number[]; materialSlotIds: string[]; sourceMappingStatus: "EVALUATED_FACE" | "EVALUATED_EDGE"; } export interface DepsgraphEvaluationIR { engine: "BlenderDepsgraph"; status: "EVALUATED"; scene: string; viewLayer: string; frame: number; objectCount: number; meshObjectCount: number; objects: DepsgraphObjectEvaluationIR[]; meshes: DepsgraphMeshEvaluationIR[]; nonMeshGeometries?: DepsgraphNonMeshGeometryIR[]; armatures?: DepsgraphArmatureEvaluationIR[]; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function numberField(record: Record, field: string): number { const value = record[field]; if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Depsgraph field ${field} is invalid`); return value; } function stringField(record: Record, field: string): string { const value = record[field]; if (typeof value !== "string") throw new Error(`Depsgraph field ${field} is invalid`); return value; } function numberArray(record: Record, field: string): number[] { const value = record[field]; if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isFinite(item))) { throw new Error(`Depsgraph field ${field} is invalid`); } return value; } function countField(record: Record, field: string): number { const value = numberField(record, field); if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Depsgraph field ${field} is not a non-negative count`); return value; } function domainCardinality(value: unknown): GeometryNodeDomainCardinalityIR { return parseGeometryNodeDomainCardinality(value, "meshes[].domainCardinality"); } function fieldMaterializations( value: unknown, cardinality: GeometryNodeDomainCardinalityIR, ): NonNullable { if (!Array.isArray(value) || value.length > 64) throw new Error("Depsgraph mesh fieldMaterializations are invalid"); const fieldIds = new Set(); return value.map((candidate) => { if (!isRecord(candidate) || Object.keys(candidate).some((key) => ![ "schemaVersion", "fieldId", "domain", "dataType", "elementCount", "scalarValueCount", "materializedByteLength", "transport", "errorCode", ].includes(key)) || candidate.schemaVersion !== 1 || candidate.domain !== "POINT" || candidate.dataType !== "FLOAT" || (candidate.transport !== "JSON" && candidate.transport !== "BINARY_REQUIRED")) { throw new Error("Depsgraph mesh field materialization is invalid"); } const fieldId = stringField(candidate, "fieldId"); if (new TextEncoder().encode(fieldId).byteLength > GEOMETRY_NODE_FIELD_BUDGET.maxIdentifierBytes || fieldIds.has(fieldId)) { throw new Error("Depsgraph mesh field materialization identity is invalid"); } fieldIds.add(fieldId); const elementCount = countField(candidate, "elementCount"); const scalarValueCount = countField(candidate, "scalarValueCount"); const materializedByteLength = countField(candidate, "materializedByteLength"); if (elementCount !== cardinality.POINT || scalarValueCount !== elementCount || materializedByteLength !== scalarValueCount * Float32Array.BYTES_PER_ELEMENT) { throw new Error("Depsgraph mesh field materialization counts are inconsistent"); } const errorCode = candidate.errorCode; if ((candidate.transport === "JSON" && errorCode !== undefined) || (candidate.transport === "JSON" && scalarValueCount > GEOMETRY_NODE_FIELD_BUDGET.maxJsonScalarValuesPerField) || (candidate.transport === "BINARY_REQUIRED" && (errorCode !== "GN_FIELD_JSON_BUDGET_EXCEEDED" || scalarValueCount <= GEOMETRY_NODE_FIELD_BUDGET.maxJsonScalarValuesPerField))) { throw new Error("Depsgraph mesh field materialization error is inconsistent"); } return { schemaVersion: 1 as const, fieldId, domain: "POINT" as const, dataType: "FLOAT" as const, elementCount, scalarValueCount, materializedByteLength, transport: candidate.transport, ...(errorCode === undefined ? {} : { errorCode: errorCode as "GN_FIELD_JSON_BUDGET_EXCEEDED" }), }; }); } function booleanField(record: Record, field: string): boolean { const value = record[field]; if (typeof value !== "boolean") throw new Error(`Depsgraph field ${field} is invalid`); return value; } function constraintReports(record: Record): DepsgraphConstraintEvaluationIR[] { const value = record.constraints; if (!Array.isArray(value)) throw new Error("Depsgraph bone constraints are invalid"); return value.map((candidate) => { if (!isRecord(candidate)) throw new Error("Depsgraph constraint entry is invalid"); const targetObjectId = candidate.targetObjectId; if (targetObjectId !== undefined && typeof targetObjectId !== "string") { throw new Error("Depsgraph constraint targetObjectId is invalid"); } return { name: stringField(candidate, "name"), typeCode: countField(candidate, "typeCode"), influence: numberField(candidate, "influence"), flag: countField(candidate, "flag"), ...(targetObjectId === undefined ? {} : { targetObjectId }), }; }); } function armatureReports(value: unknown): DepsgraphArmatureEvaluationIR[] { if (!Array.isArray(value)) throw new Error("Depsgraph armatures are invalid"); return value.map((candidate) => { if (!isRecord(candidate)) throw new Error("Depsgraph armature entry is invalid"); const bonesValue = candidate.bones; if (!Array.isArray(bonesValue)) throw new Error("Depsgraph armature bones are invalid"); const bones = bonesValue.map((boneValue) => { if (!isRecord(boneValue)) throw new Error("Depsgraph bone entry is invalid"); const poseMatrix = numberArray(boneValue, "poseMatrix"); if (poseMatrix.length !== 16) throw new Error("Depsgraph bone poseMatrix must contain 16 numbers"); return { id: stringField(boneValue, "id"), name: stringField(boneValue, "name"), poseMatrix, constraints: constraintReports(boneValue), }; }); return { id: stringField(candidate, "id"), objectId: stringField(candidate, "objectId"), bones }; }); } function modifierReports(record: Record): DepsgraphModifierEvaluationIR[] { const value = record.modifiers; if (!Array.isArray(value)) throw new Error(`Depsgraph field modifiers is invalid`); return value.map((candidate) => { if (!isRecord(candidate)) throw new Error("Depsgraph modifier entry is invalid"); const status = stringField(candidate, "status"); if (status !== "EVALUATED" && status !== "DISABLED" && status !== "BLOCKED") { throw new Error(`Depsgraph modifier status is invalid: ${status}`); } const dependsOn = candidate.dependsOn; if (dependsOn !== undefined && (!Array.isArray(dependsOn) || dependsOn.some((item) => typeof item !== "string"))) { throw new Error("Depsgraph modifier dependsOn is invalid"); } const targetObjectIds = candidate.targetObjectIds; if (targetObjectIds !== undefined && (!Array.isArray(targetObjectIds) || targetObjectIds.some((item) => typeof item !== "string"))) { throw new Error("Depsgraph modifier targetObjectIds is invalid"); } const reason = candidate.reason; const error = candidate.error; const errorCode = candidate.errorCode; const suggestion = candidate.suggestion; if (reason !== undefined && typeof reason !== "string") throw new Error("Depsgraph modifier reason is invalid"); if (error !== undefined && typeof error !== "string") throw new Error("Depsgraph modifier error is invalid"); if (errorCode !== undefined && ![ "UNSUPPORTED_MODIFIER_TYPE", "MODIFIER_TARGET_MISSING", "BLENDER_MODIFIER_ERROR", "GEOMETRY_NODES_SIMULATION_UNAVAILABLE", "GEOMETRY_NODES_EVALUATOR_UNSUPPORTED", ].includes(errorCode as string)) { throw new Error("Depsgraph modifier errorCode is invalid"); } if (suggestion !== undefined && typeof suggestion !== "string") throw new Error("Depsgraph modifier suggestion is invalid"); return { uuid: stringField(candidate, "uuid"), index: countField(candidate, "index"), persistentUid: countField(candidate, "persistentUid"), typeCode: countField(candidate, "typeCode"), type: stringField(candidate, "type"), name: stringField(candidate, "name"), showViewport: booleanField(candidate, "showViewport"), showRender: booleanField(candidate, "showRender"), showEditmode: booleanField(candidate, "showEditmode"), showOnCage: booleanField(candidate, "showOnCage"), status, ...(dependsOn === undefined ? {} : { dependsOn: dependsOn as string[] }), ...(targetObjectIds === undefined ? {} : { targetObjectIds: targetObjectIds as string[] }), ...(reason === undefined ? {} : { reason }), ...(error === undefined ? {} : { error }), ...(errorCode === undefined ? {} : { errorCode: errorCode as DepsgraphModifierEvaluationIR["errorCode"] }), ...(suggestion === undefined ? {} : { suggestion }), }; }); } export function parseDepsgraphEvaluation(value: unknown): DepsgraphEvaluationIR { if (!isRecord(value) || value.engine !== "BlenderDepsgraph" || value.status !== "EVALUATED") { throw new Error("Blender Depsgraph report has an invalid status"); } if (!Array.isArray(value.meshes)) throw new Error("Blender Depsgraph report has no mesh evaluations"); if (!Array.isArray(value.objects)) throw new Error("Blender Depsgraph report has no object evaluations"); const objects = value.objects.map((candidate) => { if (!isRecord(candidate)) throw new Error("Blender Depsgraph object entry is invalid"); const type = stringField(candidate, "type"); if (!["MESH", "CURVE", "SURFACE", "FONT", "METABALL", "CURVES", "POINT_CLOUD", "VOLUME", "LATTICE", "ARMATURE", "GREASE_PENCIL", "EMPTY", "OTHER"].includes(type)) { throw new Error(`Blender Depsgraph object type is invalid: ${type}`); } const modifiers = modifierReports(candidate); const modifierCount = countField(candidate, "modifierCount"); if (modifiers.length !== modifierCount) throw new Error("Blender Depsgraph object modifier count is inconsistent"); return { objectId: stringField(candidate, "objectId"), type: type as DepsgraphObjectEvaluationIR["type"], modifierCount, modifiers, }; }); const meshes = value.meshes.map((candidate) => { if (!isRecord(candidate)) throw new Error("Blender Depsgraph mesh entry is invalid"); const vertexCount = countField(candidate, "vertexCount"); const triangleCount = countField(candidate, "triangleCount"); const modifiers = modifierReports(candidate); const modifierCount = countField(candidate, "modifierCount"); if (modifiers.length !== modifierCount) throw new Error("Blender Depsgraph mesh modifier count is inconsistent"); const worldMatrix = numberArray(candidate, "worldMatrix"); const positions = numberArray(candidate, "positions"); const indices = numberArray(candidate, "indices"); if (worldMatrix.length !== 16 || positions.length !== vertexCount * 3 || indices.length !== triangleCount * 3) { throw new Error(`Depsgraph mesh ${stringField(candidate, "sourceMeshId")} buffer lengths are inconsistent`); } if (indices.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= vertexCount)) { throw new Error(`Depsgraph mesh ${stringField(candidate, "sourceMeshId")} has an invalid index`); } const attributesValue = candidate.attributes; const domainCardinalityValue = candidate.domainCardinality; const parsedDomainCardinality = domainCardinalityValue === undefined ? undefined : domainCardinality(domainCardinalityValue); const fieldMaterializationsValue = candidate.fieldMaterializations; if (parsedDomainCardinality !== undefined && parsedDomainCardinality.POINT !== vertexCount) { throw new Error("Depsgraph mesh POINT cardinality is inconsistent"); } const parsedFieldMaterializations = fieldMaterializationsValue === undefined ? undefined : (() => { if (parsedDomainCardinality === undefined) { throw new Error("Depsgraph mesh field materializations require domain cardinality"); } return fieldMaterializations(fieldMaterializationsValue, parsedDomainCardinality); })(); let attributes: DepsgraphMeshEvaluationIR["attributes"]; if (attributesValue !== undefined) { if (!isRecord(attributesValue) || Object.keys(attributesValue).length > 64) { throw new Error("Depsgraph mesh attributes are invalid"); } attributes = {}; for (const [name, attributeValue] of Object.entries(attributesValue)) { if (name.length === 0 || name.length > 64 || !isRecord(attributeValue) || Object.keys(attributeValue).some((key) => !["domain", "dataType", "values"].includes(key)) || attributeValue.domain !== "POINT" || attributeValue.dataType !== "FLOAT") { throw new Error(`Depsgraph mesh attribute ${name} is invalid`); } const values = numberArray(attributeValue, "values"); if (values.length !== vertexCount || parsedDomainCardinality === undefined || parsedDomainCardinality.POINT !== values.length) { throw new Error(`Depsgraph mesh attribute ${name} length is inconsistent`); } const receipt = parsedFieldMaterializations?.find((entry) => entry.fieldId === `attribute:${name}`); if (receipt === undefined || receipt.transport !== "JSON" || receipt.materializedByteLength !== values.length * Float32Array.BYTES_PER_ELEMENT) { throw new Error(`Depsgraph mesh attribute ${name} has no matching field materialization`); } attributes[name] = { domain: "POINT", dataType: "FLOAT", values }; } } if (parsedFieldMaterializations?.some((entry) => entry.transport === "JSON" && (attributes === undefined || !Object.hasOwn(attributes, entry.fieldId.replace(/^attribute:/, ""))))) { throw new Error("Depsgraph mesh JSON field materialization has no matching attribute payload"); } return { objectId: stringField(candidate, "objectId"), meshId: stringField(candidate, "meshId"), sourceMeshId: stringField(candidate, "sourceMeshId"), vertexCount, triangleCount, modifierCount, modifiers, worldMatrix, positions, indices, ...(parsedDomainCardinality === undefined ? {} : { domainCardinality: parsedDomainCardinality }), ...(parsedFieldMaterializations === undefined ? {} : { fieldMaterializations: parsedFieldMaterializations }), ...(attributes === undefined ? {} : { attributes }), }; }); const objectCount = countField(value, "objectCount"); const meshObjectCount = countField(value, "meshObjectCount"); if (objects.length !== objectCount) throw new Error("Blender Depsgraph object count is inconsistent"); if (meshes.length !== meshObjectCount) throw new Error("Blender Depsgraph mesh object count is inconsistent"); const armatures = value.armatures === undefined ? undefined : armatureReports(value.armatures); const nonMeshGeometries = value.nonMeshGeometries === undefined ? undefined : (() => { if (!Array.isArray(value.nonMeshGeometries)) throw new Error("Depsgraph non-mesh geometries are invalid"); return value.nonMeshGeometries.map((candidate): DepsgraphNonMeshGeometryIR => { if (!isRecord(candidate)) throw new Error("Depsgraph non-mesh geometry entry is invalid"); const sourceType = stringField(candidate, "sourceType"); const status = stringField(candidate, "status"); if (!["CURVE", "SURFACE", "FONT", "METABALL"].includes(sourceType) || (status !== "EVALUATED" && status !== "BLOCKED")) throw new Error("Depsgraph non-mesh geometry type or status is invalid"); const vertexCount = countField(candidate, "vertexCount"); const edgeCount = countField(candidate, "edgeCount"); const triangleCount = countField(candidate, "triangleCount"); const worldMatrix = numberArray(candidate, "worldMatrix"); if (worldMatrix.length !== 16) throw new Error("Depsgraph non-mesh world matrix is invalid"); const materialSlotIds = candidate.materialSlotIds; if (!Array.isArray(materialSlotIds) || materialSlotIds.some((item) => typeof item !== "string")) throw new Error("Depsgraph non-mesh material slots are invalid"); const common = { objectId: stringField(candidate, "objectId"), sourceDataId: stringField(candidate, "sourceDataId"), sourceType: sourceType as DepsgraphNonMeshGeometryIR["sourceType"], meshId: stringField(candidate, "meshId"), status: status as DepsgraphNonMeshGeometryIR["status"], vertexCount, edgeCount, triangleCount, worldMatrix, materialSlotIds: materialSlotIds as string[], sourceMappingStatus: stringField(candidate, "sourceMappingStatus") as DepsgraphNonMeshGeometryIR["sourceMappingStatus"], }; if (common.sourceMappingStatus !== "EVALUATED_FACE" && common.sourceMappingStatus !== "EVALUATED_EDGE") throw new Error("Depsgraph non-mesh source mapping status is invalid"); if (status === "BLOCKED") { if (candidate.errorCode !== "NON_MESH_DATA_BUDGET_EXCEEDED") throw new Error("Depsgraph blocked non-mesh geometry has no budget error"); return { ...common, errorCode: candidate.errorCode }; } const positions = numberArray(candidate, "positions"); const normals = numberArray(candidate, "normals"); const edgeVertexIndices = numberArray(candidate, "edgeVertexIndices"); const indices = numberArray(candidate, "indices"); const uvs = numberArray(candidate, "uvs"); const triangleMaterialIndices = numberArray(candidate, "triangleMaterialIndices"); const sourceElementIndices = numberArray(candidate, "sourceElementIndices"); if (positions.length !== vertexCount * 3 || normals.length !== vertexCount * 3 || edgeVertexIndices.length !== edgeCount * 2 || indices.length !== triangleCount * 3 || (uvs.length !== 0 && uvs.length !== triangleCount * 3 * 2) || triangleMaterialIndices.length !== triangleCount || sourceElementIndices.length !== triangleCount) throw new Error(`Depsgraph non-mesh ${common.sourceDataId} buffer lengths are inconsistent`); if (edgeVertexIndices.some((item) => !Number.isSafeInteger(item) || item < 0 || item >= vertexCount) || indices.some((item) => !Number.isSafeInteger(item) || item < 0 || item >= vertexCount) || triangleMaterialIndices.some((item) => !Number.isSafeInteger(item) || item < 0) || sourceElementIndices.some((item) => !Number.isSafeInteger(item) || item < 0)) throw new Error(`Depsgraph non-mesh ${common.sourceDataId} indices are invalid`); return { ...common, positions, normals, edgeVertexIndices, indices, uvs, triangleMaterialIndices, sourceElementIndices }; }); })(); return { engine: "BlenderDepsgraph", status: "EVALUATED", scene: stringField(value, "scene"), viewLayer: stringField(value, "viewLayer"), frame: numberField(value, "frame"), objectCount, meshObjectCount, objects, meshes, ...(nonMeshGeometries === undefined ? {} : { nonMeshGeometries }), ...(armatures === undefined ? {} : { armatures }), }; }