import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; import { GEOMETRY_NODE_FIELD_BUDGET, parseGeometryNodeFieldMaterializationBatch, type GeometryNodeDomainCardinalityIR, } from "../../protocol/geometry-nodes"; import { parseDepsgraphEvaluation, type DepsgraphEvaluationIR } from "../../protocol/depsgraph"; const root = path.resolve(import.meta.dirname, "../../.."); const fixture = fs.readFileSync(path.join(root, "tests/files/web/geometry_node_allowlist_evaluator.blend")); test("M10-04 binds field conversion budgets to native mesh domain cardinality", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const result = await page.evaluate(async ({ bytes }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; const client = new WebEngineClient({ timeoutMs: 90_000 }); await client.init(); await client.openBlend(source); const evaluated = await client.evaluateDepsgraph(); client.terminate(); const mesh = evaluated.depsgraph.meshes.find((candidate) => candidate.objectId === "object:M10GN_StoreAttribute"); if (!mesh?.domainCardinality) throw new Error("Store Named Attribute domain cardinality is missing"); return { depsgraph: evaluated.depsgraph, domainCardinality: mesh.domainCardinality, fieldMaterializations: mesh.fieldMaterializations ?? [], attributes: mesh.attributes ?? {}, }; }, { bytes: new Uint8Array(fixture) }); expect(result.domainCardinality).toEqual({ POINT: 8, EDGE: 12, FACE: 6, CORNER: 24, CURVE: 0, INSTANCE: 0, LAYER: 0, }); expect(result.fieldMaterializations).toEqual([{ schemaVersion: 1, fieldId: "attribute:m10_value", domain: "POINT", dataType: "FLOAT", elementCount: 8, scalarValueCount: 8, materializedByteLength: 32, transport: "JSON", }]); expect(result.attributes.m10_value.values).toHaveLength(8); expect(result.attributes.m10_value.values.every((value: number) => value === 0.375)).toBe(true); const rejects = (mutate: (candidate: DepsgraphEvaluationIR) => void): void => { const candidate = structuredClone(result.depsgraph); mutate(candidate); expect(() => parseDepsgraphEvaluation(candidate)).toThrow(); }; rejects((candidate) => { const target = candidate.meshes.find((entry) => entry.objectId === "object:M10GN_StoreAttribute")!; target.domainCardinality!.POINT += 1; }); rejects((candidate) => { const target = candidate.meshes.find((entry) => entry.objectId === "object:M10GN_StoreAttribute")!; target.fieldMaterializations![0].materializedByteLength += 4; }); rejects((candidate) => { const target = candidate.meshes.find((entry) => entry.objectId === "object:M10GN_StoreAttribute")!; (target.fieldMaterializations![0] as unknown as Record).values = [0]; }); const common = { schemaVersion: 1 as const, graphId: "node-group:M10GN_StoreAttributeGraph", graphHash: "a".repeat(64), revision: 1, transport: "JSON" as const, domainCardinality: result.domainCardinality as GeometryNodeDomainCardinalityIR, }; const batch = parseGeometryNodeFieldMaterializationBatch([ { ...common, fieldId: "field:point-to-corner", sourceDomain: "POINT", targetDomain: "CORNER", dataType: "FLOAT", }, { ...common, fieldId: "field:constant-offset", sourceDomain: "CONSTANT", targetDomain: "POINT", dataType: "VECTOR", }, ]); expect(batch).toMatchObject({ fieldCount: 2, domainConversionCount: 1, materializedElementCount: 32, materializedByteLength: 192, }); expect(batch.fields.every((field) => field.scalarValueCount <= GEOMETRY_NODE_FIELD_BUDGET.maxJsonScalarValuesPerField)).toBe(true); });