import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import crypto from "node:crypto"; import { spawnSync } from "node:child_process"; import factory from "../../web/app/src/vendor/blender/web_engine.js"; import { readIndexedTask, verifyTaskIndex } from "./task-context-lib.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const task = process.argv[process.argv.indexOf("--task") + 1]; verifyTaskIndex(); const entry = readIndexedTask(task); assert.ok(entry); assert.ok(["active", "completed"].includes(entry.state)); const wasmBinary = fs.readFileSync(path.join(root, "web/app/src/vendor/blender/web_engine.wasm")); const open = (engine, handle, input) => { const pointer = engine._malloc(input.byteLength); engine.HEAPU8.set(input, pointer); try { assert.equal(engine._web_engine_open_blend(handle, pointer, input.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); } finally { engine._free(pointer); } }; const output = (engine, handle, fn, owned = false) => { const data = engine._malloc(4); const length = engine._malloc(4); try { assert.equal(fn(handle, data, length), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); const pointer = engine.HEAPU32[data >>> 2]; const size = engine.HEAPU32[length >>> 2]; const result = engine.HEAPU8.slice(pointer, pointer + size); if (owned) engine._web_engine_free_buffer(pointer); return result; } finally { engine._free(data); engine._free(length); } }; const snapshot = (engine, handle) => JSON.parse(new TextDecoder().decode(output(engine, handle, engine._web_engine_get_scene_snapshot))); if (task === "M16-GAP-00002") { const armatureFixture = path.join(root, "tests/files/web/generated/M16-GAP-00002-datablock-Armature.blend"); const armatureDesktopPath = path.join(root, "tests/golden/M16-GAP-00002/armature-desktop-report.json"); const armatureDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-armature-desktop.py"), "--", armatureFixture, armatureDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(armatureDesktop.status, 0, `${armatureDesktop.stdout ?? ""}\n${armatureDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(armatureDesktopPath, "utf8")); const armatureBytes = fs.readFileSync(armatureFixture); const armatureEngine = await factory({ wasmBinary }); const armatureHandle = armatureEngine._web_engine_create(); open(armatureEngine, armatureHandle, armatureBytes); const armatureSnapshot = snapshot(armatureEngine, armatureHandle); const armature = armatureSnapshot.armatures?.find((value) => value.id === "armature:RiggedArmature"); const armatureMesh = armatureSnapshot.meshes?.find((value) => value.id === "mesh:RiggedShapeMesh"); assert.ok(armature); assert.deepEqual(armature.bones.map((bone) => bone.name), ["Root", "Tip"]); assert.equal(armature.bones[1].parentId, armature.bones[0].id); assert.ok(armatureMesh?.skinWeights?.boneNames?.includes("Root")); const armatureSaved = output(armatureEngine, armatureHandle, armatureEngine._web_engine_save_blend, true); const armatureReopened = armatureEngine._web_engine_create(); open(armatureEngine, armatureReopened, armatureSaved); const armatureAfter = snapshot(armatureEngine, armatureReopened).armatures?.find((value) => value.id === "armature:RiggedArmature"); assert.deepEqual(armatureAfter, armature); const armatureReport = { schemaVersion: 1, task: "M16-GAP-00002", operation: "ARMATURE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, armatureFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, armatureDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", armatureId: armature.id, bones: armature.bones.map((bone) => ({ name: bone.name, parentId: bone.parentId })) }, saveReopen: "EXACT", nextTask: "M16-GAP-00003" }; const armatureReportPath = path.join(root, "tests/golden/M16-GAP-00002/armature-local-exact-report.json"); fs.mkdirSync(path.dirname(armatureReportPath), { recursive: true }); fs.writeFileSync(armatureReportPath, `${JSON.stringify(armatureReport, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} bones=${armature.bones.length} desktop=exact saveReopen=exact next=${armatureReport.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00003") { const brushFixture = path.join(root, "tests/files/web/generated/M16-GAP-00003-datablock-Brush.blend"); const brushDesktopPath = path.join(root, "tests/golden/M16-GAP-00003/brush-desktop-report.json"); const brushDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-brush-desktop.py"), "--", brushFixture, brushDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(brushDesktop.status, 0, `${brushDesktop.stdout ?? ""}\n${brushDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(brushDesktopPath, "utf8")); const brushBytes = fs.readFileSync(brushFixture); const brushEngine = await factory({ wasmBinary }); const brushHandle = brushEngine._web_engine_create(); open(brushEngine, brushHandle, brushBytes); const brushSnapshot = snapshot(brushEngine, brushHandle); const brush = brushSnapshot.brushes?.find((value) => value.id === "brush:WebGapBrush"); assert.ok(brush); assert.equal(brush.name, "WebGapBrush"); assert.equal(brush.size, 42); assert.ok(Math.abs(brush.alpha - 0.65) < 1e-5); assert.ok(Math.abs(brush.hardness - 0.8) < 1e-5); assert.equal(brush.spacing, 17); assert.ok(Math.abs(brush.jitter - 0.2) < 1e-5); const brushSaved = output(brushEngine, brushHandle, brushEngine._web_engine_save_blend, true); const brushReopened = brushEngine._web_engine_create(); open(brushEngine, brushReopened, brushSaved); const brushAfter = snapshot(brushEngine, brushReopened).brushes?.find((value) => value.id === brush.id); assert.deepEqual(brushAfter, brush); const brushReport = { schemaVersion: 1, task: "M16-GAP-00003", operation: "BRUSH_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, brushFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, brushDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", brushId: brush.id, values: { size: brush.size, alpha: brush.alpha, hardness: brush.hardness, spacing: brush.spacing, jitter: brush.jitter, sculptTool: brush.sculptTool } }, saveReopen: "EXACT", nextTask: "M16-GAP-00004" }; const brushReportPath = path.join(root, "tests/golden/M16-GAP-00003/brush-local-exact-report.json"); fs.mkdirSync(path.dirname(brushReportPath), { recursive: true }); fs.writeFileSync(brushReportPath, `${JSON.stringify(brushReport, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} brush=${brush.id} size=${brush.size} desktop=exact saveReopen=exact next=${brushReport.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00004" && !process.env.USE_LEGACY_CAMERA_BRANCH) { const cameraFixture = path.join(root, "tests/files/web/generated/M16-GAP-00004-datablock-Camera.blend"); const cameraDesktopPath = path.join(root, "tests/golden/M16-GAP-00004/camera-desktop-report.json"); fs.mkdirSync(path.dirname(cameraDesktopPath), { recursive: true }); const cameraDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-camera-desktop.py"), "--", cameraFixture, cameraDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(cameraDesktop.status, 0, `${cameraDesktop.stdout ?? ""}\n${cameraDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(cameraDesktopPath, "utf8")); const cameraBytes = fs.readFileSync(cameraFixture); const cameraEngine = await factory({ wasmBinary }); const cameraHandle = cameraEngine._web_engine_create(); open(cameraEngine, cameraHandle, cameraBytes); const camera = snapshot(cameraEngine, cameraHandle).cameras?.find((value) => value.id === "camera:WebGapCamera"); assert.ok(camera); assert.equal(camera.projection, "PERSPECTIVE"); assert.ok(Math.abs(camera.lensMm - 52) < 1e-5); assert.ok(Math.abs(camera.near - 0.1) < 1e-5); assert.ok(Math.abs(camera.far - 2500) < 1e-5); const cameraSaved = output(cameraEngine, cameraHandle, cameraEngine._web_engine_save_blend, true); const cameraReopened = cameraEngine._web_engine_create(); open(cameraEngine, cameraReopened, cameraSaved); const cameraAfter = snapshot(cameraEngine, cameraReopened).cameras?.find((value) => value.id === camera.id); assert.deepEqual(cameraAfter, camera); const cameraReport = { schemaVersion: 1, task: "M16-GAP-00004", operation: "CAMERA_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, cameraFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, cameraDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", cameraId: camera.id, values: { projection: camera.projection, lensMm: camera.lensMm, near: camera.near, far: camera.far } }, saveReopen: "EXACT", nextTask: "M16-GAP-00005" }; const cameraReportPath = path.join(root, "tests/golden/M16-GAP-00004/camera-local-exact-report.json"); fs.mkdirSync(path.dirname(cameraReportPath), { recursive: true }); fs.writeFileSync(cameraReportPath, `${JSON.stringify(cameraReport, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} camera=${camera.id} lensMm=${camera.lensMm} desktop=exact saveReopen=exact next=${cameraReport.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00004") { const cameraFixture = path.join(root, "tests/files/web/generated/M16-GAP-00004-datablock-Camera.blend"); const cameraDesktopPath = path.join(root, "tests/golden/M16-GAP-00004/camera-desktop-report.json"); fs.mkdirSync(path.dirname(cameraDesktopPath), { recursive: true }); const cameraDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-camera-desktop.py"), "--", cameraFixture, cameraDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(cameraDesktop.status, 0, `${cameraDesktop.stdout ?? ""}\n${cameraDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(cameraDesktopPath, "utf8")); const cameraBytes = fs.readFileSync(cameraFixture); const cameraEngine = await factory({ wasmBinary }); const cameraHandle = cameraEngine._web_engine_create(); open(cameraEngine, cameraHandle, cameraBytes); const cameraSnapshot = snapshot(cameraEngine, cameraHandle); const camera = cameraSnapshot.cameras?.find((value) => value.id === "camera:WebGapCamera"); assert.ok(camera); assert.equal(camera.type, "PERSP"); assert.ok(Math.abs(camera.lens - 52) < 1e-5); assert.ok(Math.abs(camera.clipStart - 0.1) < 1e-5); assert.ok(Math.abs(camera.clipEnd - 2500) < 1e-5); const cameraSaved = output(cameraEngine, cameraHandle, cameraEngine._web_engine_save_blend, true); const cameraReopened = cameraEngine._web_engine_create(); open(cameraEngine, cameraReopened, cameraSaved); const cameraAfter = snapshot(cameraEngine, cameraReopened).cameras?.find((value) => value.id === camera.id); assert.deepEqual(cameraAfter, camera); const cameraReport = { schemaVersion: 1, task: "M16-GAP-00004", operation: "CAMERA_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, cameraFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, cameraDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", cameraId: camera.id, values: { type: camera.type, lens: camera.lens, clipStart: camera.clipStart, clipEnd: camera.clipEnd } }, saveReopen: "EXACT", nextTask: "M16-GAP-00005" }; const cameraReportPath = path.join(root, "tests/golden/M16-GAP-00004/camera-local-exact-report.json"); fs.mkdirSync(path.dirname(cameraReportPath), { recursive: true }); fs.writeFileSync(cameraReportPath, `${JSON.stringify(cameraReport, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} camera=${camera.id} lens=${camera.lens} desktop=exact saveReopen=exact next=${cameraReport.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00005" && !process.env.USE_LEGACY_COLLECTION_BRANCH) { const collectionFixture = path.join(root, "tests/files/web/generated/M16-GAP-00005-datablock-Collection.blend"); const collectionDesktopPath = path.join(root, "tests/golden/M16-GAP-00005/collection-desktop-report.json"); fs.mkdirSync(path.dirname(collectionDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-collection-desktop.py"), "--", collectionFixture, collectionDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(collectionDesktopPath, "utf8")); const collectionEngine = await factory({ wasmBinary }); const collectionHandle = collectionEngine._web_engine_create(); open(collectionEngine, collectionHandle, fs.readFileSync(collectionFixture)); const collections = snapshot(collectionEngine, collectionHandle).collections?.filter((value) => value.id.includes("WebGap")); assert.ok(collections?.length >= 2); const parent = collections.find((value) => value.id === "collection:WebGapCollection"); const child = collections.find((value) => value.id === "collection:WebGapChild"); assert.ok(parent && child); assert.ok(parent.childCollectionIds?.includes(child.id)); assert.ok(child.objectIds?.includes("object:WebGapCollectionObject")); const saved = output(collectionEngine, collectionHandle, collectionEngine._web_engine_save_blend, true); const reopened = collectionEngine._web_engine_create(); open(collectionEngine, reopened, saved); const after = snapshot(collectionEngine, reopened).collections?.filter((value) => value.id.includes("WebGap")); assert.deepEqual(after, collections); const report = { schemaVersion: 1, task: "M16-GAP-00005", operation: "COLLECTION_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, collectionFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, collectionDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", collections: collections.map((value) => ({ id: value.id, childCollectionIds: value.childCollectionIds, objectIds: value.objectIds })) }, saveReopen: "EXACT", nextTask: "M16-GAP-00006" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00005/collection-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} collections=${collections.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00005") { const collectionFixture = path.join(root, "tests/files/web/generated/M16-GAP-00005-datablock-Collection.blend"); const collectionDesktopPath = path.join(root, "tests/golden/M16-GAP-00005/collection-desktop-report.json"); fs.mkdirSync(path.dirname(collectionDesktopPath), { recursive: true }); const collectionDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-collection-desktop.py"), "--", collectionFixture, collectionDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(collectionDesktop.status, 0, `${collectionDesktop.stdout ?? ""}\n${collectionDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(collectionDesktopPath, "utf8")); const collectionBytes = fs.readFileSync(collectionFixture); const collectionEngine = await factory({ wasmBinary }); const collectionHandle = collectionEngine._web_engine_create(); open(collectionEngine, collectionHandle, collectionBytes); const collectionSnapshot = snapshot(collectionEngine, collectionHandle); const collections = collectionSnapshot.collections?.filter((value) => value.id.includes("WebGap")); assert.ok(collections?.length >= 2); const parent = collections.find((value) => value.id === "collection:WebGapCollection"); const child = collections.find((value) => value.id === "collection:WebGapChild"); assert.ok(parent && child); assert.ok(parent.children?.includes(child.id)); assert.ok(child.objectIds?.includes("object:WebGapCollectionObject")); const saved = output(collectionEngine, collectionHandle, collectionEngine._web_engine_save_blend, true); const reopened = collectionEngine._web_engine_create(); open(collectionEngine, reopened, saved); const after = snapshot(collectionEngine, reopened).collections?.filter((value) => value.id.includes("WebGap")); assert.deepEqual(after, collections); const report = { schemaVersion: 1, task: "M16-GAP-00005", operation: "COLLECTION_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, collectionFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, collectionDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", collections: collections.map((value) => ({ id: value.id, children: value.children, objectIds: value.objectIds })) }, saveReopen: "EXACT", nextTask: "M16-GAP-00006" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00005/collection-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} collections=${collections.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00006") { const curveFixture = path.join(root, "tests/files/web/generated/M16-GAP-00006-datablock-Curve.blend"); const curveDesktopPath = path.join(root, "tests/golden/M16-GAP-00006/curve-desktop-report.json"); fs.mkdirSync(path.dirname(curveDesktopPath), { recursive: true }); const curveDesktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-curve-desktop.py"), "--", curveFixture, curveDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(curveDesktop.status, 0, `${curveDesktop.stdout ?? ""}\n${curveDesktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(curveDesktopPath, "utf8")); const curveEngine = await factory({ wasmBinary }); const curveHandle = curveEngine._web_engine_create(); open(curveEngine, curveHandle, fs.readFileSync(curveFixture)); const nonMesh = snapshot(curveEngine, curveHandle).nonMeshData?.find((value) => value.id === "curve:WebGapCurve"); assert.ok(nonMesh); assert.equal(nonMesh.geometryStatus, "available"); assert.equal(nonMesh.splineCount, 1); assert.equal(nonMesh.pointCount, 3); assert.deepEqual(nonMesh.splineTypes, ["BEZIER"]); assert.deepEqual(nonMesh.splineOffsets, [0, 3]); assert.equal(nonMesh.controlPoints.length, 9); const saved = output(curveEngine, curveHandle, curveEngine._web_engine_save_blend, true); const reopened = curveEngine._web_engine_create(); open(curveEngine, reopened, saved); const after = snapshot(curveEngine, reopened).nonMeshData?.find((value) => value.id === nonMesh.id); assert.deepEqual(after, nonMesh); const report = { schemaVersion: 1, task: "M16-GAP-00006", operation: "CURVE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, curveFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, curveDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", curveId: nonMesh.id, splineCount: nonMesh.splineCount, pointCount: nonMesh.pointCount, splineTypes: nonMesh.splineTypes }, saveReopen: "EXACT", nextTask: "M16-GAP-00007" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00006/curve-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} curve=${nonMesh.id} splines=${nonMesh.splineCount} points=${nonMesh.pointCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00007") { const curvesFixture = path.join(root, "tests/files/web/generated/M16-GAP-00007-datablock-Curves.blend"); const curvesDesktopPath = path.join(root, "tests/golden/M16-GAP-00007/curves-desktop-report.json"); fs.mkdirSync(path.dirname(curvesDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-curves-desktop.py"), "--", curvesFixture, curvesDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(curvesDesktopPath, "utf8")); const curvesEngine = await factory({ wasmBinary }); const curvesHandle = curvesEngine._web_engine_create(); open(curvesEngine, curvesHandle, fs.readFileSync(curvesFixture)); const curvesData = snapshot(curvesEngine, curvesHandle).nonMeshData?.find((value) => value.id === "curves:WebGapCurves"); assert.ok(curvesData); assert.equal(curvesData.geometryStatus, "available"); assert.ok(curvesData.pointCount >= 4); assert.ok(curvesData.splineCount >= 1); assert.ok(Array.isArray(curvesData.splineOffsets)); const saved = output(curvesEngine, curvesHandle, curvesEngine._web_engine_save_blend, true); const reopened = curvesEngine._web_engine_create(); open(curvesEngine, reopened, saved); const after = snapshot(curvesEngine, reopened).nonMeshData?.find((value) => value.id === curvesData.id); assert.deepEqual(after, curvesData); const report = { schemaVersion: 1, task: "M16-GAP-00007", operation: "CURVES_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, curvesFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, curvesDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", curvesId: curvesData.id, pointCount: curvesData.pointCount, splineCount: curvesData.splineCount }, saveReopen: "EXACT", nextTask: "M16-GAP-00008" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00007/curves-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} curves=${curvesData.id} points=${curvesData.pointCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00008") { const styleFixture = path.join(root, "tests/files/web/generated/M16-GAP-00008-datablock-FreestyleLineStyle.blend"); const styleDesktopPath = path.join(root, "tests/golden/M16-GAP-00008/freestyle-desktop-report.json"); fs.mkdirSync(path.dirname(styleDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-freestyle-desktop.py"), "--", styleFixture, styleDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(styleDesktopPath, "utf8")); const styleEngine = await factory({ wasmBinary }); const styleHandle = styleEngine._web_engine_create(); open(styleEngine, styleHandle, fs.readFileSync(styleFixture)); const lineStyle = snapshot(styleEngine, styleHandle).lineStyles?.find((value) => value.id === "line-style:WebGapLineStyle"); assert.ok(lineStyle); assert.ok(Math.abs(lineStyle.alpha - 0.75) < 1e-5); assert.ok(Math.abs(lineStyle.thickness - 2.5) < 1e-5); assert.deepEqual(lineStyle.color.map((value) => Number(value.toFixed(3))), [0.2, 0.4, 0.8]); assert.equal(lineStyle.dashed, true); const saved = output(styleEngine, styleHandle, styleEngine._web_engine_save_blend, true); const reopened = styleEngine._web_engine_create(); open(styleEngine, reopened, saved); const after = snapshot(styleEngine, reopened).lineStyles?.find((value) => value.id === lineStyle.id); assert.deepEqual(after, lineStyle); const report = { schemaVersion: 1, task: "M16-GAP-00008", operation: "FREESTYLE_LINE_STYLE_LOCAL_EXACT", fixture: { path: path.relative(root, styleFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, styleDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", lineStyleId: lineStyle.id, thickness: lineStyle.thickness, modifierCounts: lineStyle.modifierCounts }, saveReopen: "EXACT", nextTask: "M16-GAP-00009" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00008/freestyle-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} lineStyle=${lineStyle.id} thickness=${lineStyle.thickness} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00009") { const gpFixture = path.join(root, "tests/files/web/generated/M16-GAP-00009-datablock-GreasePencil.blend"); const gpDesktopPath = path.join(root, "tests/golden/M16-GAP-00009/grease-pencil-desktop-report.json"); fs.mkdirSync(path.dirname(gpDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-grease-pencil-desktop.py"), "--", gpFixture, gpDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(gpDesktopPath, "utf8")); const gpEngine = await factory({ wasmBinary }); const gpHandle = gpEngine._web_engine_create(); open(gpEngine, gpHandle, fs.readFileSync(gpFixture)); const gpSnapshot = snapshot(gpEngine, gpHandle); const gp = gpSnapshot.greasePencils?.find((value) => value.name === "GreasePencilData"); assert.ok(gp); assert.ok(gp.layers?.length >= 1); assert.ok(gp.layers[0].frames?.length >= 1); const saved = output(gpEngine, gpHandle, gpEngine._web_engine_save_blend, true); const reopened = gpEngine._web_engine_create(); open(gpEngine, reopened, saved); const after = snapshot(gpEngine, reopened).greasePencils?.find((value) => value.name === gp.name); assert.deepEqual(after, gp); const report = { schemaVersion: 1, task: "M16-GAP-00009", operation: "GREASE_PENCIL_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, gpFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, gpDesktopPath).replaceAll(path.sep, "/") }, wasm: { status: "EXACT", dataName: gp.name, layerCount: gp.layers.length, frameCount: gp.layers.reduce((sum, layer) => sum + layer.frames.length, 0) }, saveReopen: "EXACT", nextTask: "M16-GAP-00010" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00009/grease-pencil-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} greasePencil=${gp.name} layers=${gp.layers.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00010") { const latticeFixture = path.join(root, "tests/files/web/generated/M16-GAP-00010-datablock-Lattice.blend"); const latticeDesktopPath = path.join(root, "tests/golden/M16-GAP-00010/lattice-desktop-report.json"); fs.mkdirSync(path.dirname(latticeDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-lattice-desktop.py"), "--", latticeFixture, latticeDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(latticeDesktopPath, "utf8")); const latticeEngine = await factory({ wasmBinary }); const latticeHandle = latticeEngine._web_engine_create(); open(latticeEngine, latticeHandle, fs.readFileSync(latticeFixture)); const lattice = snapshot(latticeEngine, latticeHandle).lattices?.find((value) => value.id === "lattice:WebGapLattice"); assert.ok(lattice); assert.equal(lattice.status, "AVAILABLE"); assert.deepEqual(lattice.dimensions, desktopEvidence.lattice.dimensions); assert.equal(lattice.pointCount, desktopEvidence.lattice.pointCount); assert.deepEqual(lattice.interpolation, ["KEY_LINEAR", "KEY_CARDINAL", "KEY_BSPLINE"]); assert.equal(lattice.useOutside, true); assert.equal(lattice.vertexGroup, "WebGapInfluence"); assert.deepEqual(lattice.points, desktopEvidence.lattice.points); const saved = output(latticeEngine, latticeHandle, latticeEngine._web_engine_save_blend, true); const reopened = latticeEngine._web_engine_create(); open(latticeEngine, reopened, saved); const after = snapshot(latticeEngine, reopened).lattices?.find((value) => value.id === lattice.id); assert.deepEqual(after, lattice); const report = { schemaVersion: 1, task: "M16-GAP-00010", operation: "LATTICE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, latticeFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, latticeDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", latticeId: lattice.id, dimensions: lattice.dimensions, pointCount: lattice.pointCount, interpolation: lattice.interpolation, pointHash: crypto.createHash("sha256").update(JSON.stringify(lattice.points)).digest("hex") }, saveReopen: "EXACT", nextTask: "M16-GAP-00011" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00010/lattice-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} lattice=${lattice.id} points=${lattice.pointCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00011") { const libraryFixture = path.join(root, "tests/files/web/generated/M16-GAP-00011-datablock-Library.blend"); const libraryDesktopPath = path.join(root, "tests/golden/M16-GAP-00011/library-desktop-report.json"); fs.mkdirSync(path.dirname(libraryDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-library-desktop.py"), "--", libraryFixture, libraryDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(libraryDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const libraryEngine = await factory({ wasmBinary }); const libraryHandle = libraryEngine._web_engine_create(); open(libraryEngine, libraryHandle, fs.readFileSync(libraryFixture)); const librarySnapshot = snapshot(libraryEngine, libraryHandle); assert.equal(librarySnapshot.libraryStatus, "AVAILABLE"); assert.ok(Array.isArray(librarySnapshot.libraries)); assert.equal(librarySnapshot.libraries.length, 1); const library = librarySnapshot.libraries[0]; assert.equal(library.id, desktopEvidence.library.id); assert.equal(library.name, desktopEvidence.library.name); assert.equal(library.sourcePath, "//WebGapLibrary.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(libraryEngine, libraryHandle, libraryEngine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = libraryEngine._web_engine_create(); open(libraryEngine, reopened, saved); const after = snapshot(libraryEngine, reopened); assert.deepEqual(after.libraries, librarySnapshot.libraries); assert.equal(after.libraryStatus, librarySnapshot.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = libraryEngine._malloc(malformed.byteLength); let malformedResult; try { libraryEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = libraryEngine._web_engine_open_blend(libraryHandle, malformedPointer, malformed.byteLength); } finally { libraryEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(libraryEngine, libraryHandle), librarySnapshot); libraryEngine._web_engine_destroy(libraryHandle); libraryEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00011", operation: "LIBRARY_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, libraryFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, libraryDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00012" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00011/library-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00012") { const lightFixture = path.join(root, "tests/files/web/generated/M16-GAP-00012-datablock-Light.blend"); const lightDesktopPath = path.join(root, "tests/golden/M16-GAP-00012/light-desktop-report.json"); fs.mkdirSync(path.dirname(lightDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-light-desktop.py"), "--", lightFixture, lightDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(lightDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const lightEngine = await factory({ wasmBinary }); const lightHandle = lightEngine._web_engine_create(); open(lightEngine, lightHandle, fs.readFileSync(lightFixture)); const lightSnapshot = snapshot(lightEngine, lightHandle); const light = lightSnapshot.lights?.find((value) => value.id === "light:WebGapLight"); assert.ok(light); const normalizedLight = { ...light }; for (const field of ["energy", "exposure", "temperature", "radius", "spotAngle", "spotBlend", "areaSize", "areaSizeY", "areaSpread", "sunAngle"]) normalizedLight[field] = Number(normalizedLight[field].toFixed(6)); normalizedLight.color = normalizedLight.color.map((value) => Number(value.toFixed(6))); assert.deepEqual(normalizedLight, desktopEvidence.light); assert.equal(light.lightType, 4); assert.deepEqual(light.color, [0.25, 0.5, 0.75]); const closeLight = (actual, expected, label) => assert.ok(Math.abs(actual - expected) <= 1e-5, `${label}: ${actual} != ${expected}`); closeLight(light.energy, 400, "energy"); closeLight(light.exposure, 1, "exposure"); closeLight(light.temperature, 5000, "temperature"); assert.equal(light.useTemperature, true); assert.equal(light.castsShadow, false); closeLight(light.radius, 0.3, "radius"); assert.equal(light.areaShape, 1); closeLight(light.areaSize, 3, "areaSize"); closeLight(light.areaSizeY, 2, "areaSizeY"); closeLight(light.areaSpread, 2.4, "areaSpread"); closeLight(light.sunAngle, 0.00918, "sunAngle"); const saved = output(lightEngine, lightHandle, lightEngine._web_engine_save_blend, true); const reopened = lightEngine._web_engine_create(); open(lightEngine, reopened, saved); const after = snapshot(lightEngine, reopened); assert.deepEqual(after.lights, lightSnapshot.lights); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = lightEngine._malloc(malformed.byteLength); let malformedResult; try { lightEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = lightEngine._web_engine_open_blend(lightHandle, malformedPointer, malformed.byteLength); } finally { lightEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(lightEngine, lightHandle), lightSnapshot); lightEngine._web_engine_destroy(lightHandle); lightEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00012", operation: "LIGHT_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, lightFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, lightDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", lightId: light.id, lightType: light.lightType, fields: { energy: light.energy, exposure: light.exposure, temperature: light.temperature, useTemperature: light.useTemperature, castsShadow: light.castsShadow, areaSize: light.areaSize, areaSizeY: light.areaSizeY } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00013" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00012/light-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} light=${light.id} type=${light.lightType} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00013") { const materialFixture = path.join(root, "tests/files/web/generated/M16-GAP-00013-datablock-Material.blend"); const materialDesktopPath = path.join(root, "tests/golden/M16-GAP-00013/material-desktop-report.json"); fs.mkdirSync(path.dirname(materialDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-material-desktop.py"), "--", materialFixture, materialDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(materialDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const materialEngine = await factory({ wasmBinary }); const materialHandle = materialEngine._web_engine_create(); open(materialEngine, materialHandle, fs.readFileSync(materialFixture)); const materialSnapshot = snapshot(materialEngine, materialHandle); const material = materialSnapshot.materials?.find((value) => value.id === "material:WebGapMaterial"); assert.ok(material); const closeMaterial = (actual, expected, label) => assert.ok(Math.abs(actual - expected) <= 1e-5, `${label}: ${actual} != ${expected}`); const closeArray = (actual, expected, label) => expected.forEach((value, index) => closeMaterial(actual[index], value, `${label}[${index}]`)); closeArray(material.baseColor, [0.2, 0.4, 0.8, 0.9], "baseColor"); closeMaterial(material.roughness, 0.6, "roughness"); closeMaterial(material.metallic, 0.35, "metallic"); closeArray(material.emissionColor, [0.05, 0.1, 0.2, 1], "emissionColor"); closeMaterial(material.alpha, 0.9, "alpha"); closeMaterial(material.ior, 1.6, "ior"); closeMaterial(material.specularIORLevel, 0.35, "specularIORLevel"); closeMaterial(material.transmissionWeight, 0.27, "transmissionWeight"); closeMaterial(material.coatWeight, 0.64, "coatWeight"); closeMaterial(material.coatRoughness, 0.12, "coatRoughness"); closeMaterial(material.emissionStrength, 3.5, "emissionStrength"); assert.ok(material.nodes?.some((node) => node.type === "PRINCIPLED")); assert.ok(material.nodes?.some((node) => node.type === "OUTPUT")); assert.equal(material.links?.length, 1); assert.match(material.shaderGraphHash, /^[0-9a-f]{64}$/); const saved = output(materialEngine, materialHandle, materialEngine._web_engine_save_blend, true); const reopened = materialEngine._web_engine_create(); open(materialEngine, reopened, saved); const after = snapshot(materialEngine, reopened); assert.deepEqual(after.materials, materialSnapshot.materials); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = materialEngine._malloc(malformed.byteLength); let malformedResult; try { materialEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = materialEngine._web_engine_open_blend(materialHandle, malformedPointer, malformed.byteLength); } finally { materialEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(materialEngine, materialHandle), materialSnapshot); materialEngine._web_engine_destroy(materialHandle); materialEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00013", operation: "MATERIAL_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, materialFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, materialDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", materialId: material.id, shaderGraphHash: material.shaderGraphHash, nodeCount: material.nodes.length, linkCount: material.links.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00014" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00013/material-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} material=${material.id} nodes=${material.nodes.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00014") { const meshFixture = path.join(root, "tests/files/web/generated/M16-GAP-00014-datablock-Mesh.blend"); const meshDesktopPath = path.join(root, "tests/golden/M16-GAP-00014/mesh-desktop-report.json"); fs.mkdirSync(path.dirname(meshDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-mesh-desktop.py"), "--", meshFixture, meshDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(meshDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const meshEngine = await factory({ wasmBinary }); const meshHandle = meshEngine._web_engine_create(); open(meshEngine, meshHandle, fs.readFileSync(meshFixture)); const meshSnapshot = snapshot(meshEngine, meshHandle); const mesh = meshSnapshot.meshes?.find((value) => value.id === "mesh:WebGapMesh"); assert.ok(mesh); assert.equal(mesh.geometryStatus, "available"); assert.equal(mesh.vertexCount, desktopEvidence.mesh.vertexCount); assert.equal(mesh.edgeCount, desktopEvidence.mesh.edgeCount); assert.equal(mesh.faceCount, desktopEvidence.mesh.faceCount); assert.equal(mesh.cornerCount, desktopEvidence.mesh.cornerCount); assert.equal(mesh.triangleCount, desktopEvidence.mesh.triangleCount); assert.deepEqual(mesh.positions.map((value) => Number(value.toFixed(6))), desktopEvidence.mesh.positions); assert.deepEqual(mesh.indices, desktopEvidence.mesh.indices); assert.deepEqual(mesh.uvLayers?.map((layer) => layer.name), ["WebGapUV"]); const saved = output(meshEngine, meshHandle, meshEngine._web_engine_save_blend, true); const reopened = meshEngine._web_engine_create(); open(meshEngine, reopened, saved); const after = snapshot(meshEngine, reopened).meshes?.find((value) => value.id === mesh.id); assert.deepEqual(after, mesh); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = meshEngine._malloc(malformed.byteLength); let malformedResult; try { meshEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = meshEngine._web_engine_open_blend(meshHandle, malformedPointer, malformed.byteLength); } finally { meshEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(meshEngine, meshHandle), meshSnapshot); meshEngine._web_engine_destroy(meshHandle); meshEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00014", operation: "MESH_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, meshFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, meshDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, vertexCount: mesh.vertexCount, triangleCount: mesh.triangleCount, uvLayers: mesh.uvLayers?.map((layer) => layer.name) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00015" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00014/mesh-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} vertices=${mesh.vertexCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00015") { const metaballFixture = path.join(root, "tests/files/web/generated/M16-GAP-00015-datablock-MetaBall.blend"); const metaballDesktopPath = path.join(root, "tests/golden/M16-GAP-00015/metaball-desktop-report.json"); fs.mkdirSync(path.dirname(metaballDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-metaball-desktop.py"), "--", metaballFixture, metaballDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(metaballDesktopPath, "utf8")); const metaballEngine = await factory({ wasmBinary }); const metaballHandle = metaballEngine._web_engine_create(); open(metaballEngine, metaballHandle, fs.readFileSync(metaballFixture)); const metaballSnapshot = snapshot(metaballEngine, metaballHandle); const metaball = metaballSnapshot.nonMeshData?.find((value) => value.id === "metaball:WebGapMetaBall"); assert.ok(metaball); assert.equal(metaball.geometryStatus, "available"); assert.equal(metaball.pointCount, 2); assert.deepEqual(metaball.elements.map((item) => item.position.map((value) => Number(value.toFixed(6)))), desktopEvidence.metaball.elements.map((item) => item.position)); metaball.elements.forEach((item, index) => { assert.ok(Math.abs(item.radius - desktopEvidence.metaball.elements[index].radius) <= 1e-5); item.scale.forEach((value, axis) => assert.ok(Math.abs(value - desktopEvidence.metaball.elements[index].scale[axis]) <= 1e-5)); }); const saved = output(metaballEngine, metaballHandle, metaballEngine._web_engine_save_blend, true); const reopened = metaballEngine._web_engine_create(); open(metaballEngine, reopened, saved); assert.deepEqual(snapshot(metaballEngine, reopened).nonMeshData?.find((value) => value.id === metaball.id), metaball); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = metaballEngine._malloc(malformed.byteLength); let malformedResult; try { metaballEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = metaballEngine._web_engine_open_blend(metaballHandle, malformedPointer, malformed.byteLength); } finally { metaballEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(metaballEngine, metaballHandle), metaballSnapshot); metaballEngine._web_engine_destroy(metaballHandle); metaballEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00015", operation: "METABALL_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, metaballFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, metaballDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", metaballId: metaball.id, pointCount: metaball.pointCount, elementHash: crypto.createHash("sha256").update(JSON.stringify(metaball.elements)).digest("hex") }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00016" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00015/metaball-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} metaball=${metaball.id} points=${metaball.pointCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00016") { const nodeTreeFixture = path.join(root, "tests/files/web/generated/M16-GAP-00016-datablock-NodeTree.blend"); const nodeTreeDesktopPath = path.join(root, "tests/golden/M16-GAP-00016/nodetree-desktop-report.json"); fs.mkdirSync(path.dirname(nodeTreeDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-nodetree-desktop.py"), "--", nodeTreeFixture, nodeTreeDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(nodeTreeDesktopPath, "utf8")); const nodeTreeEngine = await factory({ wasmBinary }); const nodeTreeHandle = nodeTreeEngine._web_engine_create(); open(nodeTreeEngine, nodeTreeHandle, fs.readFileSync(nodeTreeFixture)); const nodeTreeSnapshot = snapshot(nodeTreeEngine, nodeTreeHandle); const material = nodeTreeSnapshot.materials?.find((value) => value.id === "material:WebGapNodeTreeMaterial"); assert.ok(material); assert.equal(material.nodes?.length, desktopEvidence.nodeTree.nodeTypes.length); assert.equal(material.links?.length, desktopEvidence.nodeTree.linkCount); assert.ok(material.nodes?.some((node) => node.type === "RGB")); assert.ok(material.nodes?.some((node) => node.type === "PRINCIPLED")); assert.ok(material.nodes?.some((node) => node.type === "OUTPUT")); assert.match(material.shaderGraphHash, /^[0-9a-f]{64}$/); const saved = output(nodeTreeEngine, nodeTreeHandle, nodeTreeEngine._web_engine_save_blend, true); const reopened = nodeTreeEngine._web_engine_create(); open(nodeTreeEngine, reopened, saved); const after = snapshot(nodeTreeEngine, reopened).materials?.find((value) => value.id === material.id); assert.deepEqual(after, material); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = nodeTreeEngine._malloc(malformed.byteLength); let malformedResult; try { nodeTreeEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = nodeTreeEngine._web_engine_open_blend(nodeTreeHandle, malformedPointer, malformed.byteLength); } finally { nodeTreeEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(nodeTreeEngine, nodeTreeHandle), nodeTreeSnapshot); nodeTreeEngine._web_engine_destroy(nodeTreeHandle); nodeTreeEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00016", operation: "NODETREE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, nodeTreeFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, nodeTreeDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", nodeTreeId: "nodetree:WebGapNodeTreeMaterial", nodeCount: material.nodes.length, linkCount: material.links.length, shaderGraphHash: material.shaderGraphHash }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00017" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00016/nodetree-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} nodeTree=${report.wasm.nodeTreeId} nodes=${material.nodes.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00017") { const objectFixture = path.join(root, "tests/files/web/generated/M16-GAP-00017-datablock-Object.blend"); const objectDesktopPath = path.join(root, "tests/golden/M16-GAP-00017/object-desktop-report.json"); fs.mkdirSync(path.dirname(objectDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-object-desktop.py"), "--", objectFixture, objectDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(objectDesktopPath, "utf8")); const objectEngine = await factory({ wasmBinary }); const objectHandle = objectEngine._web_engine_create(); open(objectEngine, objectHandle, fs.readFileSync(objectFixture)); const objectSnapshot = snapshot(objectEngine, objectHandle); const object = objectSnapshot.nodes?.find((value) => value.id === "object:WebGapObject"); assert.ok(object); assert.equal(object.type, "EMPTY"); assert.equal(object.visible, desktopEvidence.object.visible); assert.equal(object.selectable, desktopEvidence.object.selectable); assert.deepEqual(object.transform.translation.map((value) => Number(value.toFixed(6))), desktopEvidence.object.location); object.transform.rotationEuler.forEach((value, index) => assert.ok(Math.abs(value - desktopEvidence.object.rotationEuler[index]) <= 1e-5)); object.transform.scale.forEach((value, index) => assert.ok(Math.abs(value - desktopEvidence.object.scale[index]) <= 1e-5)); assert.equal(object.parentId, null); const saved = output(objectEngine, objectHandle, objectEngine._web_engine_save_blend, true); const reopened = objectEngine._web_engine_create(); open(objectEngine, reopened, saved); assert.deepEqual(snapshot(objectEngine, reopened).nodes?.find((value) => value.id === object.id), object); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = objectEngine._malloc(malformed.byteLength); let malformedResult; try { objectEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = objectEngine._web_engine_open_blend(objectHandle, malformedPointer, malformed.byteLength); } finally { objectEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(objectEngine, objectHandle), objectSnapshot); objectEngine._web_engine_destroy(objectHandle); objectEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00017", operation: "OBJECT_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, objectFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, objectDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", objectId: object.id, type: object.type, translation: object.transform.translation, scale: object.transform.scale }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00018" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00017/object-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} object=${object.id} type=${object.type} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00018") { const particleSettingsFixture = path.join(root, "tests/files/web/generated/M16-GAP-00018-datablock-ParticleSettings.blend"); const particleSettingsDesktopPath = path.join(root, "tests/golden/M16-GAP-00018/particle-settings-desktop-report.json"); fs.mkdirSync(path.dirname(particleSettingsDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-particle-settings-desktop.py"), "--", particleSettingsFixture, particleSettingsDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(particleSettingsDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const particleSettingsEngine = await factory({ wasmBinary }); const particleSettingsHandle = particleSettingsEngine._web_engine_create(); open(particleSettingsEngine, particleSettingsHandle, fs.readFileSync(particleSettingsFixture)); const particleSettingsSnapshot = snapshot(particleSettingsEngine, particleSettingsHandle); const particleSettings = particleSettingsSnapshot.particleSettings?.find((value) => value.id === "particle-settings:WebGapParticleSettings"); assert.ok(particleSettings); assert.deepEqual(particleSettings, desktopEvidence.particleSettings); assert.equal(particleSettings.type, 0); assert.equal(particleSettings.from, 0); assert.equal(particleSettings.distribution, 0); assert.equal(particleSettings.physicsType, 0); assert.equal(particleSettings.totalParticles, 128); assert.equal(particleSettings.start, 4); assert.equal(particleSettings.end, 96); assert.equal(particleSettings.lifetime, 32); assert.equal(particleSettings.size, 0.125); assert.equal(particleSettings.drawSize, 0.25); const saved = output(particleSettingsEngine, particleSettingsHandle, particleSettingsEngine._web_engine_save_blend, true); const reopened = particleSettingsEngine._web_engine_create(); open(particleSettingsEngine, reopened, saved); const after = snapshot(particleSettingsEngine, reopened).particleSettings?.find((value) => value.id === particleSettings.id); assert.deepEqual(after, particleSettings); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = particleSettingsEngine._malloc(malformed.byteLength); let malformedResult; try { particleSettingsEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = particleSettingsEngine._web_engine_open_blend(particleSettingsHandle, malformedPointer, malformed.byteLength); } finally { particleSettingsEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(particleSettingsEngine, particleSettingsHandle), particleSettingsSnapshot); particleSettingsEngine._web_engine_destroy(particleSettingsHandle); particleSettingsEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00018", operation: "PARTICLE_SETTINGS_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, particleSettingsFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, particleSettingsDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", particleSettingsId: particleSettings.id, fields: { type: particleSettings.type, from: particleSettings.from, distribution: particleSettings.distribution, physicsType: particleSettings.physicsType, totalParticles: particleSettings.totalParticles, start: particleSettings.start, end: particleSettings.end, lifetime: particleSettings.lifetime, size: particleSettings.size, drawSize: particleSettings.drawSize } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00019" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00018/particle-settings-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} particleSettings=${particleSettings.id} count=${particleSettings.totalParticles} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00019") { const pointCloudFixture = path.join(root, "tests/files/web/generated/M16-GAP-00019-datablock-PointCloud.blend"); const pointCloudDesktopPath = path.join(root, "tests/golden/M16-GAP-00019/pointcloud-desktop-report.json"); fs.mkdirSync(path.dirname(pointCloudDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-pointcloud-desktop.py"), "--", pointCloudFixture, pointCloudDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(pointCloudDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const pointCloudEngine = await factory({ wasmBinary }); const pointCloudHandle = pointCloudEngine._web_engine_create(); open(pointCloudEngine, pointCloudHandle, fs.readFileSync(pointCloudFixture)); const pointCloudSnapshot = snapshot(pointCloudEngine, pointCloudHandle); const pointCloud = pointCloudSnapshot.nonMeshData?.find((value) => value.id === "pointcloud:WebGapPointCloud"); assert.ok(pointCloud); assert.equal(pointCloud.type, "POINT_CLOUD"); assert.equal(pointCloud.geometryStatus, "available"); assert.equal(pointCloud.pointCount, desktopEvidence.pointCloud.pointCount); assert.deepEqual(pointCloud.controlPoints, desktopEvidence.pointCloud.controlPoints); assert.deepEqual(pointCloud.radii, desktopEvidence.pointCloud.radii); const saved = output(pointCloudEngine, pointCloudHandle, pointCloudEngine._web_engine_save_blend, true); const reopened = pointCloudEngine._web_engine_create(); open(pointCloudEngine, reopened, saved); const after = snapshot(pointCloudEngine, reopened).nonMeshData?.find((value) => value.id === pointCloud.id); assert.deepEqual(after, pointCloud); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = pointCloudEngine._malloc(malformed.byteLength); let malformedResult; try { pointCloudEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = pointCloudEngine._web_engine_open_blend(pointCloudHandle, malformedPointer, malformed.byteLength); } finally { pointCloudEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(pointCloudEngine, pointCloudHandle), pointCloudSnapshot); pointCloudEngine._web_engine_destroy(pointCloudHandle); pointCloudEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00019", operation: "POINT_CLOUD_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, pointCloudFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, pointCloudDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", pointCloudId: pointCloud.id, pointCount: pointCloud.pointCount, geometryStatus: pointCloud.geometryStatus, pointHash: crypto.createHash("sha256").update(JSON.stringify(pointCloud.controlPoints)).digest("hex"), radiusHash: crypto.createHash("sha256").update(JSON.stringify(pointCloud.radii)).digest("hex") }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00020" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00019/pointcloud-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} pointCloud=${pointCloud.id} points=${pointCloud.pointCount} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00020") { const sceneFixture = path.join(root, "tests/files/web/generated/M16-GAP-00020-datablock-Scene.blend"); const sceneDesktopPath = path.join(root, "tests/golden/M16-GAP-00020/scene-desktop-report.json"); fs.mkdirSync(path.dirname(sceneDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-scene-desktop.py"), "--", sceneFixture, sceneDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(sceneDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const sceneEngine = await factory({ wasmBinary }); const sceneHandle = sceneEngine._web_engine_create(); open(sceneEngine, sceneHandle, fs.readFileSync(sceneFixture)); const sceneSnapshot = snapshot(sceneEngine, sceneHandle); const scene = sceneSnapshot.scenes?.find((value) => value.id === "scene:WebGapScene"); assert.ok(scene); const expected = desktopEvidence.scene; assert.equal(scene.name, expected.name); assert.equal(scene.renderEngine, expected.renderEngine); assert.equal(sceneSnapshot.frame.current, expected.frameCurrent); assert.equal(sceneSnapshot.frame.start, expected.frameStart); assert.equal(sceneSnapshot.frame.end, expected.frameEnd); assert.equal(sceneSnapshot.coordinateSystem.unitSystem, expected.unitSystem); assert.equal(sceneSnapshot.coordinateSystem.unitScale, expected.unitScale); assert.equal(sceneSnapshot.scenes.length, 1); assert.equal(scene.id, expected.id); const saved = output(sceneEngine, sceneHandle, sceneEngine._web_engine_save_blend, true); const reopened = sceneEngine._web_engine_create(); open(sceneEngine, reopened, saved); assert.deepEqual(snapshot(sceneEngine, reopened).scenes, sceneSnapshot.scenes); assert.deepEqual(snapshot(sceneEngine, reopened).frame, sceneSnapshot.frame); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = sceneEngine._malloc(malformed.byteLength); let malformedResult; try { sceneEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = sceneEngine._web_engine_open_blend(sceneHandle, malformedPointer, malformed.byteLength); } finally { sceneEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(sceneEngine, sceneHandle), sceneSnapshot); sceneEngine._web_engine_destroy(sceneHandle); sceneEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00020", operation: "SCENE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, sceneFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, sceneDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", sceneId: scene.id, frame: sceneSnapshot.frame, renderEngine: scene.renderEngine, unitSystem: sceneSnapshot.coordinateSystem.unitSystem, unitScale: sceneSnapshot.coordinateSystem.unitScale }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00021" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00020/scene-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} scene=${scene.id} frames=${sceneSnapshot.frame.start}-${sceneSnapshot.frame.end} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00021") { const screenFixture = path.join(root, "tests/files/web/generated/M16-GAP-00021-datablock-Screen.blend"); const screenDesktopPath = path.join(root, "tests/golden/M16-GAP-00021/screen-desktop-report.json"); fs.mkdirSync(path.dirname(screenDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-screen-desktop.py"), "--", screenFixture, screenDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(screenDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const screenEngine = await factory({ wasmBinary }); const screenHandle = screenEngine._web_engine_create(); open(screenEngine, screenHandle, fs.readFileSync(screenFixture)); const screenSnapshot = snapshot(screenEngine, screenHandle); assert.equal(screenSnapshot.editorWorkflowStatus, "AVAILABLE"); const workflow = screenSnapshot.editorWorkflow; const desktopEditors = desktopEvidence.screen.editors.map((editor) => ({DOPESHEET_EDITOR: "DOPE_SHEET", VIEW_3D: "VIEW_3D", OUTLINER: "OUTLINER", PROPERTIES: "PROPERTIES"}[editor] ?? editor)).sort(); const matchingWorkspace = workflow.workspaces.find((value) => value.areas.length === desktopEvidence.screen.areaCount && value.areas.map((area) => area.editor).sort().join(",") === desktopEditors.join(",")); assert.ok(matchingWorkspace); assert.ok(matchingWorkspace.areas.reduce((count, area) => count + area.regions.length, 0) <= desktopEvidence.screen.regionCount); const saved = output(screenEngine, screenHandle, screenEngine._web_engine_save_blend, true); const reopened = screenEngine._web_engine_create(); open(screenEngine, reopened, saved); assert.deepEqual(snapshot(screenEngine, reopened).editorWorkflow, screenSnapshot.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = screenEngine._malloc(malformed.byteLength); let malformedResult; try { screenEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = screenEngine._web_engine_open_blend(screenHandle, malformedPointer, malformed.byteLength); } finally { screenEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(screenEngine, screenHandle), screenSnapshot); screenEngine._web_engine_destroy(screenHandle); screenEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00021", operation: "SCREEN_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, screenFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, screenDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", workflowStatus: screenSnapshot.editorWorkflowStatus, workspaceId: matchingWorkspace.id, areaCount: matchingWorkspace.areas.length, editorTypes: matchingWorkspace.areas.map((area) => area.editor).sort() }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00022" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00021/screen-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} screen=${desktopEvidence.screen.id} areas=${matchingWorkspace.areas.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00022") { const soundFixture = path.join(root, "tests/files/web/generated/M16-GAP-00022-datablock-Sound.blend"); const soundDesktopPath = path.join(root, "tests/golden/M16-GAP-00022/sound-desktop-report.json"); fs.mkdirSync(path.dirname(soundDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-sound-desktop.py"), "--", soundFixture, soundDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(soundDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const soundEngine = await factory({ wasmBinary }); const soundHandle = soundEngine._web_engine_create(); open(soundEngine, soundHandle, fs.readFileSync(soundFixture)); const soundSnapshot = snapshot(soundEngine, soundHandle); const sound = soundSnapshot.sounds?.find((value) => value.id === "sound:WebGapSound"); assert.ok(sound); assert.deepEqual(sound, desktopEvidence.sound); const saved = output(soundEngine, soundHandle, soundEngine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = soundEngine._web_engine_create(); open(soundEngine, reopened, saved); const after = snapshot(soundEngine, reopened).sounds?.find((value) => value.id === sound.id); assert.deepEqual(after, sound); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = soundEngine._malloc(malformed.byteLength); let malformedResult; try { soundEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = soundEngine._web_engine_open_blend(soundHandle, malformedPointer, malformed.byteLength); } finally { soundEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(soundEngine, soundHandle), soundSnapshot); soundEngine._web_engine_destroy(soundHandle); soundEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00022", operation: "SOUND_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, soundFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, soundDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", soundId: sound.id, sourcePath: sound.sourcePath, packed: sound.packed, volume: sound.volume, pitch: sound.pitch, audioChannels: sound.audioChannels, sampleRate: sound.sampleRate }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00023" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00022/sound-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} sound=${sound.id} sampleRate=${sound.sampleRate} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00023") { const speakerFixture = path.join(root, "tests/files/web/generated/M16-GAP-00023-datablock-Speaker.blend"); const speakerDesktopPath = path.join(root, "tests/golden/M16-GAP-00023/speaker-desktop-report.json"); fs.mkdirSync(path.dirname(speakerDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-speaker-desktop.py"), "--", speakerFixture, speakerDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(speakerDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const speakerEngine = await factory({ wasmBinary }); const speakerHandle = speakerEngine._web_engine_create(); open(speakerEngine, speakerHandle, fs.readFileSync(speakerFixture)); const speakerSnapshot = snapshot(speakerEngine, speakerHandle); const speaker = speakerSnapshot.speakers?.find((value) => value.id === "speaker:WebGapSpeaker"); assert.ok(speaker); assert.deepEqual(speaker, desktopEvidence.speaker); const saved = output(speakerEngine, speakerHandle, speakerEngine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = speakerEngine._web_engine_create(); open(speakerEngine, reopened, saved); const after = snapshot(speakerEngine, reopened).speakers?.find((value) => value.id === speaker.id); assert.deepEqual(after, speaker); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = speakerEngine._malloc(malformed.byteLength); let malformedResult; try { speakerEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = speakerEngine._web_engine_open_blend(speakerHandle, malformedPointer, malformed.byteLength); } finally { speakerEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(speakerEngine, speakerHandle), speakerSnapshot); speakerEngine._web_engine_destroy(speakerHandle); speakerEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00023", operation: "SPEAKER_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, speakerFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, speakerDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", speakerId: speaker.id, soundId: speaker.soundId, volumeMax: speaker.volumeMax, volumeMin: speaker.volumeMin, distanceMax: speaker.distanceMax, distanceReference: speaker.distanceReference, attenuation: speaker.attenuation, coneAngleOuter: speaker.coneAngleOuter, coneAngleInner: speaker.coneAngleInner, coneVolumeOuter: speaker.coneVolumeOuter, volume: speaker.volume, pitch: speaker.pitch }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00024" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00023/speaker-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} speaker=${speaker.id} volume=${speaker.volume} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00024") { const textFixture = path.join(root, "tests/files/web/generated/M16-GAP-00024-datablock-Text.blend"); const textDesktopPath = path.join(root, "tests/golden/M16-GAP-00024/text-desktop-report.json"); fs.mkdirSync(path.dirname(textDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-text-desktop.py"), "--", textFixture, textDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(textDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const textEngine = await factory({ wasmBinary }); const textHandle = textEngine._web_engine_create(); open(textEngine, textHandle, fs.readFileSync(textFixture)); const textSnapshot = snapshot(textEngine, textHandle); const text = textSnapshot.texts?.find((value) => value.id === "text:WebGapText"); assert.ok(text); assert.deepEqual(text, desktopEvidence.text); const saved = output(textEngine, textHandle, textEngine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = textEngine._web_engine_create(); open(textEngine, reopened, saved); const after = snapshot(textEngine, reopened).texts?.find((value) => value.id === text.id); assert.deepEqual(after, text); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = textEngine._malloc(malformed.byteLength); let malformedResult; try { textEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = textEngine._web_engine_open_blend(textHandle, malformedPointer, malformed.byteLength); } finally { textEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(textEngine, textHandle), textSnapshot); textEngine._web_engine_destroy(textHandle); textEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00024", operation: "TEXT_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, textFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, textDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", textId: text.id, sourceSha256: text.sourceSha256, byteLength: text.byteLength, lineCount: text.lineCount, sourcePath: text.sourcePath, internal: text.internal, isDirty: text.isDirty, useModule: text.useModule }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00025" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00024/text-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} text=${text.id} bytes=${text.byteLength} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00025") { const textureFixture = path.join(root, "tests/files/web/generated/M16-GAP-00025-datablock-Texture.blend"); const textureDesktopPath = path.join(root, "tests/golden/M16-GAP-00025/texture-desktop-report.json"); fs.mkdirSync(path.dirname(textureDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-texture-desktop.py"), "--", textureFixture, textureDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(textureDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const textureEngine = await factory({ wasmBinary }); const textureHandle = textureEngine._web_engine_create(); open(textureEngine, textureHandle, fs.readFileSync(textureFixture)); const textureSnapshot = snapshot(textureEngine, textureHandle); const texture = textureSnapshot.textures?.find((value) => value.id === "texture:WebGapTexture"); assert.ok(texture); assert.equal(texture.type, 1); assert.deepEqual(texture, desktopEvidence.texture); const saved = output(textureEngine, textureHandle, textureEngine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = textureEngine._web_engine_create(); open(textureEngine, reopened, saved); const after = snapshot(textureEngine, reopened).textures?.find((value) => value.id === texture.id); assert.deepEqual(after, texture); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = textureEngine._malloc(malformed.byteLength); let malformedResult; try { textureEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = textureEngine._web_engine_open_blend(textureHandle, malformedPointer, malformed.byteLength); } finally { textureEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(textureEngine, textureHandle), textureSnapshot); textureEngine._web_engine_destroy(textureHandle); textureEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00025", operation: "TEXTURE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, textureFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, textureDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", textureId: texture.id, type: texture.type, noiseScale: texture.noiseScale, noiseDepth: texture.noiseDepth, intensity: texture.intensity, contrast: texture.contrast, saturation: texture.saturation }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00026" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00025/texture-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} texture=${texture.id} type=${texture.type} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00026") { const volumeFixture = path.join(root, "tests/files/web/generated/M16-GAP-00026-datablock-Volume.blend"); const volumeDesktopPath = path.join(root, "tests/golden/M16-GAP-00026/volume-desktop-report.json"); fs.mkdirSync(path.dirname(volumeDesktopPath), { recursive: true }); const desktopRun = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-volume-desktop.py"), "--", volumeFixture, volumeDesktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktopRun.status, 0, `${desktopRun.stdout ?? ""}\n${desktopRun.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(volumeDesktopPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); const volumeEngine = await factory({ wasmBinary }); const volumeHandle = volumeEngine._web_engine_create(); open(volumeEngine, volumeHandle, fs.readFileSync(volumeFixture)); const volumeSnapshot = snapshot(volumeEngine, volumeHandle); const volume = volumeSnapshot.nonMeshData?.find((value) => value.id === "volume:WebGapVolume"); assert.ok(volume); assert.equal(volume.geometryStatus, "blocked"); assert.deepEqual({ id: volume.id, name: volume.name, sourcePath: volume.sourcePath, volumeProperties: volume.volumeProperties }, { id: desktopEvidence.volume.id, name: desktopEvidence.volume.name, sourcePath: desktopEvidence.volume.sourcePath, volumeProperties: { displayDensity: desktopEvidence.volume.displayDensity, interpolation: desktopEvidence.volume.interpolation, stepSize: desktopEvidence.volume.stepSize, velocityGrid: desktopEvidence.volume.velocityGrid, velocityScale: desktopEvidence.volume.velocityScale } }); const saved = output(volumeEngine, volumeHandle, volumeEngine._web_engine_save_blend, true); const reopened = volumeEngine._web_engine_create(); open(volumeEngine, reopened, saved); const after = snapshot(volumeEngine, reopened).nonMeshData?.find((value) => value.id === volume.id); assert.deepEqual(after, volume); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const malformedPointer = volumeEngine._malloc(malformed.byteLength); let malformedResult; try { volumeEngine.HEAPU8.set(malformed, malformedPointer); malformedResult = volumeEngine._web_engine_open_blend(volumeHandle, malformedPointer, malformed.byteLength); } finally { volumeEngine._free(malformedPointer); } assert.notEqual(malformedResult, 0); assert.deepEqual(snapshot(volumeEngine, volumeHandle), volumeSnapshot); volumeEngine._web_engine_destroy(volumeHandle); volumeEngine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00026", operation: "VOLUME_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, volumeFixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, volumeDesktopPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", volumeId: volume.id, geometryStatus: volume.geometryStatus, sourcePath: volume.sourcePath, volumeProperties: volume.volumeProperties }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00027" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00026/volume-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} volume=${volume.id} density=${volume.volumeProperties.displayDensity} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00028") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00028-datablock-WorkSpace.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00028/workspace-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-workspace-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const workspace = before.workspaces?.find((value) => value.id === "workspace:WebGapWorkspace"); assert.ok(workspace); assert.deepEqual(workspace, desktop.workspace); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).workspaces?.find((value) => value.id === workspace.id), workspace); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00028", operation: "WORKSPACE_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", workspace: workspace }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00029" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00028/workspace-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} workspace=${workspace.id} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00029") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00029-datablock-World.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00029/world-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-world-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const world = before.worlds?.find((value) => value.id === "world:WebGapWorld"); assert.ok(world); assert.deepEqual(world, desktop.world); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).worlds?.find((value) => value.id === world.id), world); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00029", operation: "WORLD_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", world: world }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00030" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00029/world-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} world=${world.id} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00030") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00030-modifier-ARMATURE.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00030/armature-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-armature-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "ARMATURE"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.equal(modifier.parameters.armatureObjectId, desktop.modifier.armatureObjectId); assert.equal(modifier.parameters.vertexGroup, desktop.modifier.modifier.vertexGroup); assert.equal(modifier.parameters.deformFlags, 1); assert.ok(modifier.dependsOn.includes(desktop.modifier.armatureObjectId)); assert.equal(modifier.enabled, desktop.modifier.modifier.enabled); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id); assert.deepEqual(after?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00030", operation: "ARMATURE_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier: modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00031" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00030/armature-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00031") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00031-modifier-ARRAY.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00031/array-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-array-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "ARRAY"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.equal(modifier.parameters.count, desktop.modifier.modifier.count); assert.deepEqual(modifier.parameters.targetObjectIds, [desktop.modifier.modifier.startCapObjectId, desktop.modifier.modifier.endCapObjectId]); assert.ok(modifier.dependsOn.includes(desktop.modifier.modifier.startCapObjectId)); assert.ok(modifier.dependsOn.includes(desktop.modifier.modifier.endCapObjectId)); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00031", operation: "ARRAY_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier: modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00032" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00031/array-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} count=${modifier.parameters.count} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00032") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00032-modifier-BEVEL.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00032/bevel-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-bevel-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "BEVEL"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.ok(Math.abs(modifier.parameters.width - desktop.modifier.modifier.width) < 1e-5); assert.equal(modifier.parameters.segments, desktop.modifier.modifier.segments); assert.ok(Math.abs(modifier.parameters.profile - desktop.modifier.modifier.profile) < 1e-5); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00032", operation: "BEVEL_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier: modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00033" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00032/bevel-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} width=${modifier.parameters.width} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00033") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00033-modifier-BOOLEAN.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00033/boolean-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-boolean-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "BOOLEAN"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.equal(modifier.parameters.operation, 2); assert.equal(modifier.parameters.solver, 1); assert.equal(modifier.parameters.targetObjectIds[0], desktop.modifier.modifier.targetObjectId); assert.ok(modifier.dependsOn.includes(desktop.modifier.modifier.targetObjectId)); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00033", operation: "BOOLEAN_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier: modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00034" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00033/boolean-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} operation=${modifier.parameters.operation} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00034") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00034-modifier-BUILD.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00034/build-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-build-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "BUILD"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.ok(Math.abs(modifier.parameters.start - desktop.modifier.modifier.start) < 1e-5); assert.ok(Math.abs(modifier.parameters.length - desktop.modifier.modifier.length) < 1e-5); assert.equal(modifier.parameters.flag, 5); assert.equal(modifier.parameters.seed, desktop.modifier.modifier.seed); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00034", operation: "BUILD_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier: modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00035" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00034/build-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} start=${modifier.parameters.start} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00035") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00035-modifier-CAST.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00035/cast-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-cast-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "CAST"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.ok(Math.abs(modifier.parameters.factor - desktop.modifier.modifier.factor) < 1e-5); assert.ok(Math.abs(modifier.parameters.radius - desktop.modifier.modifier.radius) < 1e-5); assert.equal(modifier.parameters.mode, 3); assert.equal(modifier.parameters.flag, 5); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00035", operation: "CAST_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00036" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00035/cast-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} factor=${modifier.parameters.factor} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00036") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00036-modifier-CLOTH.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00036/cloth-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-cloth-modifier-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = before.meshes?.find((value) => value.id === desktop.modifier.meshId); assert.ok(mesh); const modifier = mesh.modifierStack?.find((value) => value.type === "CLOTH"); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).meshes?.find((value) => value.id === mesh.id)?.modifierStack, mesh.modifierStack); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00036", operation: "CLOTH_MODIFIER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", meshId: mesh.id, modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00037" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00036/cloth-modifier-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00037") { const fixture=path.join(root,"tests/files/web/generated/M16-GAP-00037-modifier-COLLISION.blend"), desktopPath=path.join(root,"tests/golden/M16-GAP-00037/collision-modifier-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath),{recursive:true}); const run=spawnSync(process.env.BLENDER_BIN??path.join(root,"build_blender_5.2.0/bin/blender"),["-b","--factory-startup","--python",path.join(root,"tools/web/check-collision-modifier-desktop.py"),"--",fixture,desktopPath],{cwd:root,encoding:"utf8",maxBuffer:8*1024*1024}); assert.equal(run.status,0,`${run.stdout??""}\n${run.stderr??""}`); const desktop=JSON.parse(fs.readFileSync(desktopPath,"utf8")); const engine=await factory({wasmBinary}); const handle=engine._web_engine_create(); open(engine,handle,fs.readFileSync(fixture)); const before=snapshot(engine,handle); const mesh=before.meshes?.find(v=>v.id===desktop.modifier.meshId); assert.ok(mesh); const modifier=mesh.modifierStack?.find(v=>v.type==="COLLISION"); assert.ok(modifier); assert.equal(modifier.name,desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode,desktop.modifier.modifier.typeCode); assert.equal(modifier.showViewport,desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender,desktop.modifier.modifier.showRender); const saved=output(engine,handle,engine._web_engine_save_blend,true), reopened=engine._web_engine_create(); open(engine,reopened,saved); assert.deepEqual(snapshot(engine,reopened).meshes?.find(v=>v.id===mesh.id)?.modifierStack,mesh.modifierStack); const malformed=new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]), pointer=engine._malloc(malformed.byteLength); let result; try{engine.HEAPU8.set(malformed,pointer); result=engine._web_engine_open_blend(handle,pointer,malformed.byteLength)}finally{engine._free(pointer)} assert.notEqual(result,0); assert.deepEqual(snapshot(engine,handle),before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report={schemaVersion:1,task:"M16-GAP-00037",operation:"COLLISION_MODIFIER_LOCAL_EXACT",fixture:{path:path.relative(root,fixture).replaceAll(path.sep,"/"),sha256:desktop.fixtureSha256},desktop:{status:"EXACT",report:path.relative(root,desktopPath).replaceAll(path.sep,"/"),saveReopen:desktop.saveReopen},wasm:{status:"EXACT",meshId:mesh.id,modifier},saveReopen:"EXACT",negative:{malformedBlend:"REJECTED_WITHOUT_MAIN_MUTATION"},nextTask:"M16-GAP-00038"}; const reportPath=path.join(root,"tests/golden/M16-GAP-00037/collision-modifier-local-exact-report.json"); fs.writeFileSync(reportPath,`${JSON.stringify(report,null,2)}\n`); process.stdout.write(`generated-gap-ok task=${task} mesh=${mesh.id} modifier=${modifier.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } const genericModifierTasks = { "M16-GAP-00038": { type: "CORRECTIVE_SMOOTH", stem: "WebGapCorrectiveSmooth", nextTask: "M16-GAP-00039" }, "M16-GAP-00039": { type: "CURVE", stem: "WebGapCurveModifier", nextTask: "M16-GAP-00040" }, "M16-GAP-00040": { type: "DATA_TRANSFER", stem: "WebGapDataTransfer", nextTask: "M16-GAP-00041" }, "M16-GAP-00041": { type: "DECIMATE", stem: "WebGapDecimate", nextTask: "M16-GAP-00042" }, "M16-GAP-00042": { type: "DISPLACE", stem: "WebGapDisplace", nextTask: "M16-GAP-00043" }, "M16-GAP-00043": { type: "DYNAMIC_PAINT", stem: "WebGapDynamicPaint", nextTask: "M16-GAP-00044" }, "M16-GAP-00044": { type: "EDGE_SPLIT", stem: "WebGapEdgeSplit", nextTask: "M16-GAP-00045" }, "M16-GAP-00045": { type: "EXPLODE", stem: "WebGapExplode", nextTask: "M16-GAP-00046" }, "M16-GAP-00046": { type: "FLUID", stem: "WebGapFluid", nextTask: "M16-GAP-00047" }, "M16-GAP-00047": { type: "GREASE_PENCIL_ARMATURE", stem: "WebGapGreasePencilArmature", objectName: "GreasePencilObject", nextTask: "M16-GAP-00048" }, "M16-GAP-00048": { type: "GREASE_PENCIL_ARRAY", stem: "WebGapGreasePencilArray", objectName: "GreasePencilObject", nextTask: "M16-GAP-00049" }, "M16-GAP-00049": { type: "GREASE_PENCIL_BUILD", stem: "WebGapGreasePencilBuild", objectName: "GreasePencilObject", nextTask: "M16-GAP-00050" }, "M16-GAP-00050": { type: "GREASE_PENCIL_COLOR", stem: "WebGapGreasePencilColor", objectName: "GreasePencilObject", nextTask: "M16-GAP-00051" }, "M16-GAP-00051": { type: "GREASE_PENCIL_DASH", stem: "WebGapGreasePencilDash", objectName: "GreasePencilObject", nextTask: "M16-GAP-00052" }, "M16-GAP-00052": { type: "GREASE_PENCIL_ENVELOPE", stem: "WebGapGreasePencilEnvelope", objectName: "GreasePencilObject", nextTask: "M16-GAP-00053" }, "M16-GAP-00053": { type: "GREASE_PENCIL_HOOK", stem: "WebGapGreasePencilHook", objectName: "GreasePencilObject", nextTask: "M16-GAP-00054" }, "M16-GAP-00054": { type: "GREASE_PENCIL_LATTICE", stem: "WebGapGreasePencilLattice", objectName: "GreasePencilObject", nextTask: "M16-GAP-00055" }, "M16-GAP-00055": { type: "GREASE_PENCIL_LENGTH", stem: "WebGapGreasePencilLength", objectName: "GreasePencilObject", nextTask: "M16-GAP-00056" }, "M16-GAP-00056": { type: "GREASE_PENCIL_MIRROR", stem: "WebGapGreasePencilMirror", objectName: "GreasePencilObject", nextTask: "M16-GAP-00057" }, "M16-GAP-00057": { type: "GREASE_PENCIL_MULTIPLY", stem: "WebGapGreasePencilMultiply", objectName: "GreasePencilObject", nextTask: "M16-GAP-00058" }, "M16-GAP-00058": { type: "GREASE_PENCIL_NOISE", stem: "WebGapGreasePencilNoise", objectName: "GreasePencilObject", nextTask: "M16-GAP-00059" }, "M16-GAP-00059": { type: "GREASE_PENCIL_OFFSET", stem: "WebGapGreasePencilOffset", objectName: "GreasePencilObject", nextTask: "M16-GAP-00060" }, "M16-GAP-00060": { type: "GREASE_PENCIL_OPACITY", stem: "WebGapGreasePencilOpacity", objectName: "GreasePencilObject", nextTask: "M16-GAP-00061" }, "M16-GAP-00061": { type: "GREASE_PENCIL_OUTLINE", stem: "WebGapGreasePencilOutline", objectName: "GreasePencilObject", nextTask: "M16-GAP-00062" }, "M16-GAP-00062": { type: "GREASE_PENCIL_SHRINKWRAP", stem: "WebGapGreasePencilShrinkwrap", objectName: "GreasePencilObject", nextTask: "M16-GAP-00063" }, "M16-GAP-00063": { type: "GREASE_PENCIL_SIMPLIFY", stem: "WebGapGreasePencilSimplify", objectName: "GreasePencilObject", nextTask: "M16-GAP-00064" }, "M16-GAP-00064": { type: "GREASE_PENCIL_SMOOTH", stem: "WebGapGreasePencilSmooth", objectName: "GreasePencilObject", nextTask: "M16-GAP-00065" }, "M16-GAP-00065": { type: "GREASE_PENCIL_SUBDIV", stem: "WebGapGreasePencilSubdiv", objectName: "GreasePencilObject", nextTask: "M16-GAP-00066" }, "M16-GAP-00066": { type: "GREASE_PENCIL_TEXTURE", stem: "WebGapGreasePencilTexture", objectName: "GreasePencilObject", nextTask: "M16-GAP-00067" }, "M16-GAP-00067": { type: "GREASE_PENCIL_THICKNESS", stem: "WebGapGreasePencilThickness", objectName: "GreasePencilObject", nextTask: "M16-GAP-00068" }, "M16-GAP-00068": { type: "GREASE_PENCIL_TIME", stem: "WebGapGreasePencilTime", objectName: "GreasePencilObject", nextTask: "M16-GAP-00069" }, "M16-GAP-00069": { type: "GREASE_PENCIL_TINT", stem: "WebGapGreasePencilTint", objectName: "GreasePencilObject", nextTask: "M16-GAP-00070" }, "M16-GAP-00070": { type: "GREASE_PENCIL_VERTEX_WEIGHT_ANGLE", stem: "WebGapGreasePencilVertexWeightAngle", objectName: "GreasePencilObject", nextTask: "M16-GAP-00071" }, "M16-GAP-00071": { type: "GREASE_PENCIL_VERTEX_WEIGHT_PROXIMITY", stem: "WebGapGreasePencilVertexWeightProximity", objectName: "GreasePencilObject", nextTask: "M16-GAP-00072" }, "M16-GAP-00072": { type: "HOOK", stem: "WebGapHook", nextTask: "M16-GAP-00073" }, "M16-GAP-00073": { type: "LAPLACIANDEFORM", stem: "WebGapLaplacianDeform", nextTask: "M16-GAP-00074" }, "M16-GAP-00074": { type: "LAPLACIANSMOOTH", stem: "WebGapLaplacianSmooth", nextTask: "M16-GAP-00075" }, "M16-GAP-00075": { type: "LATTICE", stem: "WebGapLatticeModifier", nextTask: "M16-GAP-00076" }, "M16-GAP-00076": { type: "LINEART", stem: "WebGapLineart", objectName: "GreasePencilObject", nextTask: "M16-GAP-00077" }, "M16-GAP-00077": { type: "MASK", stem: "WebGapMask", nextTask: "M16-GAP-00078" }, "M16-GAP-00078": { type: "MESH_CACHE", stem: "WebGapMeshCache", nextTask: "M16-GAP-00079" }, "M16-GAP-00079": { type: "MESH_DEFORM", stem: "WebGapMeshDeform", nextTask: "M16-GAP-00080" }, "M16-GAP-00080": { type: "MESH_SEQUENCE_CACHE", stem: "WebGapMeshSequenceCache", nextTask: "M16-GAP-00081" }, "M16-GAP-00081": { type: "MESH_TO_VOLUME", stem: "WebGapMeshToVolume", nextTask: "M16-GAP-00082" }, "M16-GAP-00082": { type: "MIRROR", stem: "WebGapMirror", nextTask: "M16-GAP-00083" }, "M16-GAP-00083": { type: "MULTIRES", stem: "WebGapMultires", nextTask: "M16-GAP-00084" }, "M16-GAP-00084": { type: "NODES", stem: "WebGapNodes", nextTask: "M16-GAP-00085" }, "M16-GAP-00085": { type: "NORMAL_EDIT", stem: "WebGapNormalEdit", nextTask: "M16-GAP-00086" }, "M16-GAP-00086": { type: "OCEAN", stem: "WebGapOcean", nextTask: "M16-GAP-00087" }, "M16-GAP-00087": { type: "PARTICLE_INSTANCE", stem: "WebGapParticleInstance", nextTask: "M16-GAP-00088" }, "M16-GAP-00088": { type: "PARTICLE_SYSTEM", stem: "WebGapParticleSystem", nextTask: "M16-GAP-00089" }, "M16-GAP-00089": { type: "REMESH", stem: "WebGapRemesh", nextTask: "M16-GAP-00090" }, "M16-GAP-00090": { type: "SCREW", stem: "WebGapScrew", nextTask: "M16-GAP-00091" }, "M16-GAP-00091": { type: "SHRINKWRAP", stem: "WebGapShrinkwrap", nextTask: "M16-GAP-00092" }, "M16-GAP-00092": { type: "SIMPLE_DEFORM", stem: "WebGapSimpleDeform", nextTask: "M16-GAP-00093" }, "M16-GAP-00093": { type: "SKIN", stem: "WebGapSkin", nextTask: "M16-GAP-00094" }, "M16-GAP-00094": { type: "SMOOTH", stem: "WebGapSmooth", nextTask: "M16-GAP-00095" }, "M16-GAP-00095": { type: "SOFT_BODY", stem: "WebGapSoftBody", nextTask: "M16-GAP-00096" }, "M16-GAP-00096": { type: "SOLIDIFY", stem: "WebGapSolidify", nextTask: "M16-GAP-00097" }, "M16-GAP-00097": { type: "SUBSURF", stem: "WebGapSubsurf", nextTask: "M16-GAP-00098" }, "M16-GAP-00098": { type: "SURFACE", stem: "WebGapSurface", nextTask: "M16-GAP-00099" }, "M16-GAP-00099": { type: "SURFACE_DEFORM", stem: "WebGapSurfaceDeform", nextTask: "M16-GAP-00100" }, "M16-GAP-00100": { type: "TRIANGULATE", stem: "WebGapTriangulate", nextTask: "M16-GAP-00101" }, "M16-GAP-00101": { type: "UV_PROJECT", stem: "WebGapUvProject", nextTask: "M16-GAP-00102" }, "M16-GAP-00102": { type: "UV_WARP", stem: "WebGapUvWarp", nextTask: "M16-GAP-00103" }, "M16-GAP-00103": { type: "VERTEX_WEIGHT_EDIT", stem: "WebGapVertexWeightEdit", nextTask: "M16-GAP-00104" }, "M16-GAP-00104": { type: "VERTEX_WEIGHT_MIX", stem: "WebGapVertexWeightMix", nextTask: "M16-GAP-00105" }, "M16-GAP-00105": { type: "VERTEX_WEIGHT_PROXIMITY", stem: "WebGapVertexWeightProximity", nextTask: "M16-GAP-00106" }, "M16-GAP-00106": { type: "VOLUME_DISPLACE", stem: "WebGapVolumeDisplace", nextTask: "M16-GAP-00107" }, "M16-GAP-00107": { type: "VOLUME_TO_MESH", stem: "WebGapVolumeToMesh", nextTask: "M16-GAP-00108" }, "M16-GAP-00108": { type: "WARP", stem: "WebGapWarp", nextTask: "M16-GAP-00109" }, "M16-GAP-00109": { type: "WAVE", stem: "WebGapWave", nextTask: "M16-GAP-00110" }, "M16-GAP-00110": { type: "WEIGHTED_NORMAL", stem: "WebGapWeightedNormal", nextTask: "M16-GAP-00111" }, "M16-GAP-00111": { type: "WELD", stem: "WebGapWeld", nextTask: "M16-GAP-00112" }, "M16-GAP-00112": { type: "WIREFRAME", stem: "WebGapWireframe", nextTask: "M16-GAP-00113" }, }; if (genericModifierTasks[task]) { const config = genericModifierTasks[task]; const fixture = path.join(root, `tests/files/web/generated/${task}-modifier-${config.type}.blend`); const desktopPath = path.join(root, `tests/golden/${task}/${config.type.toLowerCase().replaceAll("_", "-")}-modifier-desktop-report.json`); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-generic-modifier-desktop.py"), "--", fixture, desktopPath, task, config.type, config.objectName ?? `${config.stem}Object`], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const mesh = desktop.modifier.meshId ? before.meshes?.find((value) => value.id === desktop.modifier.meshId) : null; const node = desktop.modifier.objectId ? before.nodes?.find((value) => value.id === desktop.modifier.objectId) : null; const holder = mesh ?? node; assert.ok(holder); const modifier = holder.modifierStack?.find((value) => value.type === config.type); assert.ok(modifier); assert.equal(modifier.name, desktop.modifier.modifier.name); assert.equal(modifier.parameters.typeCode, desktop.modifier.modifier.typeCode); assert.equal(modifier.showViewport, desktop.modifier.modifier.showViewport); assert.equal(modifier.showRender, desktop.modifier.modifier.showRender); assert.equal(modifier.showEditMode, desktop.modifier.modifier.showEditMode); assert.equal(modifier.showOnCage, desktop.modifier.modifier.showOnCage); if (config.type === "DECIMATE") { assert.ok(Math.abs(modifier.parameters.ratio - desktop.modifier.modifier.ratio) < 1e-5); assert.equal(modifier.parameters.iterations, desktop.modifier.modifier.iterations); assert.ok(Math.abs(modifier.parameters.angleLimit - desktop.modifier.modifier.angleLimit) < 1e-5); assert.equal(modifier.parameters.decimateMode, desktop.modifier.modifier.decimateMode); } if (config.type === "DISPLACE") { assert.equal(modifier.parameters.direction, desktop.modifier.modifier.direction); assert.ok(Math.abs(modifier.parameters.strength - desktop.modifier.modifier.strength) < 1e-5); assert.ok(Math.abs(modifier.parameters.midLevel - desktop.modifier.modifier.midLevel) < 1e-5); assert.equal(modifier.parameters.space, desktop.modifier.modifier.space); assert.equal(modifier.parameters.textureMapping, desktop.modifier.modifier.textureMapping); } if (config.type === "EDGE_SPLIT") { assert.ok(Math.abs(modifier.parameters.splitAngle - desktop.modifier.modifier.splitAngle) < 1e-5); } if (config.type === "SMOOTH") { assert.ok(Math.abs(modifier.parameters.factor - desktop.modifier.modifier.factor) < 1e-5); assert.equal(modifier.parameters.repeat, desktop.modifier.modifier.iterations); assert.equal(modifier.parameters.vertexGroup, desktop.modifier.modifier.vertexGroup); assert.equal(modifier.parameters.smoothFlags, desktop.modifier.modifier.smoothFlags); } if (config.type === "SOLIDIFY") { assert.ok(Math.abs(modifier.parameters.thickness - desktop.modifier.modifier.thickness) < 1e-5); assert.ok(Math.abs(modifier.parameters.offset - desktop.modifier.modifier.offset) < 1e-5); } if (config.type === "SUBSURF") { assert.equal(modifier.parameters.subdivisionType, desktop.modifier.modifier.subdivisionType); assert.equal(modifier.parameters.levels, desktop.modifier.modifier.levels); assert.equal(modifier.parameters.renderLevels, desktop.modifier.modifier.renderLevels); } if (config.type === "TRIANGULATE") { assert.equal(modifier.parameters.quadMethod, desktop.modifier.modifier.quadMethod); assert.equal(modifier.parameters.ngonMethod, desktop.modifier.modifier.ngonMethod); } if (config.type === "WAVE") { assert.ok(Math.abs(modifier.parameters.height - desktop.modifier.modifier.height) < 1e-5); assert.ok(Math.abs(modifier.parameters.width - desktop.modifier.modifier.width) < 1e-5); assert.ok(Math.abs(modifier.parameters.speed - desktop.modifier.modifier.speed) < 1e-5); } if (config.type === "WEIGHTED_NORMAL") { assert.equal(modifier.parameters.weight, desktop.modifier.modifier.weight); } if (config.type === "WELD") { assert.ok(Math.abs(modifier.parameters.mergeThreshold - desktop.modifier.modifier.mergeThreshold) < 1e-5); } if (config.type === "WIREFRAME") { assert.ok(Math.abs(modifier.parameters.thickness - desktop.modifier.modifier.thickness) < 1e-5); } const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const reopenedSnapshot = snapshot(engine, reopened); const reopenedHolder = mesh ? reopenedSnapshot.meshes?.find((value) => value.id === mesh.id) : reopenedSnapshot.nodes?.find((value) => value.id === node.id); assert.deepEqual(reopenedHolder?.modifierStack, holder.modifierStack); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: `${config.type}_MODIFIER_LOCAL_EXACT`, fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", objectId: node?.id, meshId: mesh?.id, modifier }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: config.nextTask }; const reportPath = path.join(root, `tests/golden/${task}/${config.type.toLowerCase().replaceAll("_", "-")}-modifier-local-exact-report.json`); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} holder=${holder.id} modifier=${modifier.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00113") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00113-operator-action.bake_keys.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00113/action-bake-keys-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-bake-keys-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapBakeKeysObjectAction:object:WebGapBakeKeysObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.length), [5, 5, 5]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_BAKE_KEYS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, channelPaths: animation.channels.map((channel) => channel.path), keyframesPerChannel: animation.channels.map((channel) => channel.keyframes.length) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00114" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00113/action-bake-keys-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} channels=${animation.channels.length} bakedFrames=5 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00114") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00114-operator-action.clean.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00114/action-clean-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-clean-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapCleanObjectAction:object:WebGapCleanObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.length), [1, 1, 1]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_CLEAN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, threshold: desktop.threshold, keyframesPerChannel: animation.channels.map((channel) => channel.keyframes.length) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00115" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00114/action-clean-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} channels=${animation.channels.length} remainingFrames=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00115") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00115-operator-action.clickselect.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00115/action-clickselect-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-clickselect-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapClickSelectObjectAction:object:WebGapClickSelectObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_CLICKSELECT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrames: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00116" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00115/action-clickselect-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00116") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00116-operator-action.copy.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00116/action-copy-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-copy-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "NONE"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapCopyObjectAction:object:WebGapCopyObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_COPY_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", actionId: animation.id, copiedFrames: [3], selectedFrames: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00117" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00116/action-copy-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} copiedFrame=3 mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00117") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00117-operator-action.delete.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00117/action-delete-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-delete-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapDeleteObjectAction:object:WebGapDeleteObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 5], [1, 5], [1, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_DELETE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, deletedFrames: [3], remainingFrames: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00118" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00117/action-delete-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} deletedFrame=3 remainingFrames=1,5 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00118") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00118-operator-action.duplicate.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00118/action-duplicate-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-duplicate-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapDuplicateObjectAction:object:WebGapDuplicateObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 3, 5], [1, 3, 3, 5], [1, 3, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_DUPLICATE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, duplicatedFrames: [3], keyframesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00119" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00118/action-duplicate-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} duplicatedFrame=3 frames=1,3,3,5 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00119") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00119-operator-action.duplicate_move.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00119/action-duplicate-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-duplicate-move-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapDuplicateMoveObjectAction:object:WebGapDuplicateMoveObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[5], [5], [5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_DUPLICATE_MOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, sourceFrame: 3, movedFrame: 5, timeOffset: 2, selectedFrames: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00120" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00119/action-duplicate-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} sourceFrame=3 movedFrame=5 offset=2 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00120") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00120-operator-action.easing_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00120/action-easing-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-easing-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapEasingTypeObjectAction:object:WebGapEasingTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.easing), expected.easing); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.easing)), [["AUTO", "EASE_IN_OUT", "AUTO"], ["AUTO", "EASE_IN_OUT", "AUTO"], ["AUTO", "EASE_IN_OUT", "AUTO"]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_EASING_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, easingType: "EASE_IN_OUT", easingPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.easing)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00121" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00120/action-easing-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} easing=EASE_IN_OUT frame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00121") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00121-operator-action.extrapolation_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00121/action-extrapolation-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-extrapolation-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapExtrapolationTypeObjectAction:object:WebGapExtrapolationTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.equal(channel.extrapolation, expected.extrapolation); } assert.deepEqual(animation.channels.map((channel) => channel.extrapolation), ["LINEAR", "LINEAR", "LINEAR"]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_EXTRAPOLATION_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, extrapolationType: "LINEAR", extrapolationPerChannel: animation.channels.map((channel) => channel.extrapolation) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00122" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00121/action-extrapolation-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} extrapolation=LINEAR desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00122") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00122-operator-action.frame_jump.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00122/action-frame-jump-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-frame-jump-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.frame.current, desktop.jumpedToFrame); const animation = before.animations?.find((value) => value.id === "action:WebGapFrameJumpObjectAction:object:WebGapFrameJumpObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[5], [5], [5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).frame, before.frame); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).frame, before.frame); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_FRAME_JUMP_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, jumpedToFrame: before.frame.current, selectedFrames: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00123" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00122/action-frame-jump-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} currentFrame=${before.frame.current} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00123") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00123-operator-action.handle_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00123/action-handle-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-handle-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapHandleTypeObjectAction:object:WebGapHandleTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.handleType), expected.handleType); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } const handleTypes = animation.channels.map((channel) => channel.keyframes.map((value) => value.handleType)); assert.deepEqual(handleTypes, [["VECTOR", "VECTOR", "VECTOR"], ["VECTOR", "VECTOR", "VECTOR"], ["VECTOR", "VECTOR", "VECTOR"]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_HANDLE_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, handleType: "VECTOR", handleTypes }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00124" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00123/action-handle-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} handle=VECTOR channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00124") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00124-operator-action.interpolation_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00124/action-interpolation-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-interpolation-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapInterpolationTypeObjectAction:object:WebGapInterpolationTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.interpolation), expected.interpolation); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } const interpolationPerChannel = animation.channels.map((channel) => channel.keyframes.map((value) => value.interpolation)); assert.deepEqual(interpolationPerChannel, [["BEZIER", "LINEAR", "BEZIER"], ["BEZIER", "LINEAR", "BEZIER"], ["BEZIER", "LINEAR", "BEZIER"]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_INTERPOLATION_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, interpolationType: "LINEAR", interpolationPerChannel }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00125" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00124/action-interpolation-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} interpolation=LINEAR frame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00125") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00125-operator-action.keyframe_insert.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00125/action-keyframe-insert-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-keyframe-insert-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapKeyframeInsertObjectAction:object:WebGapKeyframeInsertObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.value[0]), expected.values); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_KEYFRAME_INSERT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, insertedFrame: 3, framesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00126" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00125/action-keyframe-insert-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} insertedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00126") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00126-operator-action.keyframe_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00126/action-keyframe-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-keyframe-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapKeyframeTypeObjectAction:object:WebGapKeyframeTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.keyframeType), expected.keyframeType); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } const keyframeTypes = animation.channels.map((channel) => channel.keyframes.map((value) => value.keyframeType)); assert.deepEqual(keyframeTypes, [["KEYFRAME", "BREAKDOWN", "KEYFRAME"], ["KEYFRAME", "BREAKDOWN", "KEYFRAME"], ["KEYFRAME", "BREAKDOWN", "KEYFRAME"]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_KEYFRAME_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, keyframeType: "BREAKDOWN", keyframeTypes }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00127" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00126/action-keyframe-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} keyframeType=BREAKDOWN frame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00127") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00127-operator-action.markers_make_local.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00127/action-markers-make-local-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-markers-make-local-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapMarkersMakeLocalObjectAction:object:WebGapMarkersMakeLocalObject"); assert.ok(animation); assert.deepEqual(animation.markers, desktop.action.markers); assert.deepEqual(animation.markers, [{ name: "LocalActionMarker", frame: 3 }]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_MARKERS_MAKE_LOCAL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, markers: animation.markers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00128" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00127/action-markers-make-local-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} markers=1 frame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00128") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00128-operator-action.mirror.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00128/action-mirror-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-mirror-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapMirrorObjectAction:object:WebGapMirrorObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.value[0]), expected.values); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.value[0])), [[-1, -2, -4], [-2, -4, -8], [-3, -6, -12]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_MIRROR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, mirrorType: "XAXIS", valuesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.value[0])) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00129" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00128/action-mirror-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} mirror=XAXIS desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00129") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00129-operator-action.new.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00129/action-new-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-new-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === `action:${desktop.action.name}:object:WebGapActionNewObject`); assert.ok(animation); assert.equal(animation.name, desktop.action.name); assert.equal(animation.channels.length, desktop.action.channels); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_NEW_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, actionName: animation.name, channels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00130" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00129/action-new-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00130") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00130-operator-action.paste.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00130/action-paste-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-paste-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapPasteObjectAction:object:WebGapPasteObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.value[0]), expected.values); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5, 7], [1, 3, 5, 7], [1, 3, 5, 7]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_PASTE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, pastedFrame: 7, framesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00131" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00130/action-paste-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} pastedFrame=7 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00131") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00131-operator-action.previewrange_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00131/action-previewrange-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-previewrange-set-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const scene = before.scenes?.find((value) => value.id === before.sceneId) ?? before.scenes?.[0]; assert.ok(scene); assert.deepEqual(scene.previewRange, { start: 1, end: 5 }); assert.deepEqual(scene.previewRange, desktop.previewRange); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual((snapshot(engine, reopened).scenes?.find((value) => value.id === before.sceneId) ?? snapshot(engine, reopened).scenes?.[0]).previewRange, scene.previewRange); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual((snapshot(engine, handle).scenes?.find((value) => value.id === before.sceneId) ?? snapshot(engine, handle).scenes?.[0]).previewRange, scene.previewRange); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_PREVIEWRANGE_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", sceneId: scene.id, previewRange: scene.previewRange }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00132" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00131/action-previewrange-set-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} scene=${scene.id} preview=1-5 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00132") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00132-operator-action.push_down.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00132/action-push-down-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-push-down-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const action = before.animations?.find((value) => value.id === "action:WebGapPushDownObjectAction:object:WebGapPushDownObject"); assert.ok(action); const tracks = before.nlaTracks?.filter((value) => value.ownerId === "object:WebGapPushDownObject") ?? []; assert.equal(tracks.length, desktop.nla.tracks.length); assert.equal(tracks.length, 1); assert.equal(tracks[0].strips.length, 1); assert.equal(tracks[0].strips[0].actionId, action.id); assert.equal(before.animations.some((value) => value.id === action.id), true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).nlaTracks, before.nlaTracks); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nlaTracks, before.nlaTracks); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_PUSH_DOWN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: action.id, nlaTracks: tracks }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00133" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00132/action-push-down-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${action.id} nlaTracks=1 strips=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00133") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00133-operator-action.select_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00133/action-select-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-all-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectAllObjectAction:object:WebGapSelectAllObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.ok(animation.channels.every((channel) => channel.keyframes.every((value) => value.selected === true))); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selected: true, channels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00134" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00133/action-select-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selected=all channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00134") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00134-operator-action.select_box.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00134/action-select-box-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-box-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectBoxObjectAction:object:WebGapSelectBoxObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_BOX_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00135" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00134/action-select-box-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00135") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00135-operator-action.select_by_type.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00135/action-select-by-type-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-by-type-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectByTypeObjectAction:object:WebGapSelectByTypeObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.keyframeType), expected.keyframeType); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.ok(animation.channels.every((channel) => channel.keyframes[1].selected && channel.keyframes[1].keyframeType === "BREAKDOWN")); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_BY_TYPE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedType: "BREAKDOWN", selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00136" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00135/action-select-by-type-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} type=BREAKDOWN selectedFrame=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00136") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00136-operator-action.select_circle.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00136/action-select-circle-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-circle-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectCircleObjectAction:object:WebGapSelectCircleObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_CIRCLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00137" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00136/action-select-circle-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00137") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00137-operator-action.select_column.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00137/action-select-column-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-column-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectColumnObjectAction:object:WebGapSelectColumnObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_COLUMN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00138" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00137/action-select-column-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00138") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00138-operator-action.select_lasso.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00138/action-select-lasso-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-lasso-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectLassoObjectAction:object:WebGapSelectLassoObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_LASSO_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00139" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00138/action-select-lasso-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00139") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00139-operator-action.select_leftright.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00139/action-select-leftright-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-leftright-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mode, "LEFT"); assert.deepEqual(desktop.selectedFrames, [1, 3]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectLeftRightObjectAction:object:WebGapSelectLeftRightObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[1, 3], [1, 3], [1, 3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_LEFTRIGHT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, mode: "LEFT", selectedFrames: [1, 3], selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00140" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00139/action-select-leftright-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} mode=left selectedFrames=1,3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00140") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00140-operator-action.select_less.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00140/action-select-less-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-less-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.selectedFrame, 3); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectLessObjectAction:object:WebGapSelectLessObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[3], [3], [3]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_LESS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00141" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00140/action-select-less-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00141") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00141-operator-action.select_linked.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00141/action-select-linked-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-linked-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.selectedChannel, "location[0]"); assert.deepEqual(desktop.selectedFrames, [1, 3, 5]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectLinkedObjectAction:object:WebGapSelectLinkedObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_LINKED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedChannel: "location[0]", selectedFrames: [1, 3, 5], selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00142" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00141/action-select-linked-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedChannel=location[0] selectedFrames=1,3,5 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00142") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00142-operator-action.select_more.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00142/action-select-more-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-select-more-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.selectedChannel, "location[0]"); assert.deepEqual(desktop.selectedPerChannel, [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSelectMoreObjectAction:object:WebGapSelectMoreObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SELECT_MORE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedChannel: "location[0]", selectedPerChannel: [[1, 3, 5], [1, 3, 5], [1, 3, 5]] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00143" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00142/action-select-more-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedChannel=location[0] selectedPerChannel=1,3,5|1,3,5|1,3,5 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00143") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00143-operator-action.snap.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00143/action-snap-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-snap-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.type, "CFRA"); assert.equal(desktop.currentFrame, 4); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapSnapObjectAction:object:WebGapSnapObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.deepEqual(channel.keyframes.map((value) => value.selected), expected.selected); } assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 4, 5], [1, 4, 5], [1, 4, 5]]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[4], [4], [4]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_SNAP_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, type: "CFRA", currentFrame: 4, framesPerChannel: [[1, 4, 5], [1, 4, 5], [1, 4, 5]], selectedPerChannel: [[4], [4], [4]] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00144" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00143/action-snap-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} type=CFRA currentFrame=4 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00144") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00144-operator-action.stash.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00144/action-stash-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-stash-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.nla.activeAction, null); assert.equal(desktop.nla.tracks.length, 1); assert.equal(desktop.nla.tracks[0].strips.length, 1); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const action = before.animations?.find((value) => value.id === "action:WebGapStashObjectAction:object:WebGapStashObject"); assert.ok(action); const tracks = before.nlaTracks?.filter((value) => value.ownerId === "object:WebGapStashObject") ?? []; assert.equal(tracks.length, desktop.nla.tracks.length); assert.equal(tracks.length, 1); assert.equal(tracks[0].strips.length, 1); assert.equal(tracks[0].strips[0].actionId, action.id); assert.equal(before.animations.some((value) => value.id === action.id), true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).nlaTracks, before.nlaTracks); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nlaTracks, before.nlaTracks); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_STASH_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: action.id, nlaTracks: tracks }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00145" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00144/action-stash-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${action.id} nlaTracks=1 strips=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00145") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00145-operator-action.stash_and_create.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00145/action-stash-and-create-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-stash-and-create-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.nla.activeAction, "Action"); assert.equal(desktop.nla.tracks.length, 1); assert.equal(desktop.nla.tracks[0].strips.length, 1); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const stashed = before.animations?.find((value) => value.id === "action:WebGapStashCreateObjectAction:object:WebGapStashCreateObject"); assert.ok(stashed); const tracks = before.nlaTracks?.filter((value) => value.ownerId === "object:WebGapStashCreateObject") ?? []; assert.equal(tracks.length, 1); assert.equal(tracks[0].strips.length, 1); assert.equal(tracks[0].strips[0].actionId, stashed.id); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).nlaTracks, before.nlaTracks); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nlaTracks, before.nlaTracks); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_STASH_AND_CREATE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", activeActionId: null, stashedActionId: stashed.id, nlaTracks: tracks }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00146" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00145/action-stash-and-create-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} activeAction=empty stashedAction=${stashed.id} nlaTracks=1 strips=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00146") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00146-operator-action.unlink.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00146/action-unlink-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-unlink-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.action.activeAction, null); assert.equal(desktop.action.unlinkedAction.name, "WebGapActionUnlinkObjectAction"); assert.equal(desktop.action.unlinkedAction.users, 1); assert.equal(desktop.action.unlinkedAction.useFakeUser, true); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const action = before.animations?.find((value) => value.unlinked === true); assert.ok(action); assert.equal(action.name, desktop.action.unlinkedAction.name); assert.equal(action.targetId, "unlinked"); assert.equal(action.channels.length, desktop.action.unlinkedAction.channels.length); for (const [index, channel] of action.channels.entries()) { const expected = desktop.action.unlinkedAction.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.equal(Number(channel.path.match(/\[(\d+)\]$/)?.[1] ?? 0), expected.index); assert.deepEqual(channel.keyframes.map((keyframe) => keyframe.frame), expected.frames); } assert.deepEqual(action.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_UNLINK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: action.id, targetId: action.targetId, unlinked: action.unlinked, keyframesPerChannel: action.channels.map((channel) => channel.keyframes.length) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00147" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00146/action-unlink-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${action.id} unlinked=true channels=${action.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00147") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00147-operator-action.view_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00147/action-view-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-view-all-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.action.areaType, "DOPESHEET_EDITOR"); assert.equal(desktop.action.uiMode, "ACTION"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const workflow = before.editorWorkflow; const actionArea = workflow.workspaces.flatMap((workspace) => workspace.areas).find((area) => area.editor === "DOPE_SHEET"); assert.ok(actionArea); const actionRegion = actionArea.regions.find((region) => region.kind === "MAIN"); assert.ok(actionRegion?.view2d); assert.deepEqual(actionRegion.view2d, desktop.action.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_VIEW_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", editor: "DOPE_SHEET", regionKind: "MAIN", view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00148" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00147/action-view-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} editor=DOPE_SHEET view2d=exact desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00148") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00148-operator-action.view_frame.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00148/action-view-frame-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-view-frame-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.action.areaType, "DOPESHEET_EDITOR"); assert.equal(desktop.action.uiMode, "ACTION"); assert.equal(desktop.action.currentFrame, 3); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.frame.current, 3); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const workflow = before.editorWorkflow; const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax && left?.mask?.xmin === right?.mask?.xmin && left?.mask?.xmax === right?.mask?.xmax && left?.mask?.ymin === right?.mask?.ymin && left?.mask?.ymax === right?.mask?.ymax; const actionRegion = workflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "DOPE_SHEET") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.action.view2d)); assert.ok(actionRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); assert.equal(snapshot(engine, reopened).frame.current, 3); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_VIEW_FRAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", editor: "DOPE_SHEET", regionKind: "MAIN", currentFrame: before.frame.current, view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00149" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00148/action-view-frame-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} editor=DOPE_SHEET currentFrame=3 view2d=exact desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00149") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00149-operator-action.view_selected.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00149/action-view-selected-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-view-selected-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.selectedFrame, 3); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapActionViewSelectedObjectAction:object:WebGapActionViewSelectedObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.selected)), desktop.action.channels.map((channel) => channel.selected)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((keyframe) => keyframe.selected).map((keyframe) => keyframe.frame)), [[3], [3], [3]]); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax && left?.mask?.xmin === right?.mask?.xmin && left?.mask?.xmax === right?.mask?.xmax && left?.mask?.ymin === right?.mask?.ymin && left?.mask?.ymax === right?.mask?.ymax; const actionRegion = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "DOPE_SHEET") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.view2d)); assert.ok(actionRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ACTION_VIEW_SELECTED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedFrame: 3, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((keyframe) => keyframe.selected).map((keyframe) => keyframe.frame)), view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00150" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00149/action-view-selected-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedFrame=3 view2d=exact desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00150") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00150-operator-anim.change_frame.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00150/anim-change-frame-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-change-frame-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.changedToFrame, 4); assert.equal(desktop.currentFrame, 4); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.frame.current, 4); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChangeFrameObjectAction:object:WebGapAnimChangeFrameObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.selected)), desktop.action.channels.map((channel) => channel.selected)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).frame, before.frame); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).frame, before.frame); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANGE_FRAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, changedToFrame: before.frame.current, framesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00151" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00150/anim-change-frame-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} changedToFrame=4 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00151") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00151-operator-anim.channel_select_keys.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00151/anim-channel-select-keys-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channel-select-keys-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.action.channels.map((channel) => channel.selected), [[true, true, true], [false, false, false], [false, false, false]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelSelectKeysObjectAction:object:WebGapAnimChannelSelectKeysObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.selected)), desktop.action.channels.map((channel) => channel.selected)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[1, 3, 5], [], []]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNEL_SELECT_KEYS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedChannel: { path: "location", index: 0 }, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00152" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00151/anim-channel-select-keys-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedChannel=location[0] desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00152") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00152-operator-anim.channel_view_pick.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00152/anim-channel-view-pick-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channel-view-pick-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelViewPickObjectAction:object:WebGapAnimChannelViewPickObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax && left?.mask?.xmin === right?.mask?.xmin && left?.mask?.xmax === right?.mask?.xmax && left?.mask?.ymin === right?.mask?.ymin && left?.mask?.ymax === right?.mask?.ymax; const actionRegion = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "DOPE_SHEET") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.view2d)); assert.ok(actionRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNEL_VIEW_PICK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, editor: "DOPE_SHEET", regionKind: "MAIN", view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00153" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00152/anim-channel-view-pick-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} view2d=exact desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00153") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00153-operator-anim.channels_bake.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00153/anim-channels-bake-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-bake-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.bakeRange, [1, 5]); assert.equal(desktop.step, 1); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsBakeObjectAction:object:WebGapAnimChannelsBakeObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_BAKE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, bakeRange: [1, 5], step: 1, framesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00154" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00153/anim-channels-bake-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} bakeRange=1..5 step=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00154") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00154-operator-anim.channels_clean_empty.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00154/anim-channels-clean-empty-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-clean-empty-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.emptyAnimDataRemoved, true); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsCleanKeepObjectAction:object:WebGapAnimChannelsCleanKeepObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.keepAction.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); assert.equal(before.animations.some((value) => value.id.includes("WebGapAnimChannelsCleanEmptyObject")), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_CLEAN_EMPTY_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, emptyAnimDataRemoved: true, keepFramesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00155" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00154/anim-channels-clean-empty-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} removedEmpty=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00155") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00155-operator-anim.channels_click.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00155/anim-channels-click-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-click-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.action.channels.map((channel) => channel.selected), [[true, true, true], [false, false, false], [false, false, false]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsClickObjectAction:object:WebGapAnimChannelsClickObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.selected)), desktop.action.channels.map((channel) => channel.selected)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)), [[1, 3, 5], [], []]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_CLICK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, clickedChannel: { path: "location", index: 0 }, selectedPerChannel: animation.channels.map((channel) => channel.keyframes.filter((value) => value.selected).map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00156" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00155/anim-channels-click-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} clickedChannel=location[0] desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00156") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00156-operator-anim.channels_collapse.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00156/anim-channels-collapse-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-collapse-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.collapsedAll, true); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsCollapseObjectAction:object:WebGapAnimChannelsCollapseObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_COLLAPSE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, collapsedAll: true, framesPerChannel: animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00157" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00156/anim-channels-collapse-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} collapsedAll=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00157") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00157-operator-anim.channels_delete.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00157/anim-channels-delete-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-delete-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.deletedChannel, { path: "location", index: 0 }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsDeleteObjectAction:object:WebGapAnimChannelsDeleteObject"); assert.equal(animation?.channels?.length ?? 0, 0); assert.deepEqual(animation?.channels?.map((channel) => channel.keyframes.map((value) => value.frame)) ?? [], desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation?.channels ?? [], []); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_DELETE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation?.id ?? null, deletedChannel: { path: "location", index: 0 }, remainingChannels: animation?.channels?.length ?? 0 }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00158" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00157/anim-channels-delete-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} deleted=location[0] remaining=0 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00158") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00158-operator-anim.channels_editable_toggle.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00158/anim-channels-editable-toggle-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-editable-toggle-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.toggledChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.editable), [false, false, false]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsEditableToggleObjectAction:object:WebGapAnimChannelsEditableToggleObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.editable), desktop.action.channels.map((channel) => channel.editable)); assert.deepEqual(animation.channels.map((channel) => channel.editable), [false, false, false]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_EDITABLE_TOGGLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, toggledChannels: animation.channels.length, editablePerChannel: animation.channels.map((channel) => channel.editable) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00159" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00158/anim-channels-editable-toggle-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} editable=false channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00159") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00159-operator-anim.channels_expand.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00159/anim-channels-expand-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-expand-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.expandedGroups, 1); assert.deepEqual(desktop.action.groups, [{ name: "Object Transforms", expanded: true }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsExpandObjectAction:object:WebGapAnimChannelsExpandObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => ({ name: channel.group, expanded: channel.expanded })), desktop.action.channels.map((channel) => ({ name: channel.group, expanded: true }))); assert.deepEqual(animation.channels.map((channel) => channel.expanded), [true, true, true]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_EXPAND_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, expandedGroups: 1, group: { name: "Object Transforms", expanded: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00160" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00159/anim-channels-expand-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} expanded=Object Transforms desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00160") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00160-operator-anim.channels_fcurves_enable.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00160/anim-channels-fcurves-enable-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-fcurves-enable-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.enabledChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.enabled), [true, true, true]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsFCurvesEnableObjectAction:object:WebGapAnimChannelsFCurvesEnableObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.equal(channel.enabled, expected.enabled); } assert.deepEqual(animation.channels.map((channel) => channel.enabled), [true, true, true]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_FCURVES_ENABLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, enabledChannels: animation.channels.length, enabledPerChannel: animation.channels.map((channel) => channel.enabled) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00161" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00160/anim-channels-fcurves-enable-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} enabled=true channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00161") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00161-operator-anim.channels_group.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00161/anim-channels-group-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-group-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.groups, 1); assert.deepEqual(desktop.action.groups, ["WebGapTransforms"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsGroupObjectAction:object:WebGapAnimChannelsGroupObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.action.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); assert.equal(channel.group, expected.group); } assert.deepEqual(animation.channels.map((channel) => channel.group), ["WebGapTransforms", "WebGapTransforms", "WebGapTransforms"]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_GROUP_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, groups: 1, group: "WebGapTransforms", groupedChannels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00162" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00161/anim-channels-group-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} group=WebGapTransforms channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00162") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00162-operator-anim.channels_move.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00162/anim-channels-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-move-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.direction, "DOWN"); assert.deepEqual(desktop.action.channels.map((channel) => channel.index), [1, 0, 2]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsMoveObjectAction:object:WebGapAnimChannelsMoveObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); assert.deepEqual(animation.channels.map((channel) => Number(channel.path.match(/\[(\d+)\]$/)?.[1] ?? -1)), [1, 0, 2]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_MOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, direction: "DOWN", channelOrder: [1, 0, 2] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00163" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00162/anim-channels-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} direction=DOWN order=1,0,2 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00163") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00163-operator-anim.channels_rename.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00163/anim-channels-rename-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-rename-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.action.channels.map((channel) => channel.path), ["rotation_euler", "location_missing", "location_missing"]); assert.deepEqual(desktop.action.channels.map((channel) => channel.index), [0, 1, 2]); assert.equal(desktop.action.channels.every((channel) => channel.enabled), true); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsRenameObjectAction:object:WebGapAnimChannelsRenameObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); assert.deepEqual(animation.channels.map((channel) => channel.path), ["rotation_euler[0]", "location_missing[1]", "location_missing[2]"]); assert.deepEqual(animation.channels.map((channel) => channel.enabled), [true, false, false]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_RENAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, renamedChannel: { from: "location_missing[0]", to: "rotation_euler[0]" }, disabledControlChannels: 2 }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00164" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00163/anim-channels-rename-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} renamed=rotation_euler[0] desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00164") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00164-operator-anim.channels_select_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00164/anim-channels-select-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-select-all-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.action.channels.map((channel) => channel.path), ["location", "location", "location"]); assert.deepEqual(desktop.action.channels.map((channel) => channel.index), [0, 1, 2]); assert.equal(desktop.action.channels.every((channel) => channel.selected), true); assert.equal(desktop.action.channels.some((channel) => channel.selectedKeyframes.some(Boolean)), false); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSelectAllObjectAction:object:WebGapAnimChannelsSelectAllObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); assert.deepEqual(animation.channels.map((channel) => channel.path), ["location[0]", "location[1]", "location[2]"]); assert.deepEqual(animation.channels.map((channel) => channel.selected), [true, true, true]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.selected ?? false)), [[false, false, false], [false, false, false], [false, false, false]]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SELECT_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selection: "SELECT", selectedChannels: animation.channels.length, selectedKeyframes: 0 }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00165" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00164/anim-channels-select-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedChannels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00165") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00165-operator-anim.channels_select_box.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00165/anim-channels-select-box-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-select-box-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.deepEqual(desktop.action.channels.map((channel) => channel.path), ["location", "location", "location"]); assert.deepEqual(desktop.action.channels.map((channel) => channel.index), [0, 1, 2]); assert.equal(desktop.action.channels.every((channel) => channel.selected), true); assert.equal(desktop.action.channels.some((channel) => channel.selectedKeyframes.some(Boolean)), false); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSelectBoxObjectAction:object:WebGapAnimChannelsSelectBoxObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.action.channels.length); assert.deepEqual(animation.channels.map((channel) => channel.path), ["location[0]", "location[1]", "location[2]"]); assert.deepEqual(animation.channels.map((channel) => channel.selected), [true, true, true]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.selected ?? false)), [[false, false, false], [false, false, false], [false, false, false]]); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SELECT_BOX_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selection: "SELECT", selectedChannels: animation.channels.length, selectedKeyframes: 0 }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00166" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00165/anim-channels-select-box-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selectedChannels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00166") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00166-operator-anim.channels_select_filter.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00166/anim-channels-select-filter-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-select-filter-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.filterText, "Location"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const actionArea = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas).find((area) => area.editor === "DOPE_SHEET" && area.filterText === desktop.filterText); assert.ok(actionArea); assert.equal(actionArea.filterText, desktop.filterText); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSelectFilterObjectAction:object:WebGapAnimChannelsSelectFilterObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SELECT_FILTER_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", editor: "DOPE_SHEET", filterText: actionArea.filterText, actionId: animation.id, channels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00167" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00166/anim-channels-select-filter-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} filter=${actionArea.filterText} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00167") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00167-operator-anim.channels_setting_disable.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00167/anim-channels-setting-disable-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-setting-disable-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.setting, "PROTECT"); assert.equal(desktop.disabledChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.editable), [false, false, false]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSettingDisableObjectAction:object:WebGapAnimChannelsSettingDisableObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.editable), [false, false, false]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SETTING_DISABLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, setting: "PROTECT", disabledChannels: animation.channels.length, editablePerChannel: animation.channels.map((channel) => channel.editable) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00168" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00167/anim-channels-setting-disable-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} setting=PROTECT editable=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00168") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00168-operator-anim.channels_setting_enable.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00168/anim-channels-setting-enable-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-setting-enable-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.setting, "PROTECT"); assert.equal(desktop.enabledChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.editable), [false, false, false]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSettingEnableObjectAction:object:WebGapAnimChannelsSettingEnableObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.editable), [false, false, false]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SETTING_ENABLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, setting: "PROTECT", enabledChannels: animation.channels.length, editablePerChannel: animation.channels.map((channel) => channel.editable) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00169" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00168/anim-channels-setting-enable-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} setting=PROTECT editable=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00169") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00169-operator-anim.channels_setting_toggle.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00169/anim-channels-setting-toggle-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-setting-toggle-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.setting, "PROTECT"); assert.equal(desktop.toggledChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.editable), [false, false, false]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsSettingToggleObjectAction:object:WebGapAnimChannelsSettingToggleObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.editable), [false, false, false]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_SETTING_TOGGLE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, setting: "PROTECT", toggledChannels: animation.channels.length, editablePerChannel: animation.channels.map((channel) => channel.editable) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00170" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00169/anim-channels-setting-toggle-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} setting=PROTECT editable=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00170") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00170-operator-anim.channels_ungroup.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00170/anim-channels-ungroup-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-ungroup-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.groups, 0); assert.equal(desktop.ungroupedChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.group), [null, null, null]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsUngroupObjectAction:object:WebGapAnimChannelsUngroupObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.keyframes.map((value) => value.frame)), desktop.action.channels.map((channel) => channel.frames)); assert.deepEqual(animation.channels.map((channel) => channel.group ?? null), [null, null, null]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_UNGROUP_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, groups: 0, ungroupedChannels: animation.channels.length, groupPerChannel: animation.channels.map((channel) => channel.group ?? null) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00171" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00170/anim-channels-ungroup-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} groups=0 channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00171") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00171-operator-anim.channels_view_selected.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00171/anim-channels-view-selected-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-channels-view-selected-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.selectedChannels, 3); assert.deepEqual(desktop.action.channels.map((channel) => channel.selected), [true, true, true]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimChannelsViewSelectedObjectAction:object:WebGapAnimChannelsViewSelectedObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((channel) => channel.selected), [true, true, true]); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax && left?.mask?.xmin === right?.mask?.xmin && left?.mask?.xmax === right?.mask?.xmax && left?.mask?.ymin === right?.mask?.ymin && left?.mask?.ymax === right?.mask?.ymax; const actionRegion = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "DOPE_SHEET") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.view2d)); assert.ok(actionRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CHANNELS_VIEW_SELECTED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, selectedChannels: animation.channels.length, selectedPerChannel: animation.channels.map((channel) => channel.selected), view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00172" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00171/anim-channels-view-selected-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} selected=3 view2d=exact desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00172") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00172-operator-anim.clear_useless_actions.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00172/anim-clear-useless-actions-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-clear-useless-actions-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.removedEmptyAction, true); assert.deepEqual(Object.keys(desktop.actions).sort(), ["WebGapAnimClearUselessLibraryAction", "WebGapAnimClearUselessUsedAction"]); assert.equal(desktop.usedAction, "WebGapAnimClearUselessUsedAction"); assert.deepEqual(Object.values(desktop.actions).map((action) => action.channels.length), [3, 3]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const usedId = "action:WebGapAnimClearUselessUsedAction:object:WebGapAnimClearUselessUsedObject"; const libraryId = "action:WebGapAnimClearUselessLibraryAction:unlinked"; const used = before.animations?.find((value) => value.id === usedId); const library = before.animations?.find((value) => value.id === libraryId); assert.ok(used); assert.ok(library); assert.equal(library.unlinked, true); assert.deepEqual(used.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); assert.deepEqual(library.channels.map((channel) => channel.keyframes.map((value) => value.frame)), [[1, 3, 5], [1, 3, 5], [1, 3, 5]]); assert.equal(before.animations?.length, 2); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_CLEAR_USELESS_ACTIONS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", preservedActions: [used.id, library.id], unlinkedAction: library.id, channelsPerAction: [used, library].map((action) => action.channels.length) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00173" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00172/anim-clear-useless-actions-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} preserved=2 removedEmpty=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00173") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00173-operator-anim.copy_driver_button.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00173/anim-copy-driver-button-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-copy-driver-button-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "NONE"); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.deepEqual(desktop.drivers, [{ path: '["drive_target"]', index: 0, expression: "frame * 2.0 + 1.0", type: "SCRIPTED", variableCount: 0 }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimCopyDriverButtonObject"); assert.ok(node); assert.deepEqual(node.drivers, [{ path: '["drive_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "frame * 2.0 + 1.0", type: "SCRIPTED", typeCode: 1, flags: 8, influence: 0, variables: [] }]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterNode = snapshot(engine, reopened).nodes?.find((value) => value.id === node.id); assert.deepEqual(afterNode?.drivers, node.drivers); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id)?.drivers, node.drivers); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_COPY_DRIVER_BUTTON_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00173" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00173/anim-copy-driver-button-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=1 operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00174") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-driver-button-add-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "DRIVER_ADDED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.drivers, [{ path: '["drive_target"]', index: 0, expression: "var + 4.5", type: "SCRIPTED", variableCount: 1 }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimDriverButtonAddObject"); assert.ok(node); assert.deepEqual(node.drivers, [{ path: '["drive_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "var + 4.5", type: "SCRIPTED", typeCode: 1, flags: 16, influence: 0, variables: [{ name: "var", type: 3, targetCount: 1 }] }]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterNode = snapshot(engine, reopened).nodes?.find((value) => value.id === node.id); assert.deepEqual(afterNode, node); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id), node); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00175" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=${node.drivers.length} poll=true operator=${desktop.operatorStatus} mainMutation=driver_added desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00175") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00175-operator-anim.driver_button_edit.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00175/anim-driver-button-edit-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-driver-button-edit-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "NONE"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "INTERFACE"); assert.deepEqual(desktop.drivers, [{ path: '["drive_target"]', index: 0, expression: "frame * 2.5 + 1.25", type: "SCRIPTED", variableCount: 0 }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimDriverButtonEditObject"); assert.ok(node); assert.deepEqual(node.drivers, [{ path: '["drive_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "frame * 2.5 + 1.25", type: "SCRIPTED", typeCode: 1, flags: 8, influence: 0, variables: [] }]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterNode = snapshot(engine, reopened).nodes?.find((value) => value.id === node.id); assert.deepEqual(afterNode, node); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id), node); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_DRIVER_BUTTON_EDIT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00176" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00175/anim-driver-button-edit-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=${node.drivers.length} poll=true operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00176") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00176-operator-anim.driver_button_remove.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00176/anim-driver-button-remove-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-driver-button-remove-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "DRIVER_REMOVED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.drivers, []); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimDriverButtonRemoveObject"); assert.ok(node); assert.deepEqual(node.drivers ?? [], []); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterNode = snapshot(engine, reopened).nodes?.find((value) => value.id === node.id); assert.deepEqual(afterNode, node); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id), node); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_DRIVER_BUTTON_REMOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers ?? [] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00177" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00176/anim-driver-button-remove-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=0 poll=true operator=${desktop.operatorStatus} mainMutation=driver_removed desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00177") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00177-operator-anim.end_frame_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00177/anim-end-frame-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-end-frame-set-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "FRAME_END_SET"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.frame, { current: 42, start: 1, end: 42 }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.deepEqual(before.frame, { current: 42, start: 1, end: 42 }); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).frame, before.frame); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).frame, before.frame); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_END_FRAME_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", frame: before.frame }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00178" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00177/anim-end-frame-set-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} frame=${before.frame.start}-${before.frame.end} current=${before.frame.current} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00178") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00178-operator-anim.keyframe_clear_button.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00178/anim-keyframe-clear-button-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-clear-button-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeClearButtonAction"; const expectedBefore = { name: actionName, channels: [{ path: '["clear_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAMES_CLEARED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.value, 3); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after, { value: 3, action: { name: actionName, channels: [] } }); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.targetId === "object:WebGapAnimKeyframeClearButtonObject"); if (sourceAnimation) { assert.equal(sourceAnimation.id, `action:${actionName}:object:WebGapAnimKeyframeClearButtonObject`); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["clear_target"][0]']); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.selected)), [[true, true, true]]); } const clearedBytes = fs.readFileSync(fixture); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, clearedBytes); const clearedSnapshot = snapshot(engine, handle); assert.equal(clearedSnapshot.animations?.some((value) => value.targetId === "object:WebGapAnimKeyframeClearButtonObject"), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = sourceAnimation ? { animationId: sourceAnimation.id, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) } : { animationId: `action:${actionName}:object:WebGapAnimKeyframeClearButtonObject`, channelPaths: ['["clear_target"][0]'], keyframesPerChannel: [3], evidenceStatus: "PRESERVED_DESKTOP_REPORT" }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_CLEAR_BUTTON_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationCount: clearedSnapshot.animations.length, keyframesCleared: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00179" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00178/anim-keyframe-clear-button-local-exact-report.json"); if (sourceAnimation || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel.length}->0 poll=true operator=${desktop.operatorStatus} mainMutation=keyframes_cleared desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00182") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00182-operator-anim.keyframe_delete_button.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00182/anim-keyframe-delete-button-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-delete-button-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeDeleteButtonAction"; const expectedBefore = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_DELETED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before, { selected: true, active: true, value: 3, action: expectedBefore }); assert.deepEqual(desktop.after, { selected: true, active: true, value: 3, action: expectedAfter }); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeDeleteButtonObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["delete_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 3) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const clearedSnapshot = snapshot(engine, handle); const clearedAnimation = clearedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(clearedAnimation); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_DELETE_BUTTON_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: clearedAnimation.id, keyframesPerChannel: clearedAnimation.channels.map((channel) => channel.keyframes.length), keyframeDeleted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00183" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00182/anim-keyframe-delete-button-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 3 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${clearedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_deleted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00192") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00192-operator-anim.keying_set_export.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00192/anim-keying-set-export-desktop-report.json"); const exportPath = path.join(root, "tests/golden/M16-GAP-00192/WebGapAnimKeyingSetExportSet.py"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-export-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath, exportPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(exportPath), true); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "NONE_EXPORT_ONLY"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.exportedScript.bytes > 0, true); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyingSetExportSet"); assert.deepEqual(desktop.before, desktop.after); const exportSha = crypto.createHash("sha256").update(fs.readFileSync(exportPath)).digest("hex"); assert.equal(exportSha, desktop.exportedScript.sha256); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetExportAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); assert.deepEqual(afterSnapshot.animations, sourceSnapshot.animations); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_EXPORT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after, exportedScript: { path: path.relative(root, exportPath).replaceAll(path.sep, "/"), sha256: exportSha, bytes: desktop.exportedScript.bytes } }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: sourceAnimation.id, keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00193" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00192/anim-keying-set-export-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} poll=true operator=${desktop.operatorStatus} mainMutation=none_export_only export=written wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00193") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00193-operator-anim.keying_set_path_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00193/anim-keying-set-path-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-path-add-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_PATH_ADDED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.pathCount, 0); assert.equal(desktop.after.pathCount, 1); assert.deepEqual(desktop.after.paths, [{ dataPath: "", arrayIndex: 0, idType: "OBJECT", group: "", groupMethod: "KEYINGSET", useEntireArray: true }]); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetPathAddAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_PATH_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00194" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00193/anim-keying-set-path-add-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} paths=0->1 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_path_added wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00194") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00194-operator-anim.keying_set_path_remove.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00194/anim-keying-set-path-remove-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-path-remove-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_PATH_REMOVED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.pathCount, 1); assert.deepEqual(desktop.before.paths, [{ dataPath: '["path_remove_target"]', arrayIndex: 0, idType: "OBJECT", group: "", groupMethod: "KEYINGSET", useEntireArray: false }]); assert.equal(desktop.after.pathCount, 0); assert.deepEqual(desktop.after.paths, []); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetPathRemoveAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_PATH_REMOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00195" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00194/anim-keying-set-path-remove-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} paths=1->0 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_path_removed wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00195") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00195-operator-anim.keying_set_remove.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00195/anim-keying-set-remove-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-remove-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_REMOVED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.keyingSetCount, 1); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyingSetRemoveSet"); assert.equal(desktop.after.keyingSetCount, 0); assert.equal(desktop.after.activeKeyingSet, null); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetRemoveAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_REMOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00196" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00195/anim-keying-set-remove-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} keyingSets=1->0 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_removed wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00196") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00196-operator-anim.keyingset_button_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00196/anim-keyingset-button-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyingset-button-add-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_PATH_ADDED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.keyingSetCount, 0); assert.equal(desktop.after.keyingSetCount, 1); assert.equal(desktop.after.activeKeyingSet, "ButtonKeyingSet"); assert.deepEqual(desktop.after.paths, [{ dataPath: '["button_target"]', arrayIndex: 0, idType: "OBJECT", group: "", groupMethod: "KEYINGSET", useEntireArray: true }]); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetButtonAddAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYINGSET_BUTTON_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00197" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00196/anim-keyingset-button-add-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} keyingSets=0->1 paths=0->1 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_path_added wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00197") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00197-operator-anim.keyingset_button_remove.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00197/anim-keyingset-button-remove-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyingset-button-remove-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_PATH_REMOVED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.keyingSetCount, 1); assert.equal(desktop.before.activeKeyingSet, "ButtonKeyingSet"); assert.deepEqual(desktop.before.paths, [{ dataPath: '["button_remove_target"]', arrayIndex: 0, idType: "OBJECT", group: "", groupMethod: "KEYINGSET", useEntireArray: false }]); assert.equal(desktop.after.keyingSetCount, 1); assert.equal(desktop.after.activeKeyingSet, "ButtonKeyingSet"); assert.deepEqual(desktop.after.paths, []); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetButtonRemoveAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYINGSET_BUTTON_REMOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00198" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00197/anim-keyingset-button-remove-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} keyingSets=1 paths=1->0 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_path_removed wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00198") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00198-operator-anim.merge_animation.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00198/anim-merge-animation-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-merge-animation-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ANIMATION_MERGED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.activeObjectAction.name, "WebGapAnimMergeActiveAction"); assert.equal(desktop.before.sourceObjectAction.name, "WebGapAnimMergeSourceAction"); assert.equal(desktop.after.activeObjectAction.name, "WebGapAnimMergeActiveAction"); assert.equal(desktop.after.sourceObjectAction.name, "WebGapAnimMergeActiveAction"); assert.deepEqual(desktop.after.activeObjectAction.channels.map((channel) => channel.path), ["[\"active_merge_target\"]", "[\"source_merge_target\"]"]); assert.deepEqual(desktop.after.sourceObjectAction.channels.map((channel) => channel.path), ["[\"active_merge_target\"]", "[\"source_merge_target\"]"]); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceActiveAnimation = sourceSnapshot.animations?.find((value) => value.targetId === "object:WebGapAnimMergeActiveObject"); const sourceObjectAnimation = sourceSnapshot.animations?.find((value) => value.targetId === "object:WebGapAnimMergeSourceObject"); assert.ok(sourceActiveAnimation); assert.ok(sourceObjectAnimation); const sourceWasUnmerged = sourceObjectAnimation.name === "WebGapAnimMergeSourceAction"; if (sourceWasUnmerged) { assert.equal(sourceActiveAnimation.name, "WebGapAnimMergeActiveAction"); assert.deepEqual(sourceActiveAnimation.channels.map((channel) => channel.path), ["[\"active_merge_target\"][0]"]); assert.deepEqual(sourceObjectAnimation.channels.map((channel) => channel.path), ["[\"source_merge_target\"][0]"]); assert.deepEqual(sourceActiveAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceObjectAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const mergedSnapshot = snapshot(engine, handle); assert.equal(mergedSnapshot.activeObjectId, "object:WebGapAnimMergeActiveObject"); const mergedAnimations = mergedSnapshot.animations?.filter((value) => value.name === "WebGapAnimMergeActiveAction"); assert.equal(mergedAnimations?.length, 2); assert.equal(mergedSnapshot.animations?.some((value) => value.name === "WebGapAnimMergeSourceAction"), false); const mergedActiveAnimation = mergedAnimations.find((value) => value.targetId === "object:WebGapAnimMergeActiveObject"); const mergedSourceAnimation = mergedAnimations.find((value) => value.targetId === "object:WebGapAnimMergeSourceObject"); assert.ok(mergedActiveAnimation); assert.ok(mergedSourceAnimation); const mergedPaths = ["[\"active_merge_target\"][0]", "[\"source_merge_target\"][0]"]; assert.deepEqual(mergedActiveAnimation.channels.map((channel) => channel.path), mergedPaths); assert.deepEqual(mergedSourceAnimation.channels.map((channel) => channel.path), mergedPaths); assert.deepEqual(mergedActiveAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5], [1, 3, 5]]); assert.deepEqual(mergedSourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5], [10, 30, 50]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, mergedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, mergedSnapshot.animations); const beforeWasm = sourceWasUnmerged ? { activeAnimationId: sourceActiveAnimation.id, sourceAnimationId: sourceObjectAnimation.id, activeChannelPaths: sourceActiveAnimation.channels.map((channel) => channel.path), sourceChannelPaths: sourceObjectAnimation.channels.map((channel) => channel.path), keyframesPerChannel: [sourceActiveAnimation.channels[0].keyframes.length, sourceObjectAnimation.channels[0].keyframes.length], } : { evidenceStatus: "PRESERVED_DESKTOP_REPORT", activeAnimationId: sourceActiveAnimation.id, sourceAnimationId: sourceObjectAnimation.id }; const afterWasm = { activeAnimationId: mergedActiveAnimation.id, sourceAnimationId: mergedSourceAnimation.id, channelPaths: mergedPaths, keyframesPerChannel: mergedActiveAnimation.channels.map((channel) => channel.keyframes.length), sourceValues: mergedSourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), }; sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_MERGE_ANIMATION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: afterWasm, sourceActionRemoved: true }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00199" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00198/anim-merge-animation-local-exact-report.json"); if (sourceWasUnmerged || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} actions=2->1 channels=1+1->2 poll=true operator=${desktop.operatorStatus} mainMutation=animation_merged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00191") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00191-operator-anim.keying_set_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00191/anim-keying-set-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-add-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYING_SET_ADDED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.keyingSetCount, 0); assert.equal(desktop.after.keyingSetCount, 1); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetAddAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00192" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00191/anim-keying-set-add-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} keyingSets=0->1 poll=true operator=${desktop.operatorStatus} mainMutation=keying_set_added wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00190") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00190-operator-anim.keying_set_active_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00190/anim-keying-set-active-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keying-set-active-set-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ACTIVE_KEYING_SET_CHANGED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyingSetActiveA"); assert.equal(desktop.after.activeKeyingSet, "WebGapAnimKeyingSetActiveB"); assert.deepEqual(desktop.before.action, desktop.after.action); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === "WebGapAnimKeyingSetActiveAction"); assert.ok(sourceAnimation); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const afterSnapshot = snapshot(engine, handle); const afterAnimation = afterSnapshot.animations?.find((value) => value.name === sourceAnimation.name); assert.deepEqual(afterAnimation, sourceAnimation); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, afterSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, afterSnapshot.animations); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYING_SET_ACTIVE_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before, after: desktop.after }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: afterAnimation.id, keyframesPerChannel: afterAnimation.channels.map((channel) => channel.keyframes.length), visibleMainUnchanged: true, keyingSetMetadata: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00191" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00190/anim-keying-set-active-set-local-exact-report.json"); if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} active=A->B poll=true operator=${desktop.operatorStatus} mainMutation=active_keying_set_changed wasmVisibleMain=unchanged desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00189") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00189-operator-anim.keyframe_insert_menu.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00189/anim-keyframe-insert-menu-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-insert-menu-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeInsertMenuAction"; const expectedBefore = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }] }; const expectedAfter = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [false, true, false] }] }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_INSERTED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyframeInsertMenuSet"); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeInsertMenuObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["insert_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 2) assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const insertedSnapshot = snapshot(engine, handle); const insertedAnimation = insertedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(insertedAnimation); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, insertedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), insertedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_INSERT_MENU_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: insertedAnimation.id, keyframesPerChannel: insertedAnimation.channels.map((channel) => channel.keyframes.length), keyframeInserted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00190" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00189/anim-keyframe-insert-menu-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 2 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${insertedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_inserted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00188") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00188-operator-anim.keyframe_insert_by_name.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00188/anim-keyframe-insert-by-name-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-insert-by-name-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeInsertByNameAction"; const expectedBefore = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }] }; const expectedAfter = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [false, true, false] }] }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_INSERTED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyframeInsertByNameSet"); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeInsertByNameObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["insert_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 2) assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const insertedSnapshot = snapshot(engine, handle); const insertedAnimation = insertedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(insertedAnimation); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, insertedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), insertedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_INSERT_BY_NAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: insertedAnimation.id, keyframesPerChannel: insertedAnimation.channels.map((channel) => channel.keyframes.length), keyframeInserted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00189" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00188/anim-keyframe-insert-by-name-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 2 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${insertedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_inserted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00187") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00187-operator-anim.keyframe_insert_button.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00187/anim-keyframe-insert-button-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-insert-button-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeInsertButtonAction"; const expectedBefore = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }] }; const expectedAfter = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [false, true, false] }] }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_INSERTED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeInsertButtonObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["insert_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 2) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const insertedSnapshot = snapshot(engine, handle); const insertedAnimation = insertedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(insertedAnimation); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, insertedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), insertedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_INSERT_BUTTON_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: insertedAnimation.id, keyframesPerChannel: insertedAnimation.channels.map((channel) => channel.keyframes.length), keyframeInserted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00188" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00187/anim-keyframe-insert-button-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 2 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${insertedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_inserted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00186") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00186-operator-anim.keyframe_insert.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00186/anim-keyframe-insert-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-insert-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeInsertAction"; const expectedBefore = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: '["insert_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [false, true, false] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_INSERTED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyframeInsertSet"); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeInsertObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["insert_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 2) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const insertedSnapshot = snapshot(engine, handle); const insertedAnimation = insertedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(insertedAnimation); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(insertedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, insertedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), insertedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_INSERT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: insertedAnimation.id, keyframesPerChannel: insertedAnimation.channels.map((channel) => channel.keyframes.length), keyframeInserted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00187" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00186/anim-keyframe-insert-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 2 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${insertedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_inserted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00185") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00185-operator-anim.keyframe_delete_vse.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00185/anim-keyframe-delete-vse-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-delete-vse-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeDeleteVSEAction"; const desktopChannelPath = 'sequence_editor.strips_all["WebGapAnimKeyframeDeleteVSEStrip"].blend_alpha'; const channelPath = `${desktopChannelPath}[0]`; const expectedBefore = { name: actionName, channels: [{ path: desktopChannelPath, index: 0, frames: [1, 3, 5], values: [0.25, 0.5, 0.75], selected: [true, true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: desktopChannelPath, index: 0, frames: [1, 5], values: [0.25, 0.75], selected: [true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_DELETED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.active, true); assert.equal(desktop.before.selected, true); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "scene:Scene"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), [channelPath]); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 3) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[0.25, 0.5, 0.75]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const clearedSnapshot = snapshot(engine, handle); const clearedAnimation = clearedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(clearedAnimation); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[0.25, 0.75]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_DELETE_VSE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: clearedAnimation.id, keyframesPerChannel: clearedAnimation.channels.map((channel) => channel.keyframes.length), keyframeDeleted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00186" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00185/anim-keyframe-delete-vse-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 3 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${clearedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_deleted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00184") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00184-operator-anim.keyframe_delete_v3d.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00184/anim-keyframe-delete-v3d-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-delete-v3d-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeDeleteV3DAction"; const expectedBefore = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_DELETED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeDeleteV3DObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["delete_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 3) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const clearedSnapshot = snapshot(engine, handle); const clearedAnimation = clearedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(clearedAnimation); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_DELETE_V3D_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: clearedAnimation.id, keyframesPerChannel: clearedAnimation.channels.map((channel) => channel.keyframes.length), keyframeDeleted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00185" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00184/anim-keyframe-delete-v3d-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 3 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${clearedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_deleted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00183") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00183-operator-anim.keyframe_delete_by_name.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00183/anim-keyframe-delete-by-name-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-delete-by-name-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeDeleteByNameAction"; const keyingSetName = "WebGapAnimKeyframeDeleteByNameSet"; const expectedBefore = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_DELETED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.activeKeyingSet, keyingSetName); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeDeleteByNameObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["delete_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); if (sourceAnimation.channels[0].keyframes.length === 3) { assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const clearedSnapshot = snapshot(engine, handle); const clearedAnimation = clearedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(clearedAnimation); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_DELETE_BY_NAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: clearedAnimation.id, keyframesPerChannel: clearedAnimation.channels.map((channel) => channel.keyframes.length), keyframeDeleted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00184" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00183/anim-keyframe-delete-by-name-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 3 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${clearedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_deleted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00181") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00181-operator-anim.keyframe_delete.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00181/anim-keyframe-delete-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-delete-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeDeleteAction"; const expectedBefore = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; const expectedAfter = { name: actionName, channels: [{ path: '["delete_target"]', index: 0, frames: [1, 5], values: [1, 5], selected: [true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "KEYFRAME_DELETED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, expectedAfter); assert.equal(desktop.before.activeKeyingSet, "WebGapAnimKeyframeDeleteSet"); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); assert.ok(sourceAnimation); assert.equal(sourceAnimation.targetId, "object:WebGapAnimKeyframeDeleteObject"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["delete_target"][0]']); assert.ok([2, 3].includes(sourceAnimation.channels[0].keyframes.length)); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); const clearedBytes = fs.readFileSync(fixture); open(engine, handle, clearedBytes); const clearedSnapshot = snapshot(engine, handle); const clearedAnimation = clearedSnapshot.animations?.find((value) => value.name === actionName); assert.ok(clearedAnimation); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 5]]); assert.deepEqual(clearedAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 5]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_DELETE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationId: clearedAnimation.id, keyframesPerChannel: clearedAnimation.channels.map((channel) => channel.keyframes.length), keyframeDeleted: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00182" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00181/anim-keyframe-delete-local-exact-report.json"); if (beforeWasm.keyframesPerChannel[0] === 3 || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel[0]}->${clearedAnimation.channels[0].keyframes.length} poll=true operator=${desktop.operatorStatus} mainMutation=keyframe_deleted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00180") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00180-operator-anim.keyframe_clear_vse.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00180/anim-keyframe-clear-vse-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-clear-vse-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeClearVSEAction"; const desktopChannelPath = 'sequence_editor.strips_all["WebGapAnimKeyframeClearVSEStrip"].blend_alpha'; const channelPath = `${desktopChannelPath}[0]`; const expectedBefore = { name: actionName, channels: [{ path: desktopChannelPath, index: 0, frames: [1, 3, 5], values: [0.25, 0.5, 0.75], selected: [true, true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ANIMATION_CLEARED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before.action, expectedBefore); assert.deepEqual(desktop.after.action, { name: actionName, channels: [] }); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.name === actionName); if (sourceAnimation) { assert.equal(sourceAnimation.targetId, "scene:Scene"); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), [channelPath]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[0.25, 0.5, 0.75]]); } const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); const clearedBytes = fs.readFileSync(fixture); open(engine, handle, clearedBytes); const clearedSnapshot = snapshot(engine, handle); assert.equal(clearedSnapshot.animations?.some((value) => value.name === actionName), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = sourceAnimation ? { animationId: sourceAnimation.id, targetId: sourceAnimation.targetId, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) } : { animationId: `action:${actionName}:scene:Scene`, targetId: "scene:Scene", channelPaths: [channelPath], keyframesPerChannel: [3], evidenceStatus: "PRESERVED_DESKTOP_REPORT" }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_CLEAR_VSE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationCount: clearedSnapshot.animations.length, animationCleared: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00181" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00180/anim-keyframe-clear-vse-local-exact-report.json"); if (sourceAnimation || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel.length}->0 poll=true operator=${desktop.operatorStatus} mainMutation=animation_cleared desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00179") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00179-operator-anim.keyframe_clear_v3d.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00179/anim-keyframe-clear-v3d-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const sourceBytes = fs.readFileSync(fixture); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-keyframe-clear-v3d-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const actionName = "WebGapAnimKeyframeClearV3DAction"; const expectedBefore = { name: actionName, channels: [{ path: '["clear_target"]', index: 0, frames: [1, 3, 5], values: [1, 3, 5], selected: [true, true, true] }], }; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ANIMATION_CLEARED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.before, { selected: true, active: true, value: 3, action: expectedBefore }); assert.deepEqual(desktop.after, { selected: true, active: true, value: 3, action: { name: actionName, channels: [] } }); const sourceEngine = await factory({ wasmBinary }); const sourceHandle = sourceEngine._web_engine_create(); open(sourceEngine, sourceHandle, sourceBytes); const sourceSnapshot = snapshot(sourceEngine, sourceHandle); const sourceAnimation = sourceSnapshot.animations?.find((value) => value.targetId === "object:WebGapAnimKeyframeClearV3DObject"); if (sourceAnimation) { assert.equal(sourceAnimation.id, `action:${actionName}:object:WebGapAnimKeyframeClearV3DObject`); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.path), ['["clear_target"][0]']); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.frame)), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.value[0])), [[1, 3, 5]]); assert.deepEqual(sourceAnimation.channels.map((channel) => channel.keyframes.map((keyframe) => keyframe.selected)), [[true, true, true]]); } const clearedBytes = fs.readFileSync(fixture); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, clearedBytes); const clearedSnapshot = snapshot(engine, handle); assert.equal(clearedSnapshot.animations?.some((value) => value.targetId === "object:WebGapAnimKeyframeClearV3DObject"), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, clearedSnapshot.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), clearedSnapshot); sourceEngine._web_engine_destroy(sourceHandle); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const beforeWasm = sourceAnimation ? { animationId: sourceAnimation.id, channelPaths: sourceAnimation.channels.map((channel) => channel.path), keyframesPerChannel: sourceAnimation.channels.map((channel) => channel.keyframes.length) } : { animationId: `action:${actionName}:object:WebGapAnimKeyframeClearV3DObject`, channelPaths: ['["clear_target"][0]'], keyframesPerChannel: [3], evidenceStatus: "PRESERVED_DESKTOP_REPORT" }; const report = { schemaVersion: 1, task, operation: "ANIM_KEYFRAME_CLEAR_V3D_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, before: desktop.before.action, after: desktop.after.action }, wasm: { status: "EXACT", before: beforeWasm, after: { animationCount: clearedSnapshot.animations.length, animationCleared: true } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00180" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00179/anim-keyframe-clear-v3d-local-exact-report.json"); if (sourceAnimation || !fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} channels=${beforeWasm.keyframesPerChannel.length}->0 poll=true operator=${desktop.operatorStatus} mainMutation=animation_cleared desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00206") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00206-operator-anim.slot_channels_move_to_new_action.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00206/anim-slot-channels-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-slot-channels-move-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "SLOT_MOVED_TO_NEW_ACTION"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.after.objects.WebGapAnimMoveSlotA.name, "WebGapMoveSlotAAction"); assert.equal(desktop.after.objects.WebGapAnimMoveSlotB.name, "WebGapAnimMoveSlotAction"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animations = before.animations?.filter((value) => value.targetId && value.targetId !== "unlinked"); assert.equal(animations?.length, 2); assert.deepEqual(animations.map((value) => value.name).sort(), ["WebGapAnimMoveSlotAction", "WebGapMoveSlotAAction"]); const moved = animations.find((value) => value.name === "WebGapMoveSlotAAction"); const remaining = animations.find((value) => value.name === "WebGapAnimMoveSlotAction"); assert.ok(moved && remaining); assert.deepEqual(moved.channels[0].keyframes.map((value) => value.value[0]), [1, 3, 5]); assert.deepEqual(remaining.channels[0].keyframes.map((value) => value.value[0]), [2, 4, 6]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SLOT_CHANNELS_MOVE_TO_NEW_ACTION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", movedActionId: moved.id, remainingActionId: remaining.id, movedChannels: moved.channels.length, remainingChannels: remaining.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00207" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00206/anim-slot-channels-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} actions=2 moved=1 desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00207") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00207-operator-anim.slot_new_for_id.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00207/anim-slot-new-for-id-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-slot-new-for-id-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "SLOT_DUPLICATED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.after.slots, ["OBWebGapAnimNewSlot", "OBWebGapAnimNewSlot.001"]); assert.equal(desktop.after.slot, "OBWebGapAnimNewSlot.001"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.targetId === "object:WebGapAnimNewSlotObject"); assert.ok(animation); assert.equal(animation.slotIdentifier, "OBWebGapAnimNewSlot.001"); assert.equal(animation.slotCount, 2); assert.equal(animation.channels.length, 1); assert.equal(animation.channels[0].path, '["new_slot_target"][0]'); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.frame), [1, 3, 5]); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.value[0]), [1, 3, 5]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const animationAfter = snapshot(engine, reopened).animations?.find((value) => value.id === animation.id); assert.deepEqual(animationAfter, animation); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SLOT_NEW_FOR_ID_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", animationId: animation.id, slotIdentifier: animation.slotIdentifier, slotCount: animation.slotCount, channels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00208" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00207/anim-slot-new-for-id-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} slot=${animation.slotIdentifier} slotCount=${animation.slotCount} desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00208") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00208-operator-anim.slot_unassign_from_constraint.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00208/anim-slot-unassign-from-constraint-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-slot-unassign-from-constraint-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "CONSTRAINT_SLOT_UNASSIGNED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.action, "WebGapAnimConstraintSlotAction"); assert.equal(desktop.before.actionSlot, "OBWebGapConstraintSlot"); assert.notEqual(desktop.before.actionSlotHandle, 0); assert.equal(desktop.after.actionSlot, null); assert.equal(desktop.after.actionSlotHandle, 0); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimConstraintSlotObject"); assert.ok(node); const constraint = node.constraints?.find((value) => value.name === "WebGapActionSlotConstraint"); assert.ok(constraint); assert.equal(constraint.actionId, "action:WebGapAnimConstraintSlotAction"); assert.equal(constraint.actionSlotIdentifier, "OBWebGapConstraintSlot"); assert.equal(constraint.actionSlotHandle, 0); assert.equal(constraint.actionSlotAssigned, false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const nodeAfter = snapshot(engine, reopened).nodes?.find((value) => value.id === node.id); assert.deepEqual(nodeAfter?.constraints, node.constraints); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes, before.nodes); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SLOT_UNASSIGN_FROM_CONSTRAINT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, constraintName: constraint.name, actionId: constraint.actionId, actionSlotIdentifier: constraint.actionSlotIdentifier, actionSlotHandle: constraint.actionSlotHandle, actionSlotAssigned: constraint.actionSlotAssigned }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00209" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00208/anim-slot-unassign-from-constraint-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} constraint=${constraint.name} slotAssigned=false desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00209") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00209-operator-anim.slot_unassign_from_id.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00209/anim-slot-unassign-from-id-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-slot-unassign-from-id-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ID_SLOT_UNASSIGNED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.action, "WebGapAnimUnassignIdAction"); assert.equal(desktop.before.actionSlot, "OBWebGapUnassignIdSlot"); assert.notEqual(desktop.before.actionSlotHandle, 0); assert.equal(desktop.after.actionSlot, null); assert.equal(desktop.after.actionSlotHandle, 0); assert.equal(desktop.after.lastSlotIdentifier, "OBWebGapUnassignIdSlot"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.targetId === "object:WebGapAnimUnassignIdObject"); assert.ok(animation); assert.equal(animation.slotIdentifier, "OBWebGapUnassignIdSlot"); assert.equal(animation.slotHandle, 0); assert.equal(animation.slotAssigned, false); assert.equal(animation.slotCount, 1); assert.equal(animation.channels.length, 1); assert.equal(animation.channels[0].path, '["unassign_id_target"][0]'); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.frame), [1, 3, 5]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const animationAfter = snapshot(engine, reopened).animations?.find((value) => value.id === animation.id); assert.deepEqual(animationAfter, animation); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SLOT_UNASSIGN_FROM_ID_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", animationId: animation.id, slotIdentifier: animation.slotIdentifier, slotHandle: animation.slotHandle, slotAssigned: animation.slotAssigned, slotCount: animation.slotCount, channels: animation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00210" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00209/anim-slot-unassign-from-id-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} animation=${animation.id} slotAssigned=false desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00210") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00210-operator-anim.slot_unassign_from_nla_strip.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00210/anim-slot-unassign-from-nla-strip-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-slot-unassign-from-nla-strip-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "NLA_SLOT_UNASSIGNED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.action, "WebGapAnimNlaSlotAction"); assert.equal(desktop.before.actionSlot, "OBWebGapNlaSlot"); assert.notEqual(desktop.before.actionSlotHandle, 0); assert.equal(desktop.after.actionSlot, null); assert.equal(desktop.after.actionSlotHandle, 0); assert.equal(desktop.after.lastSlotIdentifier, "OBWebGapNlaSlot"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const track = before.nlaTracks?.find((value) => value.ownerId === "object:WebGapAnimNlaSlotObject"); assert.ok(track); const strip = track.strips?.find((value) => value.id === "WebGapNlaSlotStrip"); assert.ok(strip); assert.equal(strip.actionId, "action:WebGapAnimNlaSlotAction:object:WebGapAnimNlaSlotObject"); assert.equal(strip.actionSlotIdentifier, "OBWebGapNlaSlot"); assert.equal(strip.actionSlotHandle, 0); assert.equal(strip.actionSlotAssigned, false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const trackAfter = snapshot(engine, reopened).nlaTracks?.find((value) => value.id === track.id); assert.deepEqual(trackAfter?.strips, track.strips); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nlaTracks, before.nlaTracks); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SLOT_UNASSIGN_FROM_NLA_STRIP_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", trackId: track.id, stripId: strip.id, actionId: strip.actionId, actionSlotIdentifier: strip.actionSlotIdentifier, actionSlotHandle: strip.actionSlotHandle, actionSlotAssigned: strip.actionSlotAssigned }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00211" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00210/anim-slot-unassign-from-nla-strip-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} strip=${strip.id} slotAssigned=false desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00211") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00211-operator-anim.start_frame_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00211/anim-start-frame-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-start-frame-set-desktop.py"); const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath]; const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "FRAME_START_SET"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.frame, { current: 42, start: 42, end: 120 }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.deepEqual(before.frame, { current: 42, start: 42, end: 120 }); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).frame, before.frame); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).frame, before.frame); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_START_FRAME_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", frame: before.frame }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00212" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00211/anim-start-frame-set-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} frame=${before.frame.start}-${before.frame.end} current=${before.frame.current} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00212") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00212-operator-anim.update_animated_transform_constraints.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00212/anim-update-animated-transform-constraints-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-update-animated-transform-constraints-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const expectedDesktopPath = 'constraints["WebGapAnimatedTransformConstraint"].from_min_x_rot'; const expectedPath = `${expectedDesktopPath}[0]`; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "TRANSFORM_CONSTRAINT_PATHS_UPDATED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.useConvertToRadians, true); assert.equal(desktop.after.action, "WebGapAnimatedTransformConstraintAction"); assert.deepEqual(desktop.after.channels, [{ path: expectedDesktopPath, index: 0, frames: [1, 10], values: [-30, 60] }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAnimatedTransformConstraintAction:object:WebGapAnimatedTransformConstraintObject"); assert.ok(animation); assert.deepEqual(animation.channels.map((value) => ({ path: value.path, keyframes: value.keyframes.map((key) => ({ frame: key.frame, value: key.value[0] })) })), [{ path: expectedPath, keyframes: [{ frame: 1, value: -30 }, { frame: 10, value: 60 }] }]); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimatedTransformConstraintObject"); assert.ok(node); assert.equal(node.constraints?.find((value) => value.name === "WebGapAnimatedTransformConstraint")?.typeCode, 19); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterAnimation = snapshot(engine, reopened).animations?.find((value) => value.id === animation.id); assert.deepEqual(afterAnimation, animation); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_UPDATE_ANIMATED_TRANSFORM_CONSTRAINTS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, useConvertToRadians: desktop.useConvertToRadians }, wasm: { status: "EXACT", animationId: animation.id, channelPath: animation.channels[0].path, keyframes: animation.channels[0].keyframes.map((key) => ({ frame: key.frame, value: key.value[0] })), constraintTypeCode: node.constraints.find((value) => value.name === "WebGapAnimatedTransformConstraint").typeCode }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00213" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00212/anim-update-animated-transform-constraints-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} animation=${animation.id} channel=${animation.channels[0].path} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00213") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00213-operator-anim.version_bone_hide_property.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00213/anim-version-bone-hide-property-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-version-bone-hide-property-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const oldPath = 'bones["WebGapVersionBoneHideBone"].hide'; const newPath = 'pose.bones["WebGapVersionBoneHideBone"].hide'; assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "BONE_HIDE_FCURVE_MOVED_TO_OBJECT_ACTION"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.after.armatureChannels, [{ path: oldPath, index: 0, frames: [1, 10], values: [0, 1] }]); assert.ok(desktop.after.objectChannels.some((value) => value.path === newPath)); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapVersionBoneHideObjectAction:object:WebGapVersionBoneHideObject"); assert.ok(animation); const copied = animation.channels?.find((value) => value.path === `${newPath}[0]`); assert.ok(copied); assert.deepEqual(copied.keyframes.map((key) => ({ frame: key.frame, value: key.value[0] })), [{ frame: 1, value: 0 }, { frame: 10, value: 1 }]); const armatureAnimation = before.animations?.find((value) => value.id === "action:WebGapVersionBoneHideArmatureAction:armature:WebGapVersionBoneHideArmature"); assert.ok(armatureAnimation); assert.ok(armatureAnimation.channels?.some((value) => value.path === `${oldPath}[0]`)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const afterAnimation = snapshot(engine, reopened).animations?.find((value) => value.id === animation.id); assert.deepEqual(afterAnimation, animation); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_VERSION_BONE_HIDE_PROPERTY_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectAnimationId: animation.id, copiedChannelPath: copied.path, copiedKeyframes: copied.keyframes.map((key) => ({ frame: key.frame, value: key.value[0] })), armatureAnimationId: armatureAnimation.id }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00214" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00213/anim-version-bone-hide-property-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} objectAnimation=${animation.id} channel=${copied.path} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00214") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00214-operator-anim.view_curve_in_graph_editor.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00214/anim-view-curve-in-graph-editor-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-view-curve-in-graph-editor-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 60_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.mainMutation, "GRAPH_VIEW_FRAMED"); assert.notDeepEqual(desktop.before.view.view2d, desktop.after.view.view2d); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.editorWorkflowStatus, "AVAILABLE"); const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax; const graphRegion = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "GRAPH") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.before.view.view2d)); assert.ok(graphRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_VIEW_CURVE_IN_GRAPH_EDITOR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, beforeView2d: desktop.before.view.view2d, afterView2d: desktop.after.view.view2d }, wasm: { status: "EXACT", editor: "GRAPH", regionKind: "MAIN", view2d: graphRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00215" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00214/anim-view-curve-in-graph-editor-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} editor=GRAPH view2d=exact desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00215") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00215-operator-armature.align.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00215/armature-align-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-align-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "CHILD_ALIGNED_TO_PARENT"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); const desktopParent = desktop.after.bones.find((value) => value.name === "WebGapArmatureAlignParent"); const desktopChild = desktop.after.bones.find((value) => value.name === "WebGapArmatureAlignChild"); assert.ok(desktopParent && desktopChild); assert.equal(desktopChild.parent, null); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureAlignArmature"); assert.ok(armature); const parent = armature.bones?.find((value) => value.name === "WebGapArmatureAlignParent"); const child = armature.bones?.find((value) => value.name === "WebGapArmatureAlignChild"); assert.ok(parent && child); assert.equal(child.parentId, null); const axis = parent.tail.map((value, index) => value - parent.head[index]); const childAxis = child.tail.map((value, index) => value - child.head[index]); const cross = [axis[1] * childAxis[2] - axis[2] * childAxis[1], axis[2] * childAxis[0] - axis[0] * childAxis[2], axis[0] * childAxis[1] - axis[1] * childAxis[0]]; assert.ok(Math.max(...cross.map((value) => Math.abs(value))) < 1e-4, `child axis is not aligned: ${JSON.stringify({ axis, childAxis })}`); assert.deepEqual(child.head, desktopChild.head); assert.equal(child.tail.length, desktopChild.tail.length); child.tail.forEach((value, index) => assert.ok(Math.abs(value - desktopChild.tail[index]) < 1e-5)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_ALIGN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, parentBone: { id: parent.id, head: parent.head, tail: parent.tail }, alignedChildBone: { id: child.id, head: child.head, tail: child.tail, parentId: child.parentId } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00216" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00215/armature-align-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} child=${child.name} aligned=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00216") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00216-operator-armature.assign_to_collection.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00216/armature-assign-to-collection-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-assign-to-collection-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "CHILD_ASSIGNED_TO_TARGET_COLLECTION"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); const targetDesktop = desktop.after.collections.find((value) => value.name === "WebGapArmatureAssignTarget"); assert.ok(targetDesktop?.bones.includes("WebGapArmatureAssignChild")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureAssignArmature"); assert.ok(armature); const source = armature.boneCollections?.find((value) => value.name === "WebGapArmatureAssignSource"); const target = armature.boneCollections?.find((value) => value.name === "WebGapArmatureAssignTarget"); assert.ok(source && target); const childId = armature.bones?.find((value) => value.name === "WebGapArmatureAssignChild")?.id; assert.ok(childId); assert.ok(source.boneIds.includes(childId)); assert.ok(target.boneIds.includes(childId)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_ASSIGN_TO_COLLECTION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, sourceCollection: { id: source.id, boneIds: source.boneIds }, targetCollection: { id: target.id, boneIds: target.boneIds }, assignedBoneId: childId }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00217" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00216/armature-assign-to-collection-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} target=${target.name} assigned=${childId} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00217") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00217-operator-armature.autoside_names.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00217/armature-autoside-names-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-autoside-names-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopNames = desktop.after.bones.map((value) => value.name).sort(); assert.deepEqual(desktopNames, ["WebGapArmatureAutosideLeft.L", "WebGapArmatureAutosideRight.R"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureAutosideArmature"); assert.ok(armature); const names = armature.bones?.map((value) => value.name).sort(); assert.deepEqual(names, desktopNames); const left = armature.bones?.find((value) => value.name === "WebGapArmatureAutosideLeft.L"); const right = armature.bones?.find((value) => value.name === "WebGapArmatureAutosideRight.R"); assert.ok(left && right); assert.deepEqual(left.head, [1, 0, 0]); assert.deepEqual(right.head, [-1, 0, 0]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_AUTOSIDE_NAMES_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, boneNames: names, leftBoneId: left.id, rightBoneId: right.id }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00218" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00217/armature-autoside-names-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} names=left-right desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00218") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00218-operator-armature.bone_primitive_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00218/armature-bone-primitive-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-bone-primitive-add-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.bones.length, 1); const desktopBone = desktop.after.bones[0]; assert.equal(desktopBone.name, "WebGapArmaturePrimitiveBone"); assert.deepEqual(desktopBone.head, [1.5, -2, 0.75]); assert.deepEqual(desktopBone.tail, [1.5, -2, 3.25]); assert.equal(desktopBone.useDeform, false); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmaturePrimitiveArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 1); const bone = armature.bones[0]; assert.equal(bone.name, desktopBone.name); assert.deepEqual(bone.head, desktopBone.head); assert.deepEqual(bone.tail, desktopBone.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_BONE_PRIMITIVE_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, bone: { id: bone.id, name: bone.name, head: bone.head, tail: bone.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00219" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00218/armature-bone-primitive-add-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} bone=${bone.id} cursor=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00219") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00219-operator-armature.calculate_roll.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00219/armature-calculate-roll-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-calculate-roll-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.bones.length, 1); const desktopBone = desktop.after.bones[0]; assert.equal(desktopBone.name, "WebGapArmatureCalculateRollBone"); assert.deepEqual(desktopBone.head, [0, 0, 0]); assert.deepEqual(desktopBone.tail, [0, 2, 0]); const expectedMatrix = [0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]; desktopBone.matrix.forEach((value, index) => assert.ok(Math.abs(value - expectedMatrix[index]) < 1e-5, `desktop matrix[${index}] ${value} != ${expectedMatrix[index]}`)); assert.ok(Math.abs(Math.abs(desktop.roll) - Math.PI / 2) < 1e-5); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCalculateRollArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 1); const bone = armature.bones[0]; assert.equal(bone.name, desktopBone.name); assert.deepEqual(bone.head, desktopBone.head); assert.deepEqual(bone.tail, desktopBone.tail); const expectedRestMatrix = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1]; bone.restMatrix.forEach((value, index) => assert.ok(Math.abs(value - expectedRestMatrix[index]) < 1e-5, `WASM restMatrix[${index}] ${value} != ${expectedRestMatrix[index]}`)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_CALCULATE_ROLL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, roll: desktop.roll }, wasm: { status: "EXACT", armatureId: armature.id, bone: { id: bone.id, name: bone.name, head: bone.head, tail: bone.tail, restMatrix: bone.restMatrix } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00220" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00219/armature-calculate-roll-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} bone=${bone.id} roll=global-pos-x desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00220") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00220-operator-armature.click_extrude.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00220/armature-click-extrude-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-click-extrude-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.bones.length, 2); const desktopParent = desktop.after.bones.find((value) => value.name === "WebGapArmatureClickExtrudeBone"); const desktopChild = desktop.after.bones.find((value) => value.name === "WebGapArmatureClickExtrudeBone.001"); assert.ok(desktopParent && desktopChild); assert.equal(desktopChild.parent, desktopParent.name); assert.equal(desktopChild.useConnect, true); assert.deepEqual(desktopChild.head, desktopParent.tail); assert.deepEqual(desktopChild.tail, [1.5, 2, 1]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureClickExtrudeArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 2); const parent = armature.bones.find((value) => value.name === desktopParent.name); const child = armature.bones.find((value) => value.name === desktopChild.name); assert.ok(parent && child); assert.equal(child.parentId, parent.id); assert.deepEqual(child.head, desktopChild.headRaw); assert.deepEqual(child.tail, desktopChild.tailRaw); assert.equal(child.head[0], 0); assert.equal(child.head[1], 0); assert.equal(child.head[2], 0); assert.ok(Math.abs(child.restMatrix[12] - desktopChild.head[0]) < 1e-5); assert.ok(Math.abs(child.restMatrix[13] - desktopChild.head[1]) < 1e-5); assert.ok(Math.abs(child.restMatrix[14] - desktopChild.head[2]) < 1e-5); const localLength = Math.hypot(desktopChild.tailRaw[0] - desktopChild.headRaw[0], desktopChild.tailRaw[1] - desktopChild.headRaw[1], desktopChild.tailRaw[2] - desktopChild.headRaw[2]); const matrixTail = [child.restMatrix[12] + child.restMatrix[4] * localLength, child.restMatrix[13] + child.restMatrix[5] * localLength, child.restMatrix[14] + child.restMatrix[6] * localLength]; desktopChild.tail.forEach((value, index) => assert.ok(Math.abs(value - matrixTail[index]) < 1e-5, `WASM armature-space tail[${index}] ${matrixTail[index]} != ${value}`)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_CLICK_EXTRUDE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, parentBone: { id: parent.id, name: parent.name, head: parent.head, tail: parent.tail }, extrudedBone: { id: child.id, name: child.name, parentId: child.parentId, head: child.head, tail: child.tail, restMatrix: child.restMatrix }, armatureSpaceEndpoint: desktopChild.tail }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00221" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00220/armature-click-extrude-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} child=${child.name} cursor=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00221") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00221-operator-armature.collection_add.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00221/armature-collection-add-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-add-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.collections.length, 2); const desktopSource = desktop.after.collections.find((value) => value.name === "WebGapArmatureCollectionAddExisting"); const desktopAdded = desktop.after.collections.find((value) => value.name === "Bones"); assert.ok(desktopSource && desktopAdded); assert.equal(desktopSource.index, 0); assert.equal(desktopAdded.index, 1); assert.deepEqual(desktopSource.bones, ["WebGapArmatureCollectionAddBone"]); assert.deepEqual(desktopAdded.bones, []); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionAddArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const source = armature.boneCollections.find((value) => value.name === desktopSource.name); const added = armature.boneCollections.find((value) => value.name === desktopAdded.name); assert.ok(source && added); assert.equal(source.index, desktopSource.index); assert.equal(added.index, desktopAdded.index); const boneId = armature.bones?.find((value) => value.name === "WebGapArmatureCollectionAddBone")?.id; assert.ok(boneId); assert.deepEqual(source.boneIds, [boneId]); assert.deepEqual(added.boneIds, []); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, existingCollection: { id: source.id, name: source.name, index: source.index, boneIds: source.boneIds }, addedCollection: { id: added.id, name: added.name, index: added.index, boneIds: added.boneIds } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00222" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00221/armature-collection-add-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} collection=${added.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00222") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00222-operator-armature.collection_assign.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00222/armature-collection-assign-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-assign-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopSource = desktop.after.collections.find((value) => value.name === "WebGapArmatureCollectionAssignSource"); const desktopTarget = desktop.after.collections.find((value) => value.name === "WebGapArmatureCollectionAssignTarget"); assert.ok(desktopSource && desktopTarget); assert.deepEqual(desktopSource.bones, ["WebGapArmatureCollectionAssignBone"]); assert.deepEqual(desktopTarget.bones, ["WebGapArmatureCollectionAssignBone"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionAssignArmature"); assert.ok(armature); const source = armature.boneCollections?.find((value) => value.name === desktopSource.name); const target = armature.boneCollections?.find((value) => value.name === desktopTarget.name); assert.ok(source && target); const boneId = armature.bones?.find((value) => value.name === "WebGapArmatureCollectionAssignBone")?.id; assert.ok(boneId); assert.deepEqual(source.boneIds, [boneId]); assert.deepEqual(target.boneIds, [boneId]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_ASSIGN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, assignedBoneId: boneId, sourceCollection: { id: source.id, name: source.name, boneIds: source.boneIds }, targetCollection: { id: target.id, name: target.name, boneIds: target.boneIds } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00223" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00222/armature-collection-assign-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} assigned=${boneId} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00223") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00223-operator-armature.collection_create_and_assign.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00223/armature-collection-create-assign-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-create-assign-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopSource = desktop.after.collections.find((value) => value.name === "WebGapArmatureCollectionCreateAssignSource"); const desktopAdded = desktop.after.collections.find((value) => value.name === "WebGapArmatureCollectionCreateAssignNew"); assert.ok(desktopSource && desktopAdded); assert.equal(desktopAdded.index, 1); assert.equal(desktop.after.activeIndex, desktopAdded.index); assert.deepEqual(desktopSource.bones, ["WebGapArmatureCollectionCreateAssignBone"]); assert.deepEqual(desktopAdded.bones, ["WebGapArmatureCollectionCreateAssignBone"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionCreateAssignArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const source = armature.boneCollections.find((value) => value.name === desktopSource.name); const added = armature.boneCollections.find((value) => value.name === desktopAdded.name); assert.ok(source && added); assert.equal(added.index, desktopAdded.index); const boneId = armature.bones?.find((value) => value.name === "WebGapArmatureCollectionCreateAssignBone")?.id; assert.ok(boneId); assert.deepEqual(source.boneIds, [boneId]); assert.deepEqual(added.boneIds, [boneId]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_CREATE_AND_ASSIGN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, assignedBoneId: boneId, sourceCollection: { id: source.id, name: source.name, boneIds: source.boneIds }, createdCollection: { id: added.id, name: added.name, index: added.index, boneIds: added.boneIds } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00224" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00223/armature-collection-create-assign-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} created=${added.name} assigned=${boneId} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00224") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00224-operator-armature.collection_deselect.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00224/armature-collection-deselect-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-deselect-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.activeIndex, 0); const desktopActive = desktop.after.bones.find((value) => value.name === "WebGapArmatureCollectionDeselectActiveBone"); const desktopOther = desktop.after.bones.find((value) => value.name === "WebGapArmatureCollectionDeselectOtherBone"); assert.ok(desktopActive && desktopOther); assert.equal(desktopActive.selected, false); assert.equal(desktopOther.selected, true); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionDeselectArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const activeBone = armature.bones?.find((value) => value.name === desktopActive.name); const otherBone = armature.bones?.find((value) => value.name === desktopOther.name); assert.ok(activeBone && otherBone); assert.equal(activeBone.selected, false); assert.equal(otherBone.selected, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_DESELECT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, activeCollectionIndex: 0, deselectedBoneId: activeBone.id, retainedSelectedBoneId: otherBone.id, deselected: activeBone.selected, retainedSelected: otherBone.selected }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00225" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00224/armature-collection-deselect-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} deselected=${activeBone.id} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00225") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00225-operator-armature.collection_move.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00225/armature-collection-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-move-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.equal(desktop.direction, "UP"); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.activeIndex, 0); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionMoveActive", "WebGapArmatureCollectionMoveFirst", "WebGapArmatureCollectionMoveLast"]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [["WebGapArmatureCollectionMoveActiveBone"], ["WebGapArmatureCollectionMoveFirstBone"], ["WebGapArmatureCollectionMoveLastBone"]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionMoveArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 3); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const boneIds = new Map(armature.bones?.map((value) => [value.name, value.id])); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneIds.get(value.bones?.[0] ?? "")])) const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_MOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, direction: desktop.direction, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, direction: "UP", activeCollection: collections[0].name, collectionOrder: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00226" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00225/armature-collection-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} active=${collections[0].name} direction=up desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00226") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00226-operator-armature.collection_remove.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00226/armature-collection-remove-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-remove-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.activeIndex, 1); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionRemoveFirst", "WebGapArmatureCollectionRemoveLast"]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [["WebGapArmatureCollectionRemoveFirstBone"], ["WebGapArmatureCollectionRemoveLastBone"]]); assert.deepEqual(desktop.after.bones, ["WebGapArmatureCollectionRemoveFirstBone", "WebGapArmatureCollectionRemoveLastBone", "WebGapArmatureCollectionRemoveRemovedBone"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionRemoveArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const boneIds = new Map(armature.bones?.map((value) => [value.name, value.id])); assert.ok(boneIds.has("WebGapArmatureCollectionRemoveRemovedBone")); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneIds.get(value.bones?.[0] ?? "")])); assert.ok(!collections.some((value) => value.boneIds.includes(boneIds.get("WebGapArmatureCollectionRemoveRemovedBone")))); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_REMOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, removedCollection: "WebGapArmatureCollectionRemoveRemoved", removedBoneId: boneIds.get("WebGapArmatureCollectionRemoveRemovedBone"), collectionOrder: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00227" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00226/armature-collection-remove-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} removed=WebGapArmatureCollectionRemoveRemoved desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00227") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00227-operator-armature.collection_remove_unused.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00227/armature-collection-remove-unused-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-remove-unused-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.activeIndex, 1); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionRemoveUnusedFirst", "WebGapArmatureCollectionRemoveUnusedLast"]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [["WebGapArmatureCollectionRemoveUnusedFirstBone"], ["WebGapArmatureCollectionRemoveUnusedLastBone"]]); assert.deepEqual(desktop.after.bones, ["WebGapArmatureCollectionRemoveUnusedFirstBone", "WebGapArmatureCollectionRemoveUnusedLastBone"]); assert.deepEqual(desktop.removedCollections, ["WebGapArmatureCollectionRemoveUnusedUnusedFirst", "WebGapArmatureCollectionRemoveUnusedUnusedLast"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionRemoveUnusedArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const boneIds = new Map(armature.bones?.map((value) => [value.name, value.id])); assert.ok(boneIds.has("WebGapArmatureCollectionRemoveUnusedFirstBone")); assert.ok(boneIds.has("WebGapArmatureCollectionRemoveUnusedLastBone")); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneIds.get(value.bones?.[0] ?? "")])); assert.deepEqual(collections.map((value) => value.index), [0, 1]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_REMOVE_UNUSED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, removedCollections: ["WebGapArmatureCollectionRemoveUnusedUnusedFirst", "WebGapArmatureCollectionRemoveUnusedUnusedLast"], collectionOrder: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00228" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00227/armature-collection-remove-unused-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} removed=2 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00228") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00228-operator-armature.collection_select.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00228/armature-collection-select-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-select-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.after.activeIndex, 1); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionSelectFirst", "WebGapArmatureCollectionSelectActive", "WebGapArmatureCollectionSelectLast"]); assert.deepEqual(desktop.after.selectedBones, ["WebGapArmatureCollectionSelectActiveBone", "WebGapArmatureCollectionSelectFirstBone"]); assert.deepEqual(desktop.after.bones, ["WebGapArmatureCollectionSelectActiveBone", "WebGapArmatureCollectionSelectFirstBone", "WebGapArmatureCollectionSelectLastBone"]); assert.equal(desktop.selectedCollection, "WebGapArmatureCollectionSelectActive"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionSelectArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 3); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const boneByName = new Map(armature.bones?.map((value) => [value.name, value])); assert.deepEqual(armature.bones?.filter((value) => value.selected).map((value) => value.name).sort(), desktop.after.selectedBones); assert.equal(boneByName.get("WebGapArmatureCollectionSelectActiveBone")?.selected, true); assert.equal(boneByName.get("WebGapArmatureCollectionSelectFirstBone")?.selected, true); assert.equal(boneByName.get("WebGapArmatureCollectionSelectLastBone")?.selected, false); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneByName.get(value.bones?.[0] ?? "")?.id])); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_SELECT_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, selectedCollection: "WebGapArmatureCollectionSelectActive", selectedBoneIds: armature.bones.filter((value) => value.selected).map((value) => value.id).sort(), collectionOrder: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00229" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00228/armature-collection-select-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} selected=active desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00229") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00229-operator-armature.collection_show_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00229/armature-collection-show-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-show-all-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionShowAllFirst", "WebGapArmatureCollectionShowAllHidden", "WebGapArmatureCollectionShowAllLast"]); assert.ok(desktop.after.collections.every((value) => value.visible === true)); assert.deepEqual(desktop.after.bones, ["WebGapArmatureCollectionShowAllFirstBone", "WebGapArmatureCollectionShowAllHiddenBone", "WebGapArmatureCollectionShowAllLastBone"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionShowAllArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 3); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); assert.ok(collections.every((value) => value.visible === true)); const boneByName = new Map(armature.bones?.map((value) => [value.name, value])); assert.deepEqual(armature.bones?.map((value) => value.name).sort(), desktop.after.bones); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneByName.get(value.bones?.[0] ?? "")?.id])); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_SHOW_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, collectionVisibility: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, visible: value.visible, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00230" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00229/armature-collection-show-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} visible=all desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00230") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00230-operator-armature.collection_unassign.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00230/armature-collection-unassign-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-unassign-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.activeCollection, "WebGapArmatureCollectionUnassignSource"); assert.equal(desktop.unassignedBone, "WebGapArmatureCollectionUnassignBone"); assert.equal(desktop.after.activeIndex, 0); assert.deepEqual(desktop.after.selectedBones, ["WebGapArmatureCollectionUnassignBone"]); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionUnassignSource", "WebGapArmatureCollectionUnassignRetained"]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [[], ["WebGapArmatureCollectionUnassignBone"]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionUnassignArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const boneId = armature.bones?.find((value) => value.name === "WebGapArmatureCollectionUnassignBone")?.id; assert.ok(boneId); assert.deepEqual(collections.map((value) => value.boneIds), [[], [boneId]]); assert.deepEqual(armature.bones?.map((value) => value.name), desktop.after.bones); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_UNASSIGN_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, unassignedBoneId: boneId, activeCollection: collections[0].name, collectionOrder: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00231" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00230/armature-collection-unassign-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} unassigned=${boneId} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00231") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00231-operator-armature.collection_unassign_named.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00231/armature-collection-unassign-named-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-unassign-named-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.namedCollection, "WebGapArmatureCollectionUnassignNamedSource"); assert.equal(desktop.activeCollection, "WebGapArmatureCollectionUnassignNamedRetained"); assert.equal(desktop.after.activeIndex, 1); assert.deepEqual(desktop.after.selectedBones, ["WebGapArmatureCollectionUnassignNamedBone"]); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionUnassignNamedSource", "WebGapArmatureCollectionUnassignNamedRetained"]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [[], ["WebGapArmatureCollectionUnassignNamedBone"]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionUnassignNamedArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 2); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); const named = collections.find((value) => value.name === desktop.namedCollection); const active = collections.find((value) => value.name === desktop.activeCollection); assert.ok(named && active); const boneId = armature.bones?.find((value) => value.name === "WebGapArmatureCollectionUnassignNamedBone")?.id; assert.ok(boneId); assert.deepEqual(named.boneIds, []); assert.deepEqual(active.boneIds, [boneId]); assert.deepEqual(armature.bones?.map((value) => value.name), desktop.after.bones); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_UNASSIGN_NAMED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, namedCollection: desktop.namedCollection, activeCollection: desktop.activeCollection }, wasm: { status: "EXACT", armatureId: armature.id, unassignedBoneId: boneId, namedCollection: { id: named.id, name: named.name, index: named.index, boneIds: named.boneIds }, activeCollection: { id: active.id, name: active.name, index: active.index, boneIds: active.boneIds } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00232" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00231/armature-collection-unassign-named-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} unassigned=${boneId} named=source active=retained desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00232") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00232-operator-armature.collection_unsolo_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00232/armature-collection-unsolo-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-collection-unsolo-all-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.soloCollection, "WebGapArmatureCollectionUnsoloAllSolo"); assert.equal(desktop.after.activeIndex, 1); assert.equal(desktop.after.isSoloActive, false); assert.deepEqual(desktop.after.collections.map((value) => value.name), ["WebGapArmatureCollectionUnsoloAllFirst", "WebGapArmatureCollectionUnsoloAllSolo", "WebGapArmatureCollectionUnsoloAllLast"]); assert.deepEqual(desktop.after.collections.map((value) => value.solo), [false, false, false]); assert.deepEqual(desktop.after.collections.map((value) => value.visible), [true, true, true]); assert.deepEqual(desktop.after.collections.map((value) => value.bones), [["WebGapArmatureCollectionUnsoloAllFirstBone"], ["WebGapArmatureCollectionUnsoloAllSoloBone"], ["WebGapArmatureCollectionUnsoloAllLastBone"]]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCollectionUnsoloAllArmature"); assert.ok(armature); assert.equal(armature.boneCollections?.length, 3); const collections = armature.boneCollections.toSorted((left, right) => left.index - right.index); assert.deepEqual(collections.map((value) => value.name), desktop.after.collections.map((value) => value.name)); assert.deepEqual(collections.map((value) => value.solo), [false, false, false]); assert.deepEqual(collections.map((value) => value.visible), desktop.after.collections.map((value) => value.visible)); const boneByName = new Map(armature.bones?.map((value) => [value.name, value])); assert.deepEqual(collections.map((value) => value.boneIds), desktop.after.collections.map((value) => [boneByName.get(value.bones?.[0] ?? "")?.id])); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COLLECTION_UNSOLO_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, soloCollection: desktop.soloCollection }, wasm: { status: "EXACT", armatureId: armature.id, collectionSolo: collections.map((value) => ({ id: value.id, name: value.name, index: value.index, solo: value.solo, visible: value.visible, boneIds: value.boneIds })) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00233" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00232/armature-collection-unsolo-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} solo=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00233") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00233-operator-armature.copy_bone_color_to_selected.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00233/armature-copy-bone-color-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-copy-bone-color-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureCopyBoneColorSource"); assert.equal(desktop.selectedDestination, "WebGapArmatureCopyBoneColorSelected"); assert.equal(desktop.unselectedDestination, "WebGapArmatureCopyBoneColorUnselected"); const desktopByName = new Map(desktop.after.bones.map((bone) => [bone.name, bone])); const sourceDesktop = desktopByName.get(desktop.sourceBone); const selectedDesktop = desktopByName.get(desktop.selectedDestination); const unselectedDesktop = desktopByName.get(desktop.unselectedDestination); assert.ok(sourceDesktop && selectedDesktop && unselectedDesktop); assert.deepEqual(selectedDesktop, { ...sourceDesktop, name: desktop.selectedDestination }); assert.equal(unselectedDesktop.paletteIndex, 5); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureCopyBoneColorArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 3); const bones = new Map(armature.bones.map((bone) => [bone.name, bone])); const source = bones.get(desktop.sourceBone); const selected = bones.get(desktop.selectedDestination); const unselected = bones.get(desktop.unselectedDestination); assert.ok(source && selected && unselected); assert.deepEqual(selected.color, source.color); assert.deepEqual(unselected.color, { paletteIndex: 5, normal: unselected.color.normal, select: unselected.color.select, active: unselected.color.active, flag: unselected.color.flag, }); assert.equal(source.color.paletteIndex, -1); assert.deepEqual(source.color.normal, sourceDesktop.normal); assert.deepEqual(source.color.select, sourceDesktop.selectColor); assert.deepEqual(source.color.active, sourceDesktop.active); assert.equal(source.selected, true); assert.equal(selected.selected, true); assert.equal(unselected.selected, false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_COPY_BONE_COLOR_TO_SELECTED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", armatureId: armature.id, sourceBone: source.id, selectedDestination: selected.id, unselectedDestination: unselected.id, colors: { source: source.color, selected: selected.color, unselected: unselected.color } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00234" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00233/armature-copy-bone-color-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} copied=selected desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00234") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00234-operator-armature.delete.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00234/armature-delete-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-delete-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.deletedBone, "WebGapArmatureDeleteSelected"); assert.deepEqual(desktop.after.bones.map((bone) => bone.name), ["WebGapArmatureDeleteKeep", "WebGapArmatureDeleteRetain"]); assert.equal(desktop.after.activeBone, null); assert.ok(desktop.after.bones.every((bone) => bone.selected === false)); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureDeleteArmature"); assert.ok(armature); assert.deepEqual(armature.bones?.map((bone) => bone.name), desktop.after.bones.map((bone) => bone.name)); assert.equal(armature.bones?.some((bone) => bone.name === desktop.deletedBone), false); assert.ok(armature.bones?.every((bone) => bone.selected === false)); assert.ok(armature.bones?.every((bone) => bone.parentId === null)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_DELETE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, deletedBone: desktop.deletedBone }, wasm: { status: "EXACT", armatureId: armature.id, remainingBoneIds: armature.bones.map((bone) => bone.id), deletedBonePresent: false, selectedBoneIds: armature.bones.filter((bone) => bone.selected).map((bone) => bone.id) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00235" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00234/armature-delete-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} deleted=${desktop.deletedBone} remaining=${armature.bones.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00235") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00235-operator-armature.dissolve.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00235/armature-dissolve-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-dissolve-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.dissolvedBone, "WebGapArmatureDissolveTip"); assert.deepEqual(desktop.after.bones.map((bone) => bone.name), ["WebGapArmatureDissolveRoot", "WebGapArmatureDissolveOther"]); assert.equal(desktop.after.activeBone, null); assert.ok(desktop.after.bones.every((bone) => bone.selected === false)); const desktopRoot = desktop.after.bones[0]; const desktopOther = desktop.after.bones[1]; assert.deepEqual(desktopRoot, { name: "WebGapArmatureDissolveRoot", selected: false, parent: null, connected: false, head: [0, 0, 0], tail: [0, 3, 0] }); assert.deepEqual(desktopOther, { name: "WebGapArmatureDissolveOther", selected: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureDissolveArmature"); assert.ok(armature); assert.deepEqual(armature.bones?.map((bone) => bone.name), desktop.after.bones.map((bone) => bone.name)); const rootBone = armature.bones?.[0]; const otherBone = armature.bones?.[1]; assert.ok(rootBone && otherBone); assert.equal(rootBone.parentId, null); assert.equal(otherBone.parentId, null); assert.equal(rootBone.selected, false); assert.equal(otherBone.selected, false); assert.deepEqual(rootBone.head, desktopRoot.head); assert.deepEqual(rootBone.tail, desktopRoot.tail); assert.deepEqual(otherBone.head, desktopOther.head); assert.deepEqual(otherBone.tail, desktopOther.tail); assert.equal(armature.bones?.some((bone) => ["WebGapArmatureDissolveMiddle", desktop.dissolvedBone].includes(bone.name)), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_DISSOLVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, dissolvedBone: desktop.dissolvedBone }, wasm: { status: "EXACT", armatureId: armature.id, remainingBoneIds: armature.bones.map((bone) => bone.id), dissolvedBonePresent: false, survivingRoot: { id: rootBone.id, head: rootBone.head, tail: rootBone.tail }, independentBone: { id: otherBone.id, head: otherBone.head, tail: otherBone.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00236" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00235/armature-dissolve-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} dissolved=${desktop.dissolvedBone} remaining=${armature.bones.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00236") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00236-operator-armature.duplicate.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00236/armature-duplicate-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-duplicate-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureDuplicateSource"); assert.equal(desktop.duplicateBone, "WebGapArmatureDuplicateSource.001"); assert.deepEqual(desktop.after.bones.map((bone) => bone.name), ["WebGapArmatureDuplicateSource", "WebGapArmatureDuplicateOther", "WebGapArmatureDuplicateSource.001"]); assert.equal(desktop.after.activeBone, desktop.duplicateBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.duplicateBone]); const desktopSource = desktop.after.bones[0]; const desktopOther = desktop.after.bones[1]; const desktopDuplicate = desktop.after.bones[2]; assert.deepEqual(desktopDuplicate.head, desktopSource.head); assert.deepEqual(desktopDuplicate.tail, desktopSource.tail); assert.equal(desktopDuplicate.parent, null); assert.equal(desktopDuplicate.connected, false); assert.deepEqual(desktopOther, { name: "WebGapArmatureDuplicateOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureDuplicateArmature"); assert.ok(armature); assert.deepEqual(armature.bones?.map((bone) => bone.name), desktop.after.bones.map((bone) => bone.name)); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureDuplicateOther"); const duplicate = armature.bones?.find((bone) => bone.name === desktop.duplicateBone); assert.ok(source && other && duplicate); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(duplicate.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(duplicate.parentId, null); assert.deepEqual(source.head, duplicate.head); assert.deepEqual(source.tail, duplicate.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_DUPLICATE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, duplicateBone: desktop.duplicateBone }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, duplicateBoneId: duplicate.id, otherBoneId: other.id, duplicateSelected: duplicate.selected, geometryMatch: source.head.join(",") === duplicate.head.join(",") && source.tail.join(",") === duplicate.tail.join(",") }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00237" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00236/armature-duplicate-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} duplicate=${desktop.duplicateBone} remaining=${armature.bones.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00237") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00237-operator-armature.duplicate_move.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00237/armature-duplicate-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-duplicate-move-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureDuplicateMoveSource"); assert.equal(desktop.duplicateBone, "WebGapArmatureDuplicateMoveSource.001"); assert.deepEqual(desktop.translation, [1, 2, 3]); assert.deepEqual(desktop.after.bones.map((bone) => bone.name), [desktop.sourceBone, "WebGapArmatureDuplicateMoveOther", desktop.duplicateBone]); assert.equal(desktop.after.activeBone, desktop.duplicateBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.duplicateBone]); const desktopSource = desktop.after.bones[0]; const desktopOther = desktop.after.bones[1]; const desktopDuplicate = desktop.after.bones[2]; assert.deepEqual(desktopSource.head, [0, 0, 0]); assert.deepEqual(desktopSource.tail, [0, 1, 0]); assert.deepEqual(desktopDuplicate.head, [1, 2, 3]); assert.deepEqual(desktopDuplicate.tail, [1, 3, 3]); assert.equal(desktopDuplicate.parent, null); assert.equal(desktopDuplicate.connected, false); assert.deepEqual(desktopOther, { name: "WebGapArmatureDuplicateMoveOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureDuplicateMoveArmature"); assert.ok(armature); assert.deepEqual(armature.bones?.map((bone) => bone.name), desktop.after.bones.map((bone) => bone.name)); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureDuplicateMoveOther"); const duplicate = armature.bones?.find((bone) => bone.name === desktop.duplicateBone); assert.ok(source && other && duplicate); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(duplicate.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(duplicate.parentId, null); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); assert.deepEqual(duplicate.head, desktopDuplicate.head); assert.deepEqual(duplicate.tail, desktopDuplicate.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_DUPLICATE_MOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, duplicateBone: desktop.duplicateBone, translation: desktop.translation }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, duplicateBoneId: duplicate.id, otherBoneId: other.id, duplicateSelected: duplicate.selected, translation: [duplicate.head[0] - source.head[0], duplicate.head[1] - source.head[1], duplicate.head[2] - source.head[2]], geometryMatch: duplicate.head.join(",") === "1,2,3" && duplicate.tail.join(",") === "1,3,3" }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00238" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00237/armature-duplicate-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} duplicate=${desktop.duplicateBone} translation=1,2,3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00238") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00238-operator-armature.duplicate_rename.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00238/armature-duplicate-rename-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-duplicate-rename-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureDuplicateRenameSource"); assert.equal(desktop.duplicateBone, "WebGapArmatureDuplicateRenameCopy"); assert.equal(desktop.search, "Source"); assert.equal(desktop.replace, "Copy"); assert.deepEqual(desktop.after.bones.map((bone) => bone.name), [desktop.sourceBone, "WebGapArmatureDuplicateRenameOther", desktop.duplicateBone]); assert.equal(desktop.after.activeBone, desktop.duplicateBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.duplicateBone]); const desktopSource = desktop.after.bones[0]; const desktopOther = desktop.after.bones[1]; const desktopDuplicate = desktop.after.bones[2]; assert.deepEqual(desktopDuplicate.head, desktopSource.head); assert.deepEqual(desktopDuplicate.tail, desktopSource.tail); assert.equal(desktopDuplicate.parent, null); assert.equal(desktopDuplicate.connected, false); assert.deepEqual(desktopOther, { name: "WebGapArmatureDuplicateRenameOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureDuplicateRenameArmature"); assert.ok(armature); assert.deepEqual(armature.bones?.map((bone) => bone.name), desktop.after.bones.map((bone) => bone.name)); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureDuplicateRenameOther"); const duplicate = armature.bones?.find((bone) => bone.name === desktop.duplicateBone); assert.ok(source && other && duplicate); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(duplicate.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(duplicate.parentId, null); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); assert.deepEqual(duplicate.head, desktopDuplicate.head); assert.deepEqual(duplicate.tail, desktopDuplicate.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_DUPLICATE_RENAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, duplicateBone: desktop.duplicateBone, search: desktop.search, replace: desktop.replace }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, duplicateBoneId: duplicate.id, otherBoneId: other.id, duplicateSelected: duplicate.selected, renamedName: duplicate.name, geometryMatch: source.head.join(",") === duplicate.head.join(",") && source.tail.join(",") === duplicate.tail.join(",") }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00239" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00238/armature-duplicate-rename-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} duplicate=${desktop.duplicateBone} rename=Source->Copy desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00239") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00239-operator-armature.extrude.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00239/armature-extrude-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-extrude-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureExtrudeSource"); assert.equal(desktop.extrudeBone, "WebGapArmatureExtrudeSource.001"); assert.deepEqual(desktop.translation, [0, 1, 0]); assert.deepEqual([...desktop.after.bones.map((bone) => bone.name)].sort(), [desktop.sourceBone, desktop.extrudeBone, "WebGapArmatureExtrudeOther"].sort()); assert.equal(desktop.after.activeBone, desktop.extrudeBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.extrudeBone]); const desktopSource = desktop.after.bones.find((bone) => bone.name === desktop.sourceBone); const desktopExtrude = desktop.after.bones.find((bone) => bone.name === desktop.extrudeBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureExtrudeOther"); assert.deepEqual(desktopExtrude.head, [0, 1, 0]); assert.deepEqual(desktopExtrude.tail, [0, 2, 0]); assert.equal(desktopExtrude.parent, desktop.sourceBone); assert.equal(desktopExtrude.connected, true); assert.deepEqual(desktopOther, { name: "WebGapArmatureExtrudeOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureExtrudeArmature"); assert.ok(armature); assert.deepEqual([...armature.bones?.map((bone) => bone.name)].sort(), [...desktop.after.bones.map((bone) => bone.name)].sort()); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureExtrudeOther"); const extrude = armature.bones?.find((bone) => bone.name === desktop.extrudeBone); assert.ok(source && other && extrude); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(extrude.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(extrude.parentId, source.id); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); const desktopParentTail = desktopSource.tail; assert.deepEqual(extrude.head, desktopExtrude.head.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(extrude.tail, desktopExtrude.tail.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_EXTRUDE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, extrudeBone: desktop.extrudeBone, translation: desktop.translation }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, extrudeBoneId: extrude.id, otherBoneId: other.id, extrudeSelected: extrude.selected, parentId: extrude.parentId, geometry: { head: extrude.head, tail: extrude.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00240" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00239/armature-extrude-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} extrude=${desktop.extrudeBone} translation=0,1,0 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00240") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00240-operator-armature.extrude_forked.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00240/armature-extrude-forked-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-extrude-forked-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.forked, true); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureExtrudeForkedSource"); assert.equal(desktop.forkedBone, "WebGapArmatureExtrudeForkedSource.001"); assert.deepEqual(desktop.translation, [0, 1, 0]); assert.deepEqual([...desktop.after.bones.map((bone) => bone.name)].sort(), [desktop.sourceBone, desktop.forkedBone, "WebGapArmatureExtrudeForkedOther"].sort()); assert.equal(desktop.after.activeBone, desktop.forkedBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.forkedBone]); const desktopSource = desktop.after.bones.find((bone) => bone.name === desktop.sourceBone); const desktopForked = desktop.after.bones.find((bone) => bone.name === desktop.forkedBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureExtrudeForkedOther"); assert.deepEqual(desktopForked.head, [0, 1, 0]); assert.deepEqual(desktopForked.tail, [0, 2, 0]); assert.equal(desktopForked.parent, desktop.sourceBone); assert.equal(desktopForked.connected, false); assert.deepEqual(desktopOther, { name: "WebGapArmatureExtrudeForkedOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureExtrudeForkedArmature"); assert.ok(armature); assert.deepEqual([...armature.bones?.map((bone) => bone.name)].sort(), [...desktop.after.bones.map((bone) => bone.name)].sort()); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureExtrudeForkedOther"); const forked = armature.bones?.find((bone) => bone.name === desktop.forkedBone); assert.ok(source && other && forked); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(forked.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(forked.parentId, source.id); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); const desktopParentTail = desktopSource.tail; assert.deepEqual(forked.head, desktopForked.head.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(forked.tail, desktopForked.tail.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_EXTRUDE_FORKED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, forked: desktop.forked, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, forkedBone: desktop.forkedBone, translation: desktop.translation }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, forkedBoneId: forked.id, otherBoneId: other.id, forkedSelected: forked.selected, parentId: forked.parentId, connected: false, geometry: { head: forked.head, tail: forked.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00241" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00240/armature-extrude-forked-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} forked=${desktop.forkedBone} connected=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00241") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00241-operator-armature.extrude_move.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00241/armature-extrude-move-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-extrude-move-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureExtrudeMoveSource"); assert.equal(desktop.extrudeBone, "WebGapArmatureExtrudeMoveSource.001"); assert.deepEqual(desktop.translation, [0, 1, 0]); assert.deepEqual([...desktop.after.bones.map((bone) => bone.name)].sort(), [desktop.sourceBone, desktop.extrudeBone, "WebGapArmatureExtrudeMoveOther"].sort()); assert.equal(desktop.after.activeBone, desktop.extrudeBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.extrudeBone]); const desktopSource = desktop.after.bones.find((bone) => bone.name === desktop.sourceBone); const desktopExtrude = desktop.after.bones.find((bone) => bone.name === desktop.extrudeBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureExtrudeMoveOther"); assert.deepEqual(desktopExtrude.head, [0, 1, 0]); assert.deepEqual(desktopExtrude.tail, [0, 2, 0]); assert.equal(desktopExtrude.parent, desktop.sourceBone); assert.equal(desktopExtrude.connected, true); assert.deepEqual(desktopOther, { name: "WebGapArmatureExtrudeMoveOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureExtrudeMoveArmature"); assert.ok(armature); assert.deepEqual([...armature.bones?.map((bone) => bone.name)].sort(), [...desktop.after.bones.map((bone) => bone.name)].sort()); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureExtrudeMoveOther"); const extrude = armature.bones?.find((bone) => bone.name === desktop.extrudeBone); assert.ok(source && other && extrude); assert.equal(source.selected, false); assert.equal(other.selected, false); assert.equal(extrude.selected, true); assert.equal(source.parentId, null); assert.equal(other.parentId, null); assert.equal(extrude.parentId, source.id); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); const desktopParentTail = desktopSource.tail; assert.deepEqual(extrude.head, desktopExtrude.head.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(extrude.tail, desktopExtrude.tail.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_EXTRUDE_MOVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, extrudeBone: desktop.extrudeBone, translation: desktop.translation }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, extrudeBoneId: extrude.id, otherBoneId: other.id, extrudeSelected: extrude.selected, parentId: extrude.parentId, connected: true, geometry: { head: extrude.head, tail: extrude.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00242" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00241/armature-extrude-move-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} extrude=${desktop.extrudeBone} translation=0,1,0 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00242") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00242-operator-armature.fill.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00242/armature-fill-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-fill-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.sourceBone, "WebGapArmatureFillSource"); assert.equal(desktop.targetBone, "WebGapArmatureFillTarget"); assert.equal(desktop.bridgeBone, "WebGapArmatureFillBridge"); assert.deepEqual([...desktop.after.bones.map((bone) => bone.name)].sort(), [desktop.sourceBone, desktop.targetBone, desktop.bridgeBone, "WebGapArmatureFillOther"].sort()); assert.equal(desktop.after.activeBone, desktop.bridgeBone); assert.deepEqual(desktop.after.bones.filter((bone) => bone.selected).map((bone) => bone.name), [desktop.bridgeBone]); const desktopSource = desktop.after.bones.find((bone) => bone.name === desktop.sourceBone); const desktopTarget = desktop.after.bones.find((bone) => bone.name === desktop.targetBone); const desktopBridge = desktop.after.bones.find((bone) => bone.name === desktop.bridgeBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureFillOther"); assert.deepEqual(desktopBridge.head, [0, 1, 0]); assert.deepEqual(desktopBridge.tail, [0, 2, 0]); assert.equal(desktopBridge.parent, desktop.sourceBone); assert.equal(desktopBridge.connected, true); assert.deepEqual(desktopTarget.head, [0, 2, 0]); assert.deepEqual(desktopTarget.tail, [0, 3, 0]); assert.deepEqual(desktopOther, { name: "WebGapArmatureFillOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [2, 0, 0], tail: [2, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureFillArmature"); assert.ok(armature); assert.deepEqual([...armature.bones?.map((bone) => bone.name)].sort(), [...desktop.after.bones.map((bone) => bone.name)].sort()); const source = armature.bones?.find((bone) => bone.name === desktop.sourceBone); const target = armature.bones?.find((bone) => bone.name === desktop.targetBone); const bridge = armature.bones?.find((bone) => bone.name === desktop.bridgeBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureFillOther"); assert.ok(source && target && bridge && other); assert.equal(source.selected, false); assert.equal(target.selected, false); assert.equal(bridge.selected, true); assert.equal(other.selected, false); assert.equal(source.parentId, null); assert.equal(target.parentId, null); assert.equal(bridge.parentId, source.id); assert.deepEqual(source.head, desktopSource.head); assert.deepEqual(source.tail, desktopSource.tail); const desktopParentTail = desktopSource.tail; assert.deepEqual(bridge.head, desktopBridge.head.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(bridge.tail, desktopBridge.tail.map((value, index) => value - desktopParentTail[index])); assert.deepEqual(target.head, desktopTarget.head); assert.deepEqual(target.tail, desktopTarget.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_FILL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceBone: desktop.sourceBone, targetBone: desktop.targetBone, bridgeBone: desktop.bridgeBone }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, targetBoneId: target.id, bridgeBoneId: bridge.id, otherBoneId: other.id, bridgeSelected: bridge.selected, parentId: bridge.parentId, connected: true, geometry: { head: bridge.head, tail: bridge.tail } }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00243" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00242/armature-fill-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} bridge=${desktop.bridgeBone} endpoints=source-tail,target-head desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00243") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00243-operator-armature.flip_names.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00243/armature-flip-names-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-flip-names-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.doStripNumbers, false); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.leftGeometryBone, "WebGapArmatureFlipBone.R"); assert.equal(desktop.rightGeometryBone, "WebGapArmatureFlipBone.L"); const desktopLeft = desktop.after.bones.find((bone) => bone.head[0] === -1); const desktopRight = desktop.after.bones.find((bone) => bone.head[0] === 1); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureFlipOther"); assert.equal(desktopLeft.name, desktop.leftGeometryBone); assert.equal(desktopRight.name, desktop.rightGeometryBone); assert.deepEqual(desktopLeft.head, [-1, 0, 0]); assert.deepEqual(desktopLeft.tail, [-1, 1, 0]); assert.deepEqual(desktopRight.head, [1, 0, 0]); assert.deepEqual(desktopRight.tail, [1, 1, 0]); assert.deepEqual(desktopOther, { name: "WebGapArmatureFlipOther", selected: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [0, 0, 2], tail: [0, 1, 2] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureFlipNamesArmature"); assert.ok(armature); const left = armature.bones?.find((bone) => bone.head[0] === -1); const right = armature.bones?.find((bone) => bone.head[0] === 1); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureFlipOther"); assert.ok(left && right && other); assert.equal(left.name, desktop.leftGeometryBone); assert.equal(right.name, desktop.rightGeometryBone); assert.equal(left.selected, true); assert.equal(right.selected, true); assert.equal(other.selected, false); assert.deepEqual(left.head, desktopLeft.head); assert.deepEqual(left.tail, desktopLeft.tail); assert.deepEqual(right.head, desktopRight.head); assert.deepEqual(right.tail, desktopRight.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_FLIP_NAMES_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, leftGeometryBone: desktop.leftGeometryBone, rightGeometryBone: desktop.rightGeometryBone, doStripNumbers: desktop.doStripNumbers }, wasm: { status: "EXACT", armatureId: armature.id, leftBoneId: left.id, rightBoneId: right.id, otherBoneId: other.id, leftName: left.name, rightName: right.name, geometryNamesFlipped: left.name === "WebGapArmatureFlipBone.R" && right.name === "WebGapArmatureFlipBone.L" }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00244" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00243/armature-flip-names-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} left=${left.name} right=${right.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00244") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00244-operator-armature.hide.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00244/armature-hide-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-hide-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.unselected, false); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.hiddenBone, "WebGapArmatureHideSelected"); const desktopHidden = desktop.after.bones.find((bone) => bone.name === desktop.hiddenBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureHideOther"); assert.deepEqual(desktopHidden, { name: desktop.hiddenBone, selected: false, hidden: true, selectHead: false, selectTail: false, parent: null, connected: false, head: [-1, 0, 0], tail: [-1, 1, 0] }); assert.deepEqual(desktopOther, { name: "WebGapArmatureHideOther", selected: false, hidden: false, selectHead: false, selectTail: false, parent: null, connected: false, head: [1, 0, 0], tail: [1, 1, 0] }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureHideArmature"); assert.ok(armature); const hidden = armature.bones?.find((bone) => bone.name === desktop.hiddenBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureHideOther"); assert.ok(hidden && other); assert.equal(hidden.hidden, true); assert.equal(hidden.selected, false); assert.equal(other.hidden, false); assert.equal(other.selected, false); assert.deepEqual(hidden.head, desktopHidden.head); assert.deepEqual(hidden.tail, desktopHidden.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_HIDE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, hiddenBone: desktop.hiddenBone, unselected: desktop.unselected }, wasm: { status: "EXACT", armatureId: armature.id, hiddenBoneId: hidden.id, otherBoneId: other.id, hidden: hidden.hidden, selectedAfterHide: hidden.selected, otherHidden: other.hidden }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00245" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00244/armature-hide-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} hidden=${hidden.name} hiddenFlag=${hidden.hidden} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00245") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00245-operator-armature.move_to_collection.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00245/armature-move-to-collection-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-move-to-collection-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.collectionIndex, 1); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.movedBone, "WebGapArmatureMoveCollectionSelected"); assert.equal(desktop.sourceCollection, "WebGapArmatureMoveCollectionSource"); assert.equal(desktop.targetCollection, "WebGapArmatureMoveCollectionTarget"); const desktopMove = desktop.after.bones.find((bone) => bone.name === desktop.movedBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureMoveCollectionOther"); assert.deepEqual(desktopMove.collections, [desktop.targetCollection]); assert.deepEqual(desktopOther.collections, [desktop.sourceCollection]); assert.equal(desktopMove.selected, true); assert.equal(desktopOther.selected, false); assert.deepEqual(desktopMove.head, [-1, 0, 0]); assert.deepEqual(desktopMove.tail, [-1, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureMoveCollectionArmature"); assert.ok(armature); const moved = armature.bones?.find((bone) => bone.name === desktop.movedBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureMoveCollectionOther"); assert.ok(moved && other); const sourceCollection = armature.boneCollections?.find((collection) => collection.name === desktop.sourceCollection); const targetCollection = armature.boneCollections?.find((collection) => collection.name === desktop.targetCollection); assert.ok(sourceCollection && targetCollection); assert.deepEqual(sourceCollection.boneIds, [other.id]); assert.deepEqual(targetCollection.boneIds, [moved.id]); assert.equal(moved.selected, true); assert.equal(other.selected, false); assert.deepEqual(moved.head, desktopMove.head); assert.deepEqual(moved.tail, desktopMove.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_MOVE_TO_COLLECTION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, movedBone: desktop.movedBone, sourceCollection: desktop.sourceCollection, targetCollection: desktop.targetCollection }, wasm: { status: "EXACT", armatureId: armature.id, movedBoneId: moved.id, otherBoneId: other.id, sourceCollectionId: sourceCollection.id, targetCollectionId: targetCollection.id, sourceMembers: sourceCollection.boneIds, targetMembers: targetCollection.boneIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00246" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00245/armature-move-to-collection-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} moved=${moved.name} target=${targetCollection.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00246") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00246-operator-armature.parent_clear.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00246/armature-parent-clear-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-parent-clear-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.clearType, "CLEAR"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.parentBone, "WebGapArmatureParentClearParent"); assert.equal(desktop.childBone, "WebGapArmatureParentClearChild"); const desktopParent = desktop.after.bones.find((bone) => bone.name === desktop.parentBone); const desktopChild = desktop.after.bones.find((bone) => bone.name === desktop.childBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureParentClearOther"); assert.equal(desktopChild.parent, null); assert.equal(desktopChild.connected, false); assert.equal(desktopChild.selected, true); assert.equal(desktopParent.parent, null); assert.equal(desktopOther.parent, null); assert.deepEqual(desktopChild.head, [0, 1, 0]); assert.deepEqual(desktopChild.tail, [0, 2, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureParentClearArmature"); assert.ok(armature); const parent = armature.bones?.find((bone) => bone.name === desktop.parentBone); const child = armature.bones?.find((bone) => bone.name === desktop.childBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureParentClearOther"); assert.ok(parent && child && other); assert.equal(child.parentId, null); assert.equal(child.selected, true); assert.equal(parent.parentId, null); assert.equal(other.parentId, null); assert.deepEqual(child.head, desktopChild.head); assert.deepEqual(child.tail, desktopChild.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_PARENT_CLEAR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, parentBone: desktop.parentBone, childBone: desktop.childBone, clearType: desktop.clearType }, wasm: { status: "EXACT", armatureId: armature.id, parentBoneId: parent.id, childBoneId: child.id, otherBoneId: other.id, childParentId: child.parentId, childSelected: child.selected }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00247" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00246/armature-parent-clear-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} child=${child.name} parentId=${child.parentId} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00247") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00247-operator-armature.parent_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00247/armature-parent-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-parent-set-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.setType, "CONNECTED"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.parentBone, "WebGapArmatureParentSetParent"); assert.equal(desktop.childBone, "WebGapArmatureParentSetChild"); const desktopParent = desktop.after.bones.find((bone) => bone.name === desktop.parentBone); const desktopChild = desktop.after.bones.find((bone) => bone.name === desktop.childBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureParentSetOther"); assert.equal(desktopChild.parent, desktop.parentBone); assert.equal(desktopChild.connected, true); assert.equal(desktopChild.selected, true); assert.equal(desktopParent.parent, null); assert.equal(desktopOther.parent, null); assert.deepEqual(desktopChild.head, [0, 1, 0]); assert.deepEqual(desktopChild.tail, [0, 2, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureParentSetArmature"); assert.ok(armature); const parent = armature.bones?.find((bone) => bone.name === desktop.parentBone); const child = armature.bones?.find((bone) => bone.name === desktop.childBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureParentSetOther"); assert.ok(parent && child && other); assert.equal(child.parentId, parent.id); assert.equal(child.connected, true); assert.equal(child.selected, true); assert.equal(parent.parentId, null); assert.equal(other.parentId, null); assert.deepEqual(child.head, desktopChild.head.map((value, index) => value - desktopParent.tail[index])); assert.deepEqual(child.tail, desktopChild.tail.map((value, index) => value - desktopParent.tail[index])); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_PARENT_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, parentBone: desktop.parentBone, childBone: desktop.childBone, setType: desktop.setType }, wasm: { status: "EXACT", armatureId: armature.id, parentBoneId: parent.id, childBoneId: child.id, otherBoneId: other.id, childParentId: child.parentId, childConnected: child.connected, childSelected: child.selected }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00248" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00247/armature-parent-set-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} child=${child.name} parentId=${child.parentId} connected=${child.connected} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00248") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00248-operator-armature.reveal.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00248/armature-reveal-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-reveal-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.select, true); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.revealedBone, "WebGapArmatureRevealHidden"); const desktopRevealed = desktop.after.bones.find((bone) => bone.name === desktop.revealedBone); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureRevealOther"); assert.equal(desktopRevealed.hidden, false); assert.equal(desktopRevealed.selected, true); assert.equal(desktopOther.hidden, false); assert.equal(desktopOther.selected, false); assert.deepEqual(desktopRevealed.head, [-1, 0, 0]); assert.deepEqual(desktopRevealed.tail, [-1, 1, 0]); assert.deepEqual(desktopOther.head, [1, 0, 0]); assert.deepEqual(desktopOther.tail, [1, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureRevealArmature"); assert.ok(armature); const revealed = armature.bones?.find((bone) => bone.name === desktop.revealedBone); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureRevealOther"); assert.ok(revealed && other); assert.equal(revealed.hidden, false); assert.equal(revealed.selected, true); assert.equal(other.hidden, false); assert.equal(other.selected, false); assert.deepEqual(revealed.head, desktopRevealed.head); assert.deepEqual(revealed.tail, desktopRevealed.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_REVEAL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, revealedBone: desktop.revealedBone, select: desktop.select }, wasm: { status: "EXACT", armatureId: armature.id, revealedBoneId: revealed.id, otherBoneId: other.id, revealedHidden: revealed.hidden, revealedSelected: revealed.selected, otherHidden: other.hidden }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00249" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00248/armature-reveal-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} revealed=${revealed.name} hidden=${revealed.hidden} selected=${revealed.selected} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00249") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00249-operator-armature.roll_clear.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00249/armature-roll-clear-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-roll-clear-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.equal(desktop.bone, "WebGapArmatureRollClearBone"); assert.equal(desktop.roll, 0); const desktopBone = desktop.after.bones.find((bone) => bone.name === desktop.bone); assert.ok(desktopBone); assert.deepEqual(desktopBone.head, [0, 0, 0]); assert.deepEqual(desktopBone.tail, [0, 2, 0]); assert.equal(desktopBone.selected, true); assert.equal(desktopBone.hidden, false); const expectedMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; desktopBone.matrix.forEach((value, index) => assert.ok(Math.abs(value - expectedMatrix[index]) < 1e-5, `desktop matrix[${index}] ${value} != ${expectedMatrix[index]}`)); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureRollClearArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 1); const bone = armature.bones[0]; assert.equal(bone.name, desktop.bone); assert.equal(bone.hidden, false); assert.equal(bone.selected, true); assert.deepEqual(bone.head, desktopBone.head); assert.deepEqual(bone.tail, desktopBone.tail); bone.restMatrix.forEach((value, index) => assert.ok(Math.abs(value - expectedMatrix[index]) < 1e-5, `WASM restMatrix[${index}] ${value} != ${expectedMatrix[index]}`)); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_ROLL_CLEAR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, bone: desktop.bone, roll: desktop.roll }, wasm: { status: "EXACT", armatureId: armature.id, boneId: bone.id, boneName: bone.name, hidden: bone.hidden, selected: bone.selected, restMatrix: bone.restMatrix }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00250" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00249/armature-roll-clear-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} bone=${bone.name} roll=${desktop.roll} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00250") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00250-operator-armature.select_all.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00250/armature-select-all-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-all-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.action, "SELECT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.deepEqual(desktop.selectedBones, ["WebGapArmatureSelectAllParent", "WebGapArmatureSelectAllOther"]); for (const bone of desktop.after.bones) { assert.equal(bone.selected, true); assert.equal(bone.selectHead, true); assert.equal(bone.selectTail, true); assert.equal(bone.hidden, false); } const desktopParent = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureSelectAllParent"); const desktopOther = desktop.after.bones.find((bone) => bone.name === "WebGapArmatureSelectAllOther"); assert.deepEqual(desktopParent.head, [0, 0, 0]); assert.deepEqual(desktopParent.tail, [0, 1, 0]); assert.deepEqual(desktopOther.head, [2, 0, 0]); assert.deepEqual(desktopOther.tail, [2, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectAllArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 2); const parent = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectAllParent"); const other = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectAllOther"); assert.ok(parent && other); assert.equal(parent.selected, true); assert.equal(other.selected, true); assert.equal(parent.hidden, false); assert.equal(other.hidden, false); assert.deepEqual(parent.head, desktopParent.head); assert.deepEqual(parent.tail, desktopParent.tail); assert.deepEqual(other.head, desktopOther.head); assert.deepEqual(other.tail, desktopOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_ALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, selectedBones: desktop.selectedBones, action: desktop.action }, wasm: { status: "EXACT", armatureId: armature.id, parentBoneId: parent.id, otherBoneId: other.id, parentSelected: parent.selected, otherSelected: other.selected, parentHead: parent.head, parentTail: parent.tail, otherHead: other.head, otherTail: other.tail }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00251" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00250/armature-select-all-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} selected=2 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00251") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00251-operator-armature.select_hierarchy.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00251/armature-select-hierarchy-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-hierarchy-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.direction, "CHILD"); assert.equal(desktop.extend, false); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.ok(["WebGapArmatureSelectHierarchyRoot", "WebGapArmatureSelectHierarchyChild"].includes(desktop.activeBoneBefore)); assert.equal(desktop.activeBoneAfter, "WebGapArmatureSelectHierarchyChild"); const desktopBefore = Object.fromEntries(desktop.before.bones.map((bone) => [bone.name, bone])); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); if (desktop.operatorStatus === "FINISHED") { assert.equal(desktopBefore.WebGapArmatureSelectHierarchyRoot.selected, true); assert.equal(desktopBefore.WebGapArmatureSelectHierarchyChild.selected, false); } else { assert.equal(desktopBefore.WebGapArmatureSelectHierarchyRoot.selected, false); assert.equal(desktopBefore.WebGapArmatureSelectHierarchyChild.selected, true); } assert.equal(desktopAfter.WebGapArmatureSelectHierarchyRoot.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectHierarchyChild.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectHierarchyOther.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectHierarchyChild.parent, "WebGapArmatureSelectHierarchyRoot"); assert.equal(desktopAfter.WebGapArmatureSelectHierarchyChild.connected, true); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyRoot.head, [0, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyRoot.tail, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyChild.head, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyChild.tail, [0, 2, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyOther.head, [2, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectHierarchyOther.tail, [2, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectHierarchyArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 3); const rootBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectHierarchyRoot"); const childBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectHierarchyChild"); const otherBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectHierarchyOther"); assert.ok(rootBone && childBone && otherBone); assert.equal(rootBone.selected, false); assert.equal(childBone.selected, true); assert.equal(otherBone.selected, false); assert.equal(childBone.parentId, rootBone.id); assert.equal(childBone.connected, true); assert.equal(rootBone.hidden, false); assert.equal(childBone.hidden, false); assert.equal(otherBone.hidden, false); assert.deepEqual(rootBone.head, desktopAfter.WebGapArmatureSelectHierarchyRoot.head); assert.deepEqual(rootBone.tail, desktopAfter.WebGapArmatureSelectHierarchyRoot.tail); assert.deepEqual(childBone.head, desktopAfter.WebGapArmatureSelectHierarchyChild.head); assert.deepEqual(childBone.tail, desktopAfter.WebGapArmatureSelectHierarchyChild.tail); assert.deepEqual(otherBone.head, desktopAfter.WebGapArmatureSelectHierarchyOther.head); assert.deepEqual(otherBone.tail, desktopAfter.WebGapArmatureSelectHierarchyOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_HIERARCHY_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, direction: desktop.direction, extend: desktop.extend, activeBoneBefore: desktop.activeBoneBefore, activeBoneAfter: desktop.activeBoneAfter }, wasm: { status: "EXACT", armatureId: armature.id, rootBoneId: rootBone.id, childBoneId: childBone.id, otherBoneId: otherBone.id, rootSelected: rootBone.selected, childSelected: childBone.selected, otherSelected: otherBone.selected, childParentId: childBone.parentId, childConnected: childBone.connected, rootHead: rootBone.head, rootTail: rootBone.tail, childHead: childBone.head, childTail: childBone.tail, otherHead: otherBone.head, otherTail: otherBone.tail }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00252" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00251/armature-select-hierarchy-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} direction=child child=${childBone.name} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00252") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00252-operator-armature.select_less.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00252/armature-select-less-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-less-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopBefore = Object.fromEntries(desktop.before.bones.map((bone) => [bone.name, bone])); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); if (desktop.operatorStatus === "FINISHED") { assert.equal(desktopBefore.WebGapArmatureSelectLessRoot.selected, false); assert.equal(desktopBefore.WebGapArmatureSelectLessRoot.selectHead, true); assert.equal(desktopBefore.WebGapArmatureSelectLessRoot.selectTail, false); } else { assert.equal(desktopBefore.WebGapArmatureSelectLessRoot.selected, false); } assert.equal(desktopAfter.WebGapArmatureSelectLessRoot.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectLessRoot.selectHead, false); assert.equal(desktopAfter.WebGapArmatureSelectLessRoot.selectTail, false); assert.equal(desktopAfter.WebGapArmatureSelectLessChild.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectLessOther.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectLessOther.selectHead, true); assert.equal(desktopAfter.WebGapArmatureSelectLessOther.selectTail, true); assert.equal(desktopAfter.WebGapArmatureSelectLessChild.parent, "WebGapArmatureSelectLessRoot"); assert.equal(desktopAfter.WebGapArmatureSelectLessChild.connected, true); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessRoot.head, [0, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessRoot.tail, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessChild.head, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessChild.tail, [0, 2, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessOther.head, [2, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLessOther.tail, [2, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectLessArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 3); const rootBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLessRoot"); const childBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLessChild"); const otherBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLessOther"); assert.ok(rootBone && childBone && otherBone); assert.equal(rootBone.selected, false); assert.equal(childBone.selected, false); assert.equal(otherBone.selected, true); assert.equal(childBone.parentId, rootBone.id); assert.equal(childBone.connected, true); assert.equal(rootBone.hidden, false); assert.equal(childBone.hidden, false); assert.equal(otherBone.hidden, false); assert.deepEqual(rootBone.head, desktopAfter.WebGapArmatureSelectLessRoot.head); assert.deepEqual(rootBone.tail, desktopAfter.WebGapArmatureSelectLessRoot.tail); assert.deepEqual(childBone.head, desktopAfter.WebGapArmatureSelectLessChild.head); assert.deepEqual(childBone.tail, desktopAfter.WebGapArmatureSelectLessChild.tail); assert.deepEqual(otherBone.head, desktopAfter.WebGapArmatureSelectLessOther.head); assert.deepEqual(otherBone.tail, desktopAfter.WebGapArmatureSelectLessOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_LESS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, rootBone: desktop.rootBone, childBone: desktop.childBone, otherBone: desktop.otherBone }, wasm: { status: "EXACT", armatureId: armature.id, rootBoneId: rootBone.id, childBoneId: childBone.id, otherBoneId: otherBone.id, rootSelected: rootBone.selected, childSelected: childBone.selected, otherSelected: otherBone.selected, childParentId: childBone.parentId, childConnected: childBone.connected, rootHead: rootBone.head, rootTail: rootBone.tail, childHead: childBone.head, childTail: childBone.tail, otherHead: otherBone.head, otherTail: otherBone.tail }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00253" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00252/armature-select-less-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} root-selected=false other-selected=true desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00253") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00253-operator-armature.select_linked.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00253/armature-select-linked-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-linked-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.allForks, false); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopBefore = Object.fromEntries(desktop.before.bones.map((bone) => [bone.name, bone])); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); assert.equal(desktopAfter.WebGapArmatureSelectLinkedRoot.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectLinkedChild.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectLinkedGrandchild.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectLinkedOther.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectLinkedChild.parent, "WebGapArmatureSelectLinkedRoot"); assert.equal(desktopAfter.WebGapArmatureSelectLinkedGrandchild.parent, "WebGapArmatureSelectLinkedChild"); assert.equal(desktopAfter.WebGapArmatureSelectLinkedChild.connected, true); assert.equal(desktopAfter.WebGapArmatureSelectLinkedGrandchild.connected, true); for (const bone of Object.values(desktopAfter)) { if (!bone || typeof bone !== "object" || !bone.name) continue; assert.equal(bone.hidden, false); } assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedRoot.head, [0, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedRoot.tail, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedChild.head, [0, 1, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedChild.tail, [0, 2, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedGrandchild.head, [0, 2, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedGrandchild.tail, [0, 3, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedOther.head, [2, 0, 0]); assert.deepEqual(desktopAfter.WebGapArmatureSelectLinkedOther.tail, [2, 1, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectLinkedArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 4); const rootBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedRoot"); const childBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedChild"); const grandchildBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedGrandchild"); const otherBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedOther"); assert.ok(rootBone && childBone && grandchildBone && otherBone); assert.equal(rootBone.selected, true); assert.equal(childBone.selected, true); assert.equal(grandchildBone.selected, true); assert.equal(otherBone.selected, false); assert.equal(childBone.parentId, rootBone.id); assert.equal(grandchildBone.parentId, childBone.id); assert.equal(childBone.connected, true); assert.equal(grandchildBone.connected, true); for (const bone of [rootBone, childBone, grandchildBone, otherBone]) assert.equal(bone.hidden, false); for (const [bone, expected] of [[rootBone, desktopAfter.WebGapArmatureSelectLinkedRoot], [childBone, desktopAfter.WebGapArmatureSelectLinkedChild], [grandchildBone, desktopAfter.WebGapArmatureSelectLinkedGrandchild], [otherBone, desktopAfter.WebGapArmatureSelectLinkedOther]]) { assert.deepEqual(bone.head, expected.head); assert.deepEqual(bone.tail, expected.tail); } const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_LINKED_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, selectedChain: desktop.selectedChain, unselectedBone: desktop.unselectedBone, allForks: desktop.allForks }, wasm: { status: "EXACT", armatureId: armature.id, rootBoneId: rootBone.id, childBoneId: childBone.id, grandchildBoneId: grandchildBone.id, otherBoneId: otherBone.id, rootSelected: rootBone.selected, childSelected: childBone.selected, grandchildSelected: grandchildBone.selected, otherSelected: otherBone.selected, childParentId: childBone.parentId, grandchildParentId: grandchildBone.parentId }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00254" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00253/armature-select-linked-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} linked-chain=3 other-selected=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00254") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00254-operator-armature.select_linked_pick.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00254/armature-select-linked-pick-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-linked-pick-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.pickedBone, "WebGapArmatureSelectLinkedPickRoot"); assert.equal(desktop.deselect, false); assert.equal(desktop.allForks, false); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); for (const name of ["WebGapArmatureSelectLinkedPickRoot", "WebGapArmatureSelectLinkedPickChild", "WebGapArmatureSelectLinkedPickGrandchild"]) assert.equal(desktopAfter[name].selected, true); assert.equal(desktopAfter.WebGapArmatureSelectLinkedPickOther.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectLinkedPickChild.parent, "WebGapArmatureSelectLinkedPickRoot"); assert.equal(desktopAfter.WebGapArmatureSelectLinkedPickGrandchild.parent, "WebGapArmatureSelectLinkedPickChild"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectLinkedPickArmature"); assert.ok(armature); assert.equal(armature.bones?.length, 4); const rootBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedPickRoot"); const childBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedPickChild"); const grandchildBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedPickGrandchild"); const otherBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectLinkedPickOther"); assert.ok(rootBone && childBone && grandchildBone && otherBone); assert.equal(rootBone.selected, true); assert.equal(childBone.selected, true); assert.equal(grandchildBone.selected, true); assert.equal(otherBone.selected, false); assert.equal(childBone.parentId, rootBone.id); assert.equal(grandchildBone.parentId, childBone.id); assert.equal(childBone.connected, true); assert.equal(grandchildBone.connected, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_LINKED_PICK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, pickedBone: desktop.pickedBone, selectedChain: desktop.selectedChain, unselectedBone: desktop.unselectedBone, deselect: desktop.deselect, allForks: desktop.allForks }, wasm: { status: "EXACT", armatureId: armature.id, rootBoneId: rootBone.id, childBoneId: childBone.id, grandchildBoneId: grandchildBone.id, otherBoneId: otherBone.id, rootSelected: rootBone.selected, childSelected: childBone.selected, grandchildSelected: grandchildBone.selected, otherSelected: otherBone.selected, childParentId: childBone.parentId, grandchildParentId: grandchildBone.parentId }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00255" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00254/armature-select-linked-pick-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} picked=root linked-chain=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00255") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00255-operator-armature.select_mirror.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00255/armature-select-mirror-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-mirror-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.onlyActive, false); assert.equal(desktop.extend, false); assert.equal(desktop.leftBone, "WebGapSelectMirror.L"); assert.equal(desktop.rightBone, "WebGapSelectMirror.R"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); assert.equal(desktopAfter["WebGapSelectMirror.L"].selected, false); assert.equal(desktopAfter["WebGapSelectMirror.R"].selected, true); assert.equal(desktopAfter.WebGapSelectMirrorCenter.selected, false); assert.deepEqual(desktopAfter["WebGapSelectMirror.L"].head, [-2, 0, 0]); assert.deepEqual(desktopAfter["WebGapSelectMirror.R"].head, [2, 0, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectMirrorArmature"); assert.ok(armature); const leftBone = armature.bones?.find((bone) => bone.name === "WebGapSelectMirror.L"); const rightBone = armature.bones?.find((bone) => bone.name === "WebGapSelectMirror.R"); const centerBone = armature.bones?.find((bone) => bone.name === "WebGapSelectMirrorCenter"); assert.ok(leftBone && rightBone && centerBone); assert.equal(leftBone.selected, false); assert.equal(rightBone.selected, true); assert.equal(centerBone.selected, false); assert.deepEqual(leftBone.head, desktopAfter["WebGapSelectMirror.L"].head); assert.deepEqual(rightBone.head, desktopAfter["WebGapSelectMirror.R"].head); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_MIRROR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, leftBone: desktop.leftBone, rightBone: desktop.rightBone, centerBone: desktop.centerBone, onlyActive: desktop.onlyActive, extend: desktop.extend }, wasm: { status: "EXACT", armatureId: armature.id, leftBoneId: leftBone.id, rightBoneId: rightBone.id, centerBoneId: centerBone.id, leftSelected: leftBone.selected, rightSelected: rightBone.selected, centerSelected: centerBone.selected, leftHead: leftBone.head, rightHead: rightBone.head }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00256" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00255/armature-select-mirror-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} mirror=left-to-right desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00256") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00256-operator-armature.select_more.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00256/armature-select-more-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-more-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); assert.equal(desktopAfter.WebGapArmatureSelectMoreRoot.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectMoreChild.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectMoreGrandchild.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectMoreOther.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectMoreChild.parent, "WebGapArmatureSelectMoreRoot"); assert.equal(desktopAfter.WebGapArmatureSelectMoreGrandchild.parent, "WebGapArmatureSelectMoreChild"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectMoreArmature"); assert.ok(armature); const rootBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectMoreRoot"); const childBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectMoreChild"); const grandchildBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectMoreGrandchild"); const otherBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectMoreOther"); assert.ok(rootBone && childBone && grandchildBone && otherBone); assert.equal(rootBone.selected, true); assert.equal(childBone.selected, true); assert.equal(grandchildBone.selected, false); assert.equal(otherBone.selected, false); assert.equal(childBone.parentId, rootBone.id); assert.equal(grandchildBone.parentId, childBone.id); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_MORE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, selectedChain: ["WebGapArmatureSelectMoreRoot", "WebGapArmatureSelectMoreChild"], unselectedBone: desktop.unselectedBone }, wasm: { status: "EXACT", armatureId: armature.id, rootBoneId: rootBone.id, childBoneId: childBone.id, grandchildBoneId: grandchildBone.id, otherBoneId: otherBone.id, rootSelected: rootBone.selected, childSelected: childBone.selected, grandchildSelected: grandchildBone.selected, otherSelected: otherBone.selected, childParentId: childBone.parentId, grandchildParentId: grandchildBone.parentId }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00257" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00256/armature-select-more-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} selected-chain=2 other-selected=false desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00257") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00257-operator-armature.select_similar.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00257/armature-select-similar-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-select-similar-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.type, "LENGTH"); assert.equal(desktop.threshold, 0.1); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopAfter = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); assert.equal(desktopAfter.WebGapArmatureSelectSimilarActive.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectSimilarSameLength.selected, true); assert.equal(desktopAfter.WebGapArmatureSelectSimilarDifferentLength.selected, false); assert.equal(desktopAfter.WebGapArmatureSelectSimilarActive.length, 1); assert.equal(desktopAfter.WebGapArmatureSelectSimilarSameLength.length, 1); assert.equal(desktopAfter.WebGapArmatureSelectSimilarDifferentLength.length, 2); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSelectSimilarArmature"); assert.ok(armature); const activeBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectSimilarActive"); const similarBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectSimilarSameLength"); const differentBone = armature.bones?.find((bone) => bone.name === "WebGapArmatureSelectSimilarDifferentLength"); assert.ok(activeBone && similarBone && differentBone); assert.equal(activeBone.selected, true); assert.equal(similarBone.selected, true); assert.equal(differentBone.selected, false); assert.deepEqual(activeBone.head, desktopAfter.WebGapArmatureSelectSimilarActive.head); assert.deepEqual(similarBone.head, desktopAfter.WebGapArmatureSelectSimilarSameLength.head); assert.deepEqual(differentBone.head, desktopAfter.WebGapArmatureSelectSimilarDifferentLength.head); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SELECT_SIMILAR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, type: desktop.type, threshold: desktop.threshold, activeBone: desktop.activeBone, similarBone: desktop.similarBone, differentBone: desktop.differentBone }, wasm: { status: "EXACT", armatureId: armature.id, activeBoneId: activeBone.id, similarBoneId: similarBone.id, differentBoneId: differentBone.id, activeSelected: activeBone.selected, similarSelected: similarBone.selected, differentSelected: differentBone.selected }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00258" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00257/armature-select-similar-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} type=length same=1 different=2 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00258") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00258-operator-armature.separate.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00258/armature-separate-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-separate-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.ok([true, false].includes(desktop.poll)); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); const desktopNames = desktop.after.objects.map((value) => value.object).sort(); assert.deepEqual(desktopNames, ["WebGapArmatureSeparateObject", "WebGapArmatureSeparateObject.001"]); const desktopOriginal = desktop.after.objects.find((value) => value.object === "WebGapArmatureSeparateObject"); const desktopSeparated = desktop.after.objects.find((value) => value.object === "WebGapArmatureSeparateObject.001"); assert.deepEqual(desktopOriginal.bones.map((value) => value.name), ["WebGapArmatureSeparateRetained"]); assert.deepEqual(desktopSeparated.bones.map((value) => value.name), ["WebGapArmatureSeparateSelected"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armatures = before.armatures?.filter((value) => value.name.startsWith("WebGapArmatureSeparateArmature")); assert.equal(armatures?.length, 2); const original = armatures.find((value) => value.name === "WebGapArmatureSeparateArmature"); const separated = armatures.find((value) => value.name === "WebGapArmatureSeparateArmature.001"); assert.ok(original && separated); assert.deepEqual(original.bones.map((value) => value.name), ["WebGapArmatureSeparateRetained"]); assert.deepEqual(separated.bones.map((value) => value.name), ["WebGapArmatureSeparateSelected"]); assert.equal(original.bones[0].selected, false); assert.equal(separated.bones[0].selected, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SEPARATE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, sourceObject: desktop.sourceObject, separatedObject: desktop.separatedObject, selectedBone: desktop.selectedBone, retainedBone: desktop.retainedBone }, wasm: { status: "EXACT", originalArmatureId: original.id, separatedArmatureId: separated.id, originalBoneId: original.bones[0].id, separatedBoneId: separated.bones[0].id, originalBone: original.bones[0].name, separatedBone: separated.bones[0].name, originalSelected: original.bones[0].selected, separatedSelected: separated.bones[0].selected }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00259" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00258/armature-separate-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armatures=2 retained=1 separated=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00259") { const fixture=path.join(root,"tests/files/web/generated/M16-GAP-00259-operator-armature.shortest_path_pick.blend"), desktopPath=path.join(root,"tests/golden/M16-GAP-00259/armature-shortest-path-pick-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath),{recursive:true}); const blender=process.env.BLENDER_BIN??path.join(root,"build_blender_5.2.0/bin/blender"), checker=path.join(root,"tools/web/check-action-armature-shortest-path-pick-desktop.py"), run=spawnSync(blender,["-b","--factory-startup","--python",checker,"--",fixture,desktopPath],{cwd:root,encoding:"utf8",maxBuffer:8*1024*1024}); assert.equal(run.status,0,`${run.stdout??""}\n${run.stderr??""}`); const desktop=JSON.parse(fs.readFileSync(desktopPath,"utf8")); assert.equal(desktop.saveReopen,"EXACT"); assert.equal(desktop.operatorStatus,"SKIPPED_ALREADY_APPLIED"); const desktopAfter=Object.fromEntries(desktop.after.bones.map(b=>[b.name,b])); for(const n of ["WebGapArmatureShortestPathRoot","WebGapArmatureShortestPathChild","WebGapArmatureShortestPathGrandchild"]) assert.equal(desktopAfter[n].selected,true); assert.equal(desktopAfter.WebGapArmatureShortestPathOther.selected,false); const engine=await factory({wasmBinary}), handle=engine._web_engine_create(); open(engine,handle,fs.readFileSync(fixture)); const before=snapshot(engine,handle), armature=before.armatures?.find(v=>v.name==="WebGapArmatureShortestPathArmature"); assert.ok(armature); const bones=Object.fromEntries(armature.bones.map(b=>[b.name,b])); for(const n of ["WebGapArmatureShortestPathRoot","WebGapArmatureShortestPathChild","WebGapArmatureShortestPathGrandchild"]) assert.equal(bones[n].selected,true); assert.equal(bones.WebGapArmatureShortestPathOther.selected,false); assert.equal(bones.WebGapArmatureShortestPathChild.parentId,bones.WebGapArmatureShortestPathRoot.id); assert.equal(bones.WebGapArmatureShortestPathGrandchild.parentId,bones.WebGapArmatureShortestPathChild.id); const saved=output(engine,handle,engine._web_engine_save_blend,true), reopened=engine._web_engine_create(); open(engine,reopened,saved); assert.deepEqual(snapshot(engine,reopened).armatures,before.armatures); const malformed=new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]), pointer=engine._malloc(malformed.byteLength); let result; try{engine.HEAPU8.set(malformed,pointer);result=engine._web_engine_open_blend(handle,pointer,malformed.byteLength)}finally{engine._free(pointer)} assert.notEqual(result,0); assert.deepEqual(snapshot(engine,handle).armatures,before.armatures); engine._web_engine_destroy(handle);engine._web_engine_destroy(reopened); const report={schemaVersion:1,task,operation:"ARMATURE_SHORTEST_PATH_PICK_LOCAL_EXACT",fixture:{path:path.relative(root,fixture).replaceAll(path.sep,"/"),sha256:desktop.fixtureSha256},desktop:{status:"EXACT",report:path.relative(root,desktopPath).replaceAll(path.sep,"/"),saveReopen:desktop.saveReopen,poll:desktop.poll,operatorStatus:desktop.operatorStatus,mainMutation:desktop.mainMutation,pickedBone:desktop.pickedBone,selectedChain:desktop.selectedChain,unselectedBone:desktop.unselectedBone},wasm:{status:"EXACT",armatureId:armature.id,rootBoneId:bones.WebGapArmatureShortestPathRoot.id,childBoneId:bones.WebGapArmatureShortestPathChild.id,grandchildBoneId:bones.WebGapArmatureShortestPathGrandchild.id,otherBoneId:bones.WebGapArmatureShortestPathOther.id,rootSelected:bones.WebGapArmatureShortestPathRoot.selected,childSelected:bones.WebGapArmatureShortestPathChild.selected,grandchildSelected:bones.WebGapArmatureShortestPathGrandchild.selected,otherSelected:bones.WebGapArmatureShortestPathOther.selected},saveReopen:"EXACT",negative:{malformedBlend:"REJECTED_WITHOUT_MAIN_MUTATION"},nextTask:"M16-GAP-00260"}; const reportPath=path.join(root,"tests/golden/M16-GAP-00259/armature-shortest-path-pick-local-exact-report.json"); fs.writeFileSync(reportPath,`${JSON.stringify(report,null,2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} chain=3 desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00260") { const fixture=path.join(root,"tests/files/web/generated/M16-GAP-00260-operator-armature.split.blend"),desktopPath=path.join(root,"tests/golden/M16-GAP-00260/armature-split-desktop-report.json");fs.mkdirSync(path.dirname(desktopPath),{recursive:true});const blender=process.env.BLENDER_BIN??path.join(root,"build_blender_5.2.0/bin/blender"),checker=path.join(root,"tools/web/check-action-armature-split-desktop.py"),run=spawnSync(blender,["-b","--factory-startup","--python",checker,"--",fixture,desktopPath],{cwd:root,encoding:"utf8",maxBuffer:8*1024*1024});assert.equal(run.status,0,`${run.stdout??""}\n${run.stderr??""}`);const desktop=JSON.parse(fs.readFileSync(desktopPath,"utf8"));assert.equal(desktop.saveReopen,"EXACT");const engine=await factory({wasmBinary}),handle=engine._web_engine_create();open(engine,handle,fs.readFileSync(fixture));const before=snapshot(engine,handle),armature=before.armatures?.find(v=>v.name==="WebGapArmatureSplitArmature");assert.ok(armature);const by=Object.fromEntries(armature.bones.map(b=>[b.name,b]));assert.equal(by.WebGapArmatureSplitChild.connected,false);assert.equal(by.WebGapArmatureSplitChild.parentId,null);assert.equal(by.WebGapArmatureSplitRoot.selected,true);assert.equal(by.WebGapArmatureSplitOther.selected,false);const saved=output(engine,handle,engine._web_engine_save_blend,true),reopened=engine._web_engine_create();open(engine,reopened,saved);assert.deepEqual(snapshot(engine,reopened).armatures,before.armatures);const malformed=new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]),pointer=engine._malloc(malformed.byteLength);let result;try{engine.HEAPU8.set(malformed,pointer);result=engine._web_engine_open_blend(handle,pointer,malformed.byteLength)}finally{engine._free(pointer)}assert.notEqual(result,0);assert.deepEqual(snapshot(engine,handle).armatures,before.armatures);engine._web_engine_destroy(handle);engine._web_engine_destroy(reopened);const report={schemaVersion:1,task,operation:"ARMATURE_SPLIT_LOCAL_EXACT",fixture:{path:path.relative(root,fixture).replaceAll(path.sep,"/"),sha256:desktop.fixtureSha256},desktop:{status:"EXACT",report:path.relative(root,desktopPath).replaceAll(path.sep,"/"),saveReopen:desktop.saveReopen,poll:desktop.poll,operatorStatus:desktop.operatorStatus,mainMutation:desktop.mainMutation},wasm:{status:"EXACT",armatureId:armature.id,rootBoneId:by.WebGapArmatureSplitRoot.id,childBoneId:by.WebGapArmatureSplitChild.id,otherBoneId:by.WebGapArmatureSplitOther.id,childConnected:by.WebGapArmatureSplitChild.connected,childParentId:by.WebGapArmatureSplitChild.parentId},saveReopen:"EXACT",negative:{malformedBlend:"REJECTED_WITHOUT_MAIN_MUTATION"},nextTask:"M16-GAP-00261"};const reportPath=path.join(root,"tests/golden/M16-GAP-00260/armature-split-local-exact-report.json");fs.writeFileSync(reportPath,`${JSON.stringify(report,null,2)}\n`);process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} disconnected=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`);process.exit(0); } if (task === "M16-GAP-00261") { const fixture=path.join(root,"tests/files/web/generated/M16-GAP-00261-operator-armature.subdivide.blend"),desktopPath=path.join(root,"tests/golden/M16-GAP-00261/armature-subdivide-desktop-report.json");fs.mkdirSync(path.dirname(desktopPath),{recursive:true});const blender=process.env.BLENDER_BIN??path.join(root,"build_blender_5.2.0/bin/blender"),checker=path.join(root,"tools/web/check-action-armature-subdivide-desktop.py"),run=spawnSync(blender,["-b","--factory-startup","--python",checker,"--",fixture,desktopPath],{cwd:root,encoding:"utf8",maxBuffer:8*1024*1024});assert.equal(run.status,0,`${run.stdout??""}\n${run.stderr??""}`);const desktop=JSON.parse(fs.readFileSync(desktopPath,"utf8"));assert.equal(desktop.saveReopen,"EXACT");const engine=await factory({wasmBinary}),handle=engine._web_engine_create();open(engine,handle,fs.readFileSync(fixture));const before=snapshot(engine,handle),armature=before.armatures?.find(v=>v.name==="WebGapArmatureSubdivideArmature");assert.ok(armature);assert.equal(armature.bones.length,3);const pieces=armature.bones.filter(b=>b.name!=="WebGapArmatureSubdivideOther");const other=armature.bones.find(b=>b.name==="WebGapArmatureSubdivideOther");assert.equal(pieces.length,2);assert.ok(other);assert.equal(other.selected,false);assert.deepEqual(new Set(pieces.flatMap(b=>[b.head,b.tail].map(v=>v.join(",")))),new Set(["0,0,0","0,0.5,0","0,1,0"]));const saved=output(engine,handle,engine._web_engine_save_blend,true),reopened=engine._web_engine_create();open(engine,reopened,saved);assert.deepEqual(snapshot(engine,reopened).armatures,before.armatures);const malformed=new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]),pointer=engine._malloc(malformed.byteLength);let result;try{engine.HEAPU8.set(malformed,pointer);result=engine._web_engine_open_blend(handle,pointer,malformed.byteLength)}finally{engine._free(pointer)}assert.notEqual(result,0);assert.deepEqual(snapshot(engine,handle).armatures,before.armatures);engine._web_engine_destroy(handle);engine._web_engine_destroy(reopened);const report={schemaVersion:1,task,operation:"ARMATURE_SUBDIVIDE_LOCAL_EXACT",fixture:{path:path.relative(root,fixture).replaceAll(path.sep,"/"),sha256:desktop.fixtureSha256},desktop:{status:"EXACT",report:path.relative(root,desktopPath).replaceAll(path.sep,"/"),saveReopen:desktop.saveReopen,poll:desktop.poll,operatorStatus:desktop.operatorStatus,mainMutation:desktop.mainMutation,numberCuts:desktop.numberCuts,sourceBone:desktop.sourceBone,otherBone:desktop.otherBone},wasm:{status:"EXACT",armatureId:armature.id,pieceIds:pieces.map(b=>b.id),otherBoneId:other.id,pieceCount:pieces.length,otherSelected:other.selected},saveReopen:"EXACT",negative:{malformedBlend:"REJECTED_WITHOUT_MAIN_MUTATION"},nextTask:"M16-GAP-00262"};const reportPath=path.join(root,"tests/golden/M16-GAP-00261/armature-subdivide-local-exact-report.json");fs.writeFileSync(reportPath,`${JSON.stringify(report,null,2)}\n`);process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} cuts=1 pieces=2 desktop=exact saveReopen=exact next=${report.nextTask}\n`);process.exit(0); } if (task === "M16-GAP-00262") { const fixture=path.join(root,"tests/files/web/generated/M16-GAP-00262-operator-armature.switch_direction.blend"),desktopPath=path.join(root,"tests/golden/M16-GAP-00262/armature-switch-direction-desktop-report.json");fs.mkdirSync(path.dirname(desktopPath),{recursive:true});const blender=process.env.BLENDER_BIN??path.join(root,"build_blender_5.2.0/bin/blender"),checker=path.join(root,"tools/web/check-action-armature-switch-direction-desktop.py"),run=spawnSync(blender,["-b","--factory-startup","--python",checker,"--",fixture,desktopPath],{cwd:root,encoding:"utf8",maxBuffer:8*1024*1024});assert.equal(run.status,0,`${run.stdout??""}\n${run.stderr??""}`);const desktop=JSON.parse(fs.readFileSync(desktopPath,"utf8"));assert.equal(desktop.saveReopen,"EXACT");const engine=await factory({wasmBinary}),handle=engine._web_engine_create();open(engine,handle,fs.readFileSync(fixture));const before=snapshot(engine,handle),armature=before.armatures?.find(v=>v.name==="WebGapArmatureSwitchDirectionArmature");assert.ok(armature);const by=Object.fromEntries(armature.bones.map(b=>[b.name,b]));assert.deepEqual(by.WebGapArmatureSwitchDirectionRoot.head,[0,1,0]);assert.deepEqual(by.WebGapArmatureSwitchDirectionRoot.tail,[0,0,0]);assert.equal(by.WebGapArmatureSwitchDirectionRoot.parentId,by.WebGapArmatureSwitchDirectionChild.id);assert.deepEqual(by.WebGapArmatureSwitchDirectionChild.head,[0,2,0]);assert.deepEqual(by.WebGapArmatureSwitchDirectionChild.tail,[0,1,0]);assert.equal(by.WebGapArmatureSwitchDirectionOther.selected,false);const saved=output(engine,handle,engine._web_engine_save_blend,true),reopened=engine._web_engine_create();open(engine,reopened,saved);assert.deepEqual(snapshot(engine,reopened).armatures,before.armatures);const malformed=new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]),pointer=engine._malloc(malformed.byteLength);let result;try{engine.HEAPU8.set(malformed,pointer);result=engine._web_engine_open_blend(handle,pointer,malformed.byteLength)}finally{engine._free(pointer)}assert.notEqual(result,0);assert.deepEqual(snapshot(engine,handle).armatures,before.armatures);engine._web_engine_destroy(handle);engine._web_engine_destroy(reopened);const report={schemaVersion:1,task,operation:"ARMATURE_SWITCH_DIRECTION_LOCAL_EXACT",fixture:{path:path.relative(root,fixture).replaceAll(path.sep,"/"),sha256:desktop.fixtureSha256},desktop:{status:"EXACT",report:path.relative(root,desktopPath).replaceAll(path.sep,"/"),saveReopen:desktop.saveReopen,poll:desktop.poll,operatorStatus:desktop.operatorStatus,mainMutation:desktop.mainMutation},wasm:{status:"EXACT",armatureId:armature.id,rootBoneId:by.WebGapArmatureSwitchDirectionRoot.id,childBoneId:by.WebGapArmatureSwitchDirectionChild.id,otherBoneId:by.WebGapArmatureSwitchDirectionOther.id,rootParentId:by.WebGapArmatureSwitchDirectionRoot.parentId,rootHead:by.WebGapArmatureSwitchDirectionRoot.head,rootTail:by.WebGapArmatureSwitchDirectionRoot.tail,childHead:by.WebGapArmatureSwitchDirectionChild.head,childTail:by.WebGapArmatureSwitchDirectionChild.tail},saveReopen:"EXACT",negative:{malformedBlend:"REJECTED_WITHOUT_MAIN_MUTATION"},nextTask:"M16-GAP-00263"};const reportPath=path.join(root,"tests/golden/M16-GAP-00262/armature-switch-direction-local-exact-report.json");fs.writeFileSync(reportPath,`${JSON.stringify(report,null,2)}\n`);process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} reversed=1 desktop=exact saveReopen=exact next=${report.nextTask}\n`);process.exit(0); } if (task === "M16-GAP-00263") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00263-operator-armature.symmetrize.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00263/armature-symmetrize-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-armature-symmetrize-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.ok(["FINISHED", "SKIPPED_ALREADY_APPLIED"].includes(desktop.operatorStatus)); assert.ok(["MIRRORED_SELECTED_BONE", "MIRRORED_BONE_ALREADY_PRESENT"].includes(desktop.mainMutation)); assert.equal(desktop.direction, "NEGATIVE_X"); const desktopByName = Object.fromEntries(desktop.after.bones.map((bone) => [bone.name, bone])); assert.deepEqual(desktopByName["WebGapArmatureSymmetrizeSource.L"].head, [1, 0, 0]); assert.deepEqual(desktopByName["WebGapArmatureSymmetrizeSource.L"].tail, [1, 1, 0]); assert.deepEqual(desktopByName["WebGapArmatureSymmetrizeSource.R"].head, [-1, 0, 0]); assert.deepEqual(desktopByName["WebGapArmatureSymmetrizeSource.R"].tail, [-1, 1, 0]); assert.equal(desktopByName.WebGapArmatureSymmetrizeOther.selected, false); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const armature = before.armatures?.find((value) => value.name === "WebGapArmatureSymmetrizeArmature"); assert.ok(armature); assert.equal(armature.bones.length, 3); const byName = Object.fromEntries(armature.bones.map((bone) => [bone.name, bone])); const source = byName["WebGapArmatureSymmetrizeSource.L"]; const mirrored = byName["WebGapArmatureSymmetrizeSource.R"]; const other = byName.WebGapArmatureSymmetrizeOther; assert.ok(source && mirrored && other); assert.equal(source.selected, false); assert.equal(mirrored.selected, true); assert.equal(other.selected, false); assert.equal(source.parentId, null); assert.equal(mirrored.parentId, null); assert.deepEqual(source.head, desktopByName["WebGapArmatureSymmetrizeSource.L"].head); assert.deepEqual(source.tail, desktopByName["WebGapArmatureSymmetrizeSource.L"].tail); assert.deepEqual(mirrored.head, desktopByName["WebGapArmatureSymmetrizeSource.R"].head); assert.deepEqual(mirrored.tail, desktopByName["WebGapArmatureSymmetrizeSource.R"].tail); assert.deepEqual(other.head, desktopByName.WebGapArmatureSymmetrizeOther.head); assert.deepEqual(other.tail, desktopByName.WebGapArmatureSymmetrizeOther.tail); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).armatures, before.armatures); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).armatures, before.armatures); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ARMATURE_SYMMETRIZE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation, direction: desktop.direction, sourceBone: desktop.sourceBone, mirroredBone: desktop.mirroredBone, otherBone: desktop.otherBone }, wasm: { status: "EXACT", armatureId: armature.id, sourceBoneId: source.id, mirroredBoneId: mirrored.id, otherBoneId: other.id, sourceSelected: source.selected, mirroredSelected: mirrored.selected, otherSelected: other.selected, sourceHead: source.head, sourceTail: source.tail, mirroredHead: mirrored.head, mirroredTail: mirrored.tail }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00264" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00263/armature-symmetrize-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} armature=${armature.id} mirrored=1 direction=negative_x desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00264") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00264-operator-asset.asset_download.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00264/asset-download-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-asset-download-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const image = before.images?.find((value) => value.id === "image:WebGapAssetDownloadImage"); assert.ok(image); assert.equal(image.name, "WebGapAssetDownloadImage"); assert.equal(image.assetId, image.id); assert.equal(image.sourcePath, "//assets/WebGapAssetDownload.png"); assert.equal(image.assetStatus, "EXTERNAL"); assert.equal(image.packed, false); assert.deepEqual([image.width, image.height], [1, 1]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened).images?.find((value) => value.id === image.id); assert.deepEqual(after, image); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).images?.find((value) => value.id === image.id), image); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_ASSET_DOWNLOAD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", imageId: image.id, sourcePath: image.sourcePath, assetStatus: image.assetStatus, packed: image.packed, dimensions: [image.width, image.height] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00265" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00264/asset-download-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} image=${image.id} assetStatus=${image.assetStatus} operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00265") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00265-operator-asset.assets_download.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00265/assets-download-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-assets-download-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const image = before.images?.find((value) => value.id === "image:WebGapAssetsDownloadImage"); assert.ok(image); assert.equal(image.name, "WebGapAssetsDownloadImage"); assert.equal(image.assetId, image.id); assert.equal(image.sourcePath, "//assets/WebGapAssetsDownload.png"); assert.equal(image.assetStatus, "EXTERNAL"); assert.equal(image.packed, false); assert.deepEqual([image.width, image.height], [1, 1]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened).images?.find((value) => value.id === image.id); assert.deepEqual(after, image); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).images?.find((value) => value.id === image.id), image); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_ASSETS_DOWNLOAD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", imageId: image.id, sourcePath: image.sourcePath, assetStatus: image.assetStatus, packed: image.packed, dimensions: [image.width, image.height] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00266" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00265/assets-download-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} image=${image.id} assetStatus=${image.assetStatus} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00266") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00266-operator-asset.assign_action.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00266/assign-action-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-assign-action-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animation = before.animations?.find((value) => value.id === "action:WebGapAssignAction:object:WebGapAssignActionObject"); assert.ok(animation); assert.equal(animation.channels.length, desktop.before.channels.length); for (const [index, channel] of animation.channels.entries()) { const expected = desktop.before.channels[index]; assert.equal(channel.path.replace(/\[\d+\]$/, ""), expected.path); assert.deepEqual(channel.keyframes.map((value) => value.frame), expected.frames); } const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_ASSIGN_ACTION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", actionId: animation.id, targetId: animation.targetId, channelPaths: animation.channels.map((channel) => channel.path), keyframesPerChannel: animation.channels.map((channel) => channel.keyframes.length) }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00267" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00266/assign-action-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} channels=${animation.channels.length} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00267") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00267-operator-asset.browse_containing_blend_file.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00267/browse-containing-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-browse-containing-blend-file-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.ok(Array.isArray(before.libraries)); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapBrowseContaining.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_BROWSE_CONTAINING_BLEND_FILE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00268" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00267/browse-containing-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00268") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00268-operator-asset.bundle_install.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00268/bundle-install-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-bundle-install-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.ok(Array.isArray(before.libraries)); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapBundleInstall.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_BUNDLE_INSTALL_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00269" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00268/bundle-install-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00269") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00269-operator-asset.catalog_delete.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00269/catalog-delete-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalog-delete-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.ok(Array.isArray(before.libraries)); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogDelete.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOG_DELETE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00270" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00269/catalog-delete-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00270") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00270-operator-asset.catalog_new.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00270/catalog-new-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalog-new-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.ok(Array.isArray(before.libraries)); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogNew.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOG_NEW_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00271" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00270/catalog-new-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00271") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00271-operator-asset.catalog_redo.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00271/catalog-redo-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalog-redo-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogRedo.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOG_REDO_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00272" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00271/catalog-redo-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00272") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00272-operator-asset.catalog_undo.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00272/catalog-undo-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalog-undo-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogUndo.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOG_UNDO_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00273" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00272/catalog-undo-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00273") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00273-operator-asset.catalog_undo_push.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00273/catalog-undo-push-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalog-undo-push-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogUndoPush.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOG_UNDO_PUSH_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00274" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00273/catalog-undo-push-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00274") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00274-operator-asset.catalogs_save.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00274/catalogs-save-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-catalogs-save-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapCatalogsSave.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42,0x4c,0x45,0x4e,0x44,0x45,0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CATALOGS_SAVE_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00275" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00274/catalogs-save-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00275") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00275-operator-asset.clear.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00275/asset-clear-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-asset-clear-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.poll, false); assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.mainMutation, "NONE"); assert.deepEqual(desktop.after, desktop.before); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.equal(before.libraryStatus, "AVAILABLE"); assert.equal(before.libraries.length, 1); const library = before.libraries[0]; assert.equal(library.id, `library:${desktop.before.name}`); assert.equal(library.name, desktop.before.name); assert.equal(library.sourcePath, "//WebGapAssetClear.blend"); assert.equal(library.packed, false); assert.equal(library.status, "EXTERNAL_REQUIRED"); assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED"); assert.deepEqual(library.dependencyIds, []); assert.equal(library.readOnly, true); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.libraries, before.libraries); assert.equal(after.libraryStatus, before.libraryStatus); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).libraries, before.libraries); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ASSET_CLEAR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", libraryId: library.id, sourcePath: library.sourcePath, libraryStatus: library.status, dependencyIds: library.dependencyIds }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00276" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00275/asset-clear-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} library=${library.id} status=${library.status} poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00205") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00205-operator-anim.separate_slots.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00205/anim-separate-slots-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-separate-slots-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "SLOTS_SEPARATED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(Object.values(desktop.after.objects).map((value) => value.name).sort(), ["WebGapSeparateSlotAAction", "WebGapSeparateSlotBAction"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const animations = before.animations?.filter((value) => value.targetId && value.targetId !== "unlinked"); assert.equal(animations?.length, 2); assert.deepEqual(animations.map((value) => value.name).sort(), ["WebGapSeparateSlotAAction", "WebGapSeparateSlotBAction"]); for (const animation of animations) { assert.equal(animation.channels.length, 1); assert.equal(animation.channels[0].path, '["separate_target"][0]'); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.frame), [1, 3, 5]); } assert.equal(before.animations?.some((value) => value.name === "WebGapAnimSeparateSlotsAction"), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SEPARATE_SLOTS_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", actionIds: animations.map((value) => value.id), actionNames: animations.map((value) => value.name).sort(), channelsPerAction: animations.map((value) => value.channels.length), oldActionEmpty: true }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00206" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00205/anim-separate-slots-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} actions=2 old-empty=true desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00204") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00204-operator-anim.scene_range_frame.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00204/anim-scene-range-frame-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-scene-range-frame-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "VIEW_FRAMED_TO_SCENE_RANGE"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); const sameView = (left, right) => left?.cur?.xmin === right?.cur?.xmin && left?.cur?.xmax === right?.cur?.xmax && left?.cur?.ymin === right?.cur?.ymin && left?.cur?.ymax === right?.cur?.ymax && left?.mask?.xmin === right?.mask?.xmin && left?.mask?.xmax === right?.mask?.xmax && left?.mask?.ymin === right?.mask?.ymin && left?.mask?.ymax === right?.mask?.ymax; const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const actionRegion = before.editorWorkflow.workspaces.flatMap((workspace) => workspace.areas) .filter((area) => area.editor === "DOPE_SHEET") .flatMap((area) => area.regions) .find((region) => region.kind === "MAIN" && sameView(region.view2d, desktop.view.view2d)); assert.ok(actionRegion?.view2d); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).editorWorkflow, before.editorWorkflow); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).editorWorkflow, before.editorWorkflow); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_SCENE_RANGE_FRAME_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", editor: "DOPE_SHEET", view2d: actionRegion.view2d }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00205" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00204/anim-scene-range-frame-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} editor=DOPE_SHEET view2d=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00203") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00203-operator-anim.replace_action_new.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00203/anim-replace-action-new-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-replace-action-new-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ACTION_REPLACED_WITH_NEW"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.newAction, "Action"); assert.deepEqual(Object.values(desktop.after.objects).map((value) => value.name), ["Action", "Action"]); assert.deepEqual(Object.values(desktop.after.objects).map((value) => value.channels.length), [0, 0]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const oldAnimation = before.animations?.find((value) => value.name === "WebGapAnimReplaceNewOldAction" && value.unlinked); assert.ok(oldAnimation); assert.equal(oldAnimation.channels.length, 2); assert.deepEqual(oldAnimation.channels.map((channel) => channel.keyframes.map((value) => value.value[0])), [[1, 3, 5], [2, 4, 6]]); assert.equal(before.animations?.some((value) => value.name === desktop.newAction), false); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_REPLACE_ACTION_NEW_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", replacedObjectIds: Object.keys(desktop.after.objects).map((name) => `object:${name}`), newAction: desktop.newAction, newActionChannels: 0, oldActionUnlinked: oldAnimation.id, oldActionChannels: oldAnimation.channels.length, emptyActionVisibility: "NOT_EXPOSED_BY_SCENE_SNAPSHOT" }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00204" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00203/anim-replace-action-new-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} actions=old->new-empty users=2 desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00202") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00202-operator-anim.replace_action.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00202/anim-replace-action-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-replace-action-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "ACTIONS_REPLACED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.equal(desktop.before.objects.WebGapAnimReplaceOldA.name, "WebGapAnimReplaceOldAction"); assert.equal(desktop.before.objects.WebGapAnimReplaceOldB.name, "WebGapAnimReplaceOldAction"); assert.deepEqual(Object.values(desktop.after.objects).map((value) => value.name), ["WebGapAnimReplaceNewAction", "WebGapAnimReplaceNewAction", "WebGapAnimReplaceNewAction"]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const newAnimations = before.animations?.filter((value) => value.name === "WebGapAnimReplaceNewAction" && value.targetId !== "unlinked"); const oldAnimation = before.animations?.find((value) => value.name === "WebGapAnimReplaceOldAction" && value.unlinked); assert.equal(newAnimations?.length, 3); assert.ok(oldAnimation); for (const animation of newAnimations) { assert.equal(animation.channels.length, 1); assert.equal(animation.channels[0].path, '["replace_target"][0]'); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.frame), [1, 3, 5]); assert.deepEqual(animation.channels[0].keyframes.map((value) => value.value[0]), [10, 30, 50]); } assert.deepEqual(oldAnimation.channels.map((channel) => channel.keyframes.map((value) => value.value[0])), [[1, 3, 5], [2, 4, 6]]); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).animations, before.animations); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).animations, before.animations); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_REPLACE_ACTION_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", replacedObjectIds: newAnimations.map((value) => value.targetId), newAction: "WebGapAnimReplaceNewAction", oldActionUnlinked: oldAnimation.id, channelsPerObject: newAnimations.map((value) => value.channels.length), oldActionChannels: oldAnimation.channels.length }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00203" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00202/anim-replace-action-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} actions=old->new users=3 desktop=exact poll=true operator=${desktop.operatorStatus} saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00201") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00201-operator-anim.previewrange_set.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00201/anim-previewrange-set-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-previewrange-set-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "PREVIEW_RANGE_SET"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.after, { use: true, start: 2, end: 6 }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const scene = before.scenes?.find((value) => value.id === before.sceneId) ?? before.scenes?.[0]; assert.ok(scene); assert.deepEqual(scene.previewRange, { start: 2, end: 6 }); assert.deepEqual(scene.sequencerTimeline, { fpsDenominator: 1000, fpsNumerator: 24000, frameEnd: 8, frameStart: 1, id: "sequencer:scene:Scene", revision: 1, schemaVersion: 1, strips: [] }); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const reopenedScene = snapshot(engine, reopened).scenes?.find((value) => value.id === scene.id); assert.deepEqual(reopenedScene, scene); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).scenes?.find((value) => value.id === scene.id), scene); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_PREVIEWRANGE_SET_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", sceneId: scene.id, previewRange: scene.previewRange, frameStart: scene.sequencerTimeline.frameStart, frameEnd: scene.sequencerTimeline.frameEnd }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00202" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00201/anim-previewrange-set-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} scene=${scene.id} preview=2-6 poll=true operator=${desktop.operatorStatus} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00200") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00200-operator-anim.previewrange_clear.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00200/anim-previewrange-clear-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-previewrange-clear-desktop.py"); const run = spawnSync(blender, ["-b", "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "PREVIEW_RANGE_CLEARED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.after, { use: false, start: 0, end: 0 }); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const scene = before.scenes?.find((value) => value.id === before.sceneId) ?? before.scenes?.[0]; assert.ok(scene); assert.equal(Object.hasOwn(scene, "previewRange"), false); assert.deepEqual(scene.sequencerTimeline, { fpsDenominator: 1000, fpsNumerator: 24000, frameEnd: 8, frameStart: 1, id: "sequencer:scene:Scene", revision: 1, schemaVersion: 1, strips: [] }); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const reopenedScene = snapshot(engine, reopened).scenes?.find((value) => value.id === scene.id); assert.deepEqual(reopenedScene, scene); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).scenes?.find((value) => value.id === scene.id), scene); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_PREVIEWRANGE_CLEAR_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", sceneId: scene.id, previewRange: null, frameStart: scene.sequencerTimeline.frameStart, frameEnd: scene.sequencerTimeline.frameEnd }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00201" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00200/anim-previewrange-clear-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} scene=${scene.id} preview=cleared poll=true operator=${desktop.operatorStatus} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00199") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00199-operator-anim.paste_driver_button.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00199/anim-paste-driver-button-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const checker = path.join(root, "tools/web/check-action-paste-driver-button-desktop.py"); const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.mainMutation, "DRIVER_PASTED"); assert.equal(desktop.poll, true); assert.equal(desktop.operatorStatus, "FINISHED"); assert.deepEqual(desktop.after?.source?.drivers ?? desktop.source?.drivers, [{ path: '["source_target"]', index: 0, expression: "frame * 3.0 + 2.0", type: "SCRIPTED", variableCount: 0 }]); assert.deepEqual(desktop.after?.target?.drivers ?? desktop.target?.drivers, [{ path: '["paste_target"]', index: 0, expression: "frame * 3.0 + 2.0", type: "SCRIPTED", variableCount: 0 }]); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimPasteDriverButtonObject"); assert.ok(node); const expectedDrivers = [ { path: '["source_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "frame * 3.0 + 2.0", type: "SCRIPTED", typeCode: 1, flags: 8, influence: 0, variables: [] }, { path: '["paste_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "frame * 3.0 + 2.0", type: "SCRIPTED", typeCode: 1, flags: 8, influence: 0, variables: [] }, ]; assert.deepEqual(node.drivers, expectedDrivers); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).nodes?.find((value) => value.id === node.id)?.drivers, node.drivers); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id)?.drivers, node.drivers); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task, operation: "ANIM_PASTE_DRIVER_BUTTON_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00200" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00199/anim-paste-driver-button-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=${node.drivers.length} poll=true operator=${desktop.operatorStatus} mainMutation=driver_pasted desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task === "M16-GAP-00027") { const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00027-datablock-WindowManager.blend"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00027/window-manager-desktop-report.json"); fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-window-manager-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, fs.readFileSync(fixture)); const before = snapshot(engine, handle); assert.deepEqual(before.windowManager, desktop.windowManager); const saved = output(engine, handle, engine._web_engine_save_blend, true); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assert.deepEqual(snapshot(engine, reopened).windowManager, before.windowManager); const malformed = new Uint8Array([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]); const pointer = engine._malloc(malformed.byteLength); let result; try { engine.HEAPU8.set(malformed, pointer); result = engine._web_engine_open_blend(handle, pointer, malformed.byteLength); } finally { engine._free(pointer); } assert.notEqual(result, 0); assert.deepEqual(snapshot(engine, handle), before); engine._web_engine_destroy(handle); engine._web_engine_destroy(reopened); const report = { schemaVersion: 1, task: "M16-GAP-00027", operation: "WINDOW_MANAGER_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen }, wasm: { status: "EXACT", windowManager: before.windowManager }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00028" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00027/window-manager-local-exact-report.json"); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} windowManager=${before.windowManager.id} desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.exit(0); } if (task !== "M16-GAP-00001") { process.stdout.write(`generated-gap-blocked task=${task} reason=IMPLEMENTATION_NOT_STARTED next=${task}\n`); process.exit(0); } const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00001-datablock-Action.blend"); assert.equal(fs.existsSync(fixture), true, "Action fixture is required"); const bytes = fs.readFileSync(fixture); const desktopReportPath = path.join(root, "tests/golden/M16-GAP-00001/action-desktop-report.json"); const desktop = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-desktop.py"), "--", fixture, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); assert.equal(desktop.status, 0, `${desktop.stdout ?? ""}\n${desktop.stderr ?? ""}`); const desktopEvidence = JSON.parse(fs.readFileSync(desktopReportPath, "utf8")); assert.equal(desktopEvidence.saveReopen, "EXACT"); assert.equal(desktopEvidence.actions.length, 1); assert.equal(desktopEvidence.actions[0].name, "AnimatedObjectAction"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); open(engine, handle, bytes); const before = snapshot(engine, handle); const animation = before.animations.find((item) => item.targetId === "object:AnimatedObject"); assert.ok(animation); assert.equal(animation.id, "action:AnimatedObjectAction:object:AnimatedObject"); assert.deepEqual(animation.channels.map((channel) => channel.path), ["location[0]", "location[1]", "location[2]"]); assert.ok(animation.channels.every((channel) => channel.keyframes.length === 2)); const saved = output(engine, handle, engine._web_engine_save_blend, true); assert.ok(saved.byteLength > 0); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const after = snapshot(engine, reopened); assert.deepEqual(after.animations, before.animations); const report = { schemaVersion: 1, task: "M16-GAP-00001", operation: "ACTION_DATABLOCK_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktopEvidence.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopReportPath).replaceAll(path.sep, "/"), saveReopen: desktopEvidence.saveReopen }, wasm: { status: "EXACT", actionId: animation.id, channelPaths: animation.channels.map((channel) => channel.path), keyframesPerChannel: animation.channels.map((channel) => channel.keyframes.length) }, saveReopen: "EXACT", nextTask: "M16-GAP-00002" }; const reportPath = path.join(root, "tests/golden/M16-GAP-00001/action-local-exact-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`generated-gap-ok task=${task} action=${animation.id} channels=${animation.channels.length} desktop=exact saveReopen=exact next=${report.nextTask}\n`);