import fs from "node:fs"; import factory from "../../web/app/src/vendor/blender/web_engine.js"; const wasmBinary = fs.readFileSync(new URL("../../web/app/src/vendor/blender/web_engine.wasm", import.meta.url)); const emptyBlend = fs.readFileSync(new URL("../../tests/files/web/empty.blend", import.meta.url)); const attributeBlend = fs.readFileSync(new URL("../../tests/files/web/attribute_scene.blend", import.meta.url)); const animationBlend = fs.readFileSync(new URL("../../tests/files/web/animation_scene.blend", import.meta.url)); const riggedShapeBlend = fs.readFileSync(new URL("../../tests/files/web/rigged_shape_scene.blend", import.meta.url)); const blend = fs.readFileSync(new URL("../../tests/files/web/basic_scene.blend", import.meta.url)); const invalidBlend = Uint8Array.from([0x42, 0x41, 0x44, 0x00]); const engine = await factory({ wasmBinary }); function openFixture(engineHandle, bytes) { const pointer = engine._malloc(bytes.byteLength); engine.HEAPU8.set(bytes, pointer); const result = engine._web_engine_open_blend(engineHandle, pointer, bytes.byteLength); engine._free(pointer); return result; } function readHandleSnapshot(engineHandle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); const result = engine._web_engine_get_scene_snapshot(engineHandle, dataOut, lengthOut); if (result !== 0) throw new Error("mode smoke snapshot request failed"); const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; const snapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length))); engine._free(dataOut); engine._free(lengthOut); return snapshot; } function readHandleDepsgraph(engineHandle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); const result = engine._web_engine_evaluate_depsgraph(engineHandle, dataOut, lengthOut); if (result !== 0) { const error = engine.UTF8ToString(engine._web_engine_last_error_message()); engine._free(dataOut); engine._free(lengthOut); return { unavailable: true, code: result, error }; } const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; const report = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length))); engine._free(dataOut); engine._free(lengthOut); return report; } function applyHandleCommand(engineHandle, command) { const bytes = new TextEncoder().encode(JSON.stringify(command)); const pointer = engine._malloc(bytes.byteLength); engine.HEAPU8.set(bytes, pointer); const result = engine._web_engine_apply_command(engineHandle, pointer, bytes.byteLength); engine._free(pointer); return result; } function saveHandleBlend(engineHandle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); const result = engine._web_engine_save_blend(engineHandle, dataOut, lengthOut); if (result !== 0) { const message = engine.UTF8ToString(engine._web_engine_last_error_message()); engine._free(dataOut); engine._free(lengthOut); throw new Error(`blend save failed: ${message}`); } const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; const bytes = engine.HEAPU8.slice(pointer, pointer + length); engine._web_engine_free_buffer(pointer); engine._free(dataOut); engine._free(lengthOut); return bytes; } const handle = engine._web_engine_create(); if (handle !== 1 || engine._web_engine_get_live_handles() !== 1) { throw new Error(`unexpected engine state: handle=${handle}`); } const invalidInput = engine._malloc(invalidBlend.byteLength); engine.HEAPU8.set(invalidBlend, invalidInput); if (engine._web_engine_open_blend(handle, invalidInput, invalidBlend.byteLength) !== -4) { throw new Error("invalid blend input should return BLEND_READ_FAILED"); } engine._free(invalidInput); const emptyInput = engine._malloc(emptyBlend.byteLength); engine.HEAPU8.set(emptyBlend, emptyInput); if (engine._web_engine_open_blend(handle, emptyInput, emptyBlend.byteLength) !== 0) { throw new Error(`empty.blend rejected: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(emptyInput); const attributeInput = engine._malloc(attributeBlend.byteLength); engine.HEAPU8.set(attributeBlend, attributeInput); if (engine._web_engine_open_blend(handle, attributeInput, attributeBlend.byteLength) !== 0) { throw new Error(`attribute_scene.blend rejected: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(attributeInput); const attributeDataOut = engine._malloc(4); const attributeLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_snapshot(handle, attributeDataOut, attributeLengthOut) !== 0) { throw new Error("attribute snapshot request failed"); } const attributePointer = engine.HEAPU32[attributeDataOut >>> 2]; const attributeLength = engine.HEAPU32[attributeLengthOut >>> 2]; const attributeSnapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(attributePointer, attributePointer + attributeLength))); const attributeMesh = attributeSnapshot.meshes[0]; const attributeMaterial = attributeSnapshot.materials.find((material) => material.id === "material:AttributeRed"); const attributeImage = attributeSnapshot.images.find((image) => image.id === "image:AttributeTexture"); if (attributeMesh.geometryStatus !== "available" || attributeMesh.faceCount !== 2 || attributeMesh.triangleCount !== 3 || attributeMesh.normals?.length !== 15 || attributeMesh.uvs?.length !== 14 || attributeMesh.colors?.length !== 28 || attributeMesh.triangleCornerIndices?.length !== 9 || attributeMesh.triangleMaterialIndices?.join(",") !== "0,0,1" || attributeMesh.materialSlotIds?.join(",") !== "material:AttributeRed,material:AttributeBlue" || attributeMesh.bounds?.min?.join(",") !== "-1,-1,0" || attributeMesh.bounds?.max?.join(",") !== "1,1,1" || !attributeMaterial || Math.abs(attributeMaterial.metallic - 0.25) > 1e-6 || Math.abs(attributeMaterial.roughness - 0.35) > 1e-6 || Math.abs(attributeMaterial.alpha - 0.8) > 1e-6 || Math.abs(attributeMaterial.ior - 1.33) > 1e-6 || attributeMaterial.imageIds?.join(",") !== "image:AttributeTexture" || !attributeMaterial.warnings?.includes("linked_input_not_evaluated:Base Color") || attributeImage?.mimeType !== "image/png" || !attributeImage.sourcePath?.endsWith("textures/attribute.png")) { throw new Error(`unexpected attribute SceneIR: ${JSON.stringify(attributeMesh)}`); } engine._free(attributeDataOut); engine._free(attributeLengthOut); const animationInput = engine._malloc(animationBlend.byteLength); engine.HEAPU8.set(animationBlend, animationInput); if (engine._web_engine_open_blend(handle, animationInput, animationBlend.byteLength) !== 0) { throw new Error(`animation_scene.blend rejected: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(animationInput); const animationDataOut = engine._malloc(4); const animationLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_snapshot(handle, animationDataOut, animationLengthOut) !== 0) { throw new Error("animation snapshot request failed"); } const animationPointer = engine.HEAPU32[animationDataOut >>> 2]; const animationLength = engine.HEAPU32[animationLengthOut >>> 2]; const animationSnapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(animationPointer, animationPointer + animationLength))); const animation = animationSnapshot.animations.find((item) => item.targetId === "object:AnimatedObject"); if (animationSnapshot.frame.start !== 1 || animationSnapshot.frame.end !== 10 || !animation || animation.frameStart !== 1 || animation.frameEnd !== 10 || animation.channels.length !== 3 || animation.channels.some((channel) => channel.keyframes.length !== 2) || animation.channels.find((channel) => channel.path === "location[2]")?.keyframes[1]?.value?.[0] !== 4) { throw new Error(`unexpected animation SceneIR: ${JSON.stringify(animationSnapshot.animations)}`); } engine._free(animationDataOut); engine._free(animationLengthOut); const riggedInput = engine._malloc(riggedShapeBlend.byteLength); engine.HEAPU8.set(riggedShapeBlend, riggedInput); if (engine._web_engine_open_blend(handle, riggedInput, riggedShapeBlend.byteLength) !== 0) { throw new Error(`rigged_shape_scene.blend rejected: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(riggedInput); const riggedSnapshot = readHandleSnapshot(handle); const riggedMesh = riggedSnapshot.meshes.find((mesh) => mesh.id === "mesh:RiggedShapeMesh"); const riggedArmature = riggedSnapshot.armatures?.find((armature) => armature.id === "armature:RiggedArmature"); const riggedModifier = riggedMesh?.modifierStack?.find((modifier) => modifier.name === "Preview Decimate"); const riggedArmatureModifier = riggedMesh?.modifierStack?.find((modifier) => modifier.type === "ARMATURE"); if (!riggedMesh || riggedMesh.skinWeights?.boneNames?.join(",") !== "Root,Tip" || riggedMesh.skinWeights.indices?.length !== 16 || riggedMesh.skinWeights.weights?.length !== 16 || riggedMesh.skinWeights.armatureId !== riggedArmature?.id || riggedMesh.skinWeights.jointIds?.length !== 2 || riggedArmature?.bones?.length !== 2 || riggedArmature.bones[1]?.parentId !== riggedArmature.bones[0]?.id || riggedArmature.bones.some((bone) => bone.restMatrix.length !== 16 || bone.poseMatrix?.length !== 16) || riggedMesh.skinWeights.bindMatrix?.[12] === 0 || riggedMesh.shapeKeys?.length !== 1 || riggedMesh.shapeKeys[0]?.name !== "Smile" || riggedMesh.shapeKeys[0]?.positions?.[8] !== 0.5 || !riggedModifier || riggedModifier.type !== "DECIMATE" || riggedModifier.parameters?.ratio !== 0.75 || riggedArmatureModifier?.parameters?.armatureObjectId !== "object:RiggedArmatureObject" || riggedArmatureModifier?.parameters?.targetObjectIds?.[0] !== "object:RiggedArmatureObject" || riggedArmatureModifier?.dependsOn?.[0] !== "object:RiggedArmatureObject" || !riggedModifier.enabled || riggedModifier.showRender) { throw new Error(`unexpected rigged SceneIR: ${JSON.stringify(riggedMesh)}`); } const riggedDepsgraph = readHandleDepsgraph(handle); if (riggedDepsgraph.unavailable) { if (riggedDepsgraph.code !== -3 || !riggedDepsgraph.error.includes("headless frame evaluation is memory-safe")) { throw new Error(`Depsgraph did not fail safely: ${JSON.stringify(riggedDepsgraph)}`); } } else { const evaluatedRiggedMesh = riggedDepsgraph.meshes?.find((mesh) => mesh.sourceMeshId === "mesh:RiggedShapeMesh"); if (riggedDepsgraph.engine !== "BlenderDepsgraph" || riggedDepsgraph.status !== "EVALUATED" || riggedDepsgraph.meshObjectCount < 1 || !evaluatedRiggedMesh || evaluatedRiggedMesh.positions?.length !== evaluatedRiggedMesh.vertexCount * 3 || evaluatedRiggedMesh.indices?.length !== evaluatedRiggedMesh.triangleCount * 3 || evaluatedRiggedMesh.modifierCount < 1) { throw new Error(`unexpected Blender Depsgraph report: ${JSON.stringify(riggedDepsgraph)}`); } } const modifierUuid = riggedModifier.uuid; if (applyHandleCommand(handle, { type: "setModifierEnabled", meshId: riggedMesh.id, modifierUuid, enabled: false }) !== 0) { throw new Error(`setModifierEnabled failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const modifierDisabled = readHandleSnapshot(handle); if (modifierDisabled.meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.uuid === modifierUuid)?.enabled !== false || modifierDisabled.revision !== riggedSnapshot.revision + 1) { throw new Error(`modifier disable did not update SceneIR: ${JSON.stringify(modifierDisabled.meshes[0]?.modifierStack)}`); } const modifierSaveDataOut = engine._malloc(4); const modifierSaveLengthOut = engine._malloc(4); if (engine._web_engine_save_blend(handle, modifierSaveDataOut, modifierSaveLengthOut) !== 0) { throw new Error(`modifier authoritative save failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const modifierSavedPointer = engine.HEAPU32[modifierSaveDataOut >>> 2]; const modifierSavedLength = engine.HEAPU32[modifierSaveLengthOut >>> 2]; const modifierSaved = engine.HEAPU8.slice(modifierSavedPointer, modifierSavedPointer + modifierSavedLength); engine._web_engine_free_buffer(modifierSavedPointer); engine._free(modifierSaveDataOut); engine._free(modifierSaveLengthOut); const reopenedModifierHandle = engine._web_engine_create(); if (openFixture(reopenedModifierHandle, modifierSaved) !== 0) { throw new Error(`modifier save could not be reopened: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const reopenedModifierSnapshot = readHandleSnapshot(reopenedModifierHandle); if (reopenedModifierSnapshot.meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.uuid === modifierUuid)?.enabled !== false) { throw new Error("modifier save lost disabled state"); } engine._web_engine_destroy(reopenedModifierHandle); if (engine._web_engine_undo(handle) !== 0) throw new Error("modifier undo failed"); const modifierUndone = readHandleSnapshot(handle); if (modifierUndone.meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.uuid === modifierUuid)?.enabled !== true) { throw new Error("modifier undo did not restore enabled state"); } const modifierUndoSaved = saveHandleBlend(handle); const modifierUndoHandle = engine._web_engine_create(); if (openFixture(modifierUndoHandle, modifierUndoSaved) !== 0 || readHandleSnapshot(modifierUndoHandle).meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.uuid === modifierUuid)?.enabled !== true) { throw new Error("modifier undo did not restore an authoritative, reopenable Main"); } engine._web_engine_destroy(modifierUndoHandle); if (engine._web_engine_redo(handle) !== 0) throw new Error("modifier redo failed"); const modifierRedone = readHandleSnapshot(handle); if (modifierRedone.meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.uuid === modifierUuid)?.enabled !== false) { throw new Error("modifier redo did not restore disabled state"); } saveHandleBlend(handle); const armatureModifier = modifierRedone.meshes.find((mesh) => mesh.id === riggedMesh.id)?.modifierStack?.find((modifier) => modifier.type === "ARMATURE"); if (!armatureModifier || applyHandleCommand(handle, { type: "setModifierEnabled", meshId: riggedMesh.id, modifierUuid: armatureModifier.uuid, enabled: false }) !== 0) { throw new Error(`armature modifier disable failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const skinSource = readHandleSnapshot(handle); const skinProfile = { schemaVersion: 1, sourceMeshRevision: skinSource.revision, attributePolicy: "PRESERVE", mode: "COLLAPSE", ratio: 0.5, vertexGroup: null, vertexGroupFactor: 1, vertexGroupInvert: false, triangulate: false, useSymmetry: false, symmetryAxis: 0, symmetryTolerance: 1e-4, skinPolicy: { maxInfluences: 4, minWeight: 0.001, maxPositionError: 0.001, shapeKeys: "PRESERVE" }, }; const { skinPolicy: ignoredSkinPolicy, ...skinlessProfile } = skinProfile; if (applyHandleCommand(handle, { type: "decimateMesh", meshId: riggedMesh.id, profile: skinlessProfile }) !== -3 || !engine.UTF8ToString(engine._web_engine_last_error_message()).includes("requires skinPolicy")) { throw new Error("skinless decimate was not safely blocked"); } if (applyHandleCommand(handle, { type: "decimateMesh", meshId: riggedMesh.id, profile: skinProfile }) !== 0) { throw new Error(`skin-aware decimate failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const skinDecimated = readHandleSnapshot(handle).meshes.find((mesh) => mesh.id === riggedMesh.id); if (!skinDecimated?.skinWeights || skinDecimated.skinWeights.indices.length !== skinDecimated.vertexCount * 4 || skinDecimated.skinWeights.weights.length !== skinDecimated.vertexCount * 4 || skinDecimated.skinWeights.jointIds?.join(",") !== riggedMesh.skinWeights.jointIds?.join(",") || skinDecimated.skinWeights.bindMatrix?.join(",") !== riggedMesh.skinWeights.bindMatrix?.join(",") || skinDecimated.shapeKeys?.some((shape) => shape.positions.length !== skinDecimated.vertexCount * 3) || !skinDecimated.shapeKeys?.[0]?.positions.some((value) => Math.abs(value) > 0)) { throw new Error(`skin-aware decimate did not remap attributes: ${JSON.stringify(skinDecimated)}`); } for (let vertex = 0; vertex < skinDecimated.vertexCount; vertex++) { const total = skinDecimated.skinWeights.weights.slice(vertex * 4, vertex * 4 + 4).reduce((sum, weight) => sum + weight, 0); if (Math.abs(total - 1) > 1e-5) throw new Error(`skin weights are not normalized at vertex ${vertex}`); } const skinSaveDataOut = engine._malloc(4); const skinSaveLengthOut = engine._malloc(4); if (engine._web_engine_save_blend(handle, skinSaveDataOut, skinSaveLengthOut) !== -3 || !engine.UTF8ToString(engine._web_engine_last_error_message()).includes("BLEND_WRITE_REQUIRES_MAIN_AUTHORITY")) { throw new Error("skin/shape-key decimate should remain structurally blocked from save"); } engine._free(skinSaveDataOut); engine._free(skinSaveLengthOut); const input = engine._malloc(blend.byteLength); engine.HEAPU8.set(blend, input); if (engine._web_engine_open_blend(handle, input, blend.byteLength) !== 0) { throw new Error(`basic_scene.blend rejected: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(input); const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); if (engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut) !== 0) { throw new Error("snapshot request failed"); } const snapshotPointer = engine.HEAPU32[dataOut >>> 2]; const snapshotLength = engine.HEAPU32[lengthOut >>> 2]; const snapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(snapshotPointer, snapshotPointer + snapshotLength))); if (snapshot.schemaVersion !== 1 || snapshot.nodes.length !== 3 || snapshot.meshes[0].vertexCount !== 8 || snapshot.meshes[0].geometryStatus !== "available" || snapshot.meshes[0].positions.length !== 24 || snapshot.meshes[0].indices.length !== 36 || snapshot.nodes.some((node) => node.worldMatrix.length !== 16 || typeof node.selectable !== "boolean") || !Array.isArray(snapshot.images) || !Array.isArray(snapshot.animations) || snapshot.cameras[0]?.sensorWidthMm !== 36 || snapshot.cameras[0]?.sensorHeightMm !== 24 || snapshot.lights[0]?.areaSize !== 5 || snapshot.lights[0]?.areaSizeY !== 1 || snapshot.scenes[0]?.worldId !== "world:World" || snapshot.worlds[0]?.id !== "world:World") { throw new Error(`unexpected SceneIR: ${JSON.stringify({ nodes: snapshot.nodes.length, meshes: snapshot.meshes.length })}`); } if (engine._web_engine_apply_command(handle, 0, 0) !== -2) { throw new Error("empty command input should return INVALID_ARGUMENT"); } const frameCommand = new TextEncoder().encode(JSON.stringify({ type: "setFrame", frame: 12 })); const frameCommandPointer = engine._malloc(frameCommand.byteLength); engine.HEAPU8.set(frameCommand, frameCommandPointer); if (engine._web_engine_apply_command(handle, frameCommandPointer, frameCommand.byteLength) !== 0) { throw new Error(`setFrame command failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(frameCommandPointer); const frameDeltaDataOut = engine._malloc(4); const frameDeltaLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_delta(handle, frameDeltaDataOut, frameDeltaLengthOut) !== 0) { throw new Error("setFrame SceneDelta request failed"); } const frameDeltaPointer = engine.HEAPU32[frameDeltaDataOut >>> 2]; const frameDeltaLength = engine.HEAPU32[frameDeltaLengthOut >>> 2]; const frameDelta = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(frameDeltaPointer, frameDeltaPointer + frameDeltaLength))); const frameRevision = snapshot.revision + 1; if (frameDelta.baseRevision !== snapshot.revision || frameDelta.nextRevision !== frameRevision || frameDelta.frame?.current !== 12) { throw new Error(`unexpected setFrame SceneDelta: ${JSON.stringify(frameDelta)}`); } engine._free(frameDeltaDataOut); engine._free(frameDeltaLengthOut); const visibilityCommand = new TextEncoder().encode(JSON.stringify({ type: "setObjectVisibility", objectId: "object:BasicCube", visible: false })); const visibilityCommandPointer = engine._malloc(visibilityCommand.byteLength); engine.HEAPU8.set(visibilityCommand, visibilityCommandPointer); if (engine._web_engine_apply_command(handle, visibilityCommandPointer, visibilityCommand.byteLength) !== 0) { throw new Error(`setObjectVisibility command failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(visibilityCommandPointer); const visibilityRevision = frameRevision + 1; const visibilityDataOut = engine._malloc(4); const visibilityLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_snapshot(handle, visibilityDataOut, visibilityLengthOut) !== 0) { throw new Error("visibility snapshot request failed"); } const visibilitySnapshotPointer = engine.HEAPU32[visibilityDataOut >>> 2]; const visibilitySnapshotLength = engine.HEAPU32[visibilityLengthOut >>> 2]; const visibilitySnapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(visibilitySnapshotPointer, visibilitySnapshotPointer + visibilitySnapshotLength))); if (visibilitySnapshot.revision !== visibilityRevision || visibilitySnapshot.nodes.find((node) => node.id === "object:BasicCube")?.visible !== false) { throw new Error(`unexpected visibility snapshot: ${JSON.stringify(visibilitySnapshot)}`); } engine._free(visibilityDataOut); engine._free(visibilityLengthOut); const authoritySaveDataOut = engine._malloc(4); const authoritySaveLengthOut = engine._malloc(4); if (engine._web_engine_save_blend(handle, authoritySaveDataOut, authoritySaveLengthOut) !== 0) { throw new Error(`authoritative save request failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const authoritySavedPointer = engine.HEAPU32[authoritySaveDataOut >>> 2]; const authoritySavedLength = engine.HEAPU32[authoritySaveLengthOut >>> 2]; if (authoritySavedPointer === 0 || authoritySavedLength === 0) { throw new Error("authoritative save produced an empty buffer"); } const authoritySaved = engine.HEAPU8.slice(authoritySavedPointer, authoritySavedPointer + authoritySavedLength); engine._web_engine_free_buffer(authoritySavedPointer); engine._free(authoritySaveDataOut); engine._free(authoritySaveLengthOut); const reopenedAuthorityHandle = engine._web_engine_create(); if (openFixture(reopenedAuthorityHandle, authoritySaved) !== 0) { throw new Error(`authoritative save could not be reopened: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const reopenedAuthoritySnapshot = readHandleSnapshot(reopenedAuthorityHandle); if (reopenedAuthoritySnapshot.frame?.current !== 12 || reopenedAuthoritySnapshot.nodes.find((node) => node.id === "object:BasicCube")?.visible !== false) { throw new Error(`authoritative save lost edits: ${JSON.stringify(reopenedAuthoritySnapshot.frame)}`); } engine._web_engine_destroy(reopenedAuthorityHandle); const decimateMesh = visibilitySnapshot.meshes.find((mesh) => mesh.id === visibilitySnapshot.nodes.find((node) => node.id === "object:BasicCube")?.dataId) ?? visibilitySnapshot.meshes[0]; const decimateProfile = { schemaVersion: 1, sourceMeshRevision: visibilitySnapshot.revision, attributePolicy: "PRESERVE", mode: "COLLAPSE", ratio: 0.5, vertexGroup: null, vertexGroupFactor: 1, vertexGroupInvert: false, triangulate: false, useSymmetry: false, symmetryAxis: 0, symmetryTolerance: 1e-4, }; const decimateCommand = new TextEncoder().encode(JSON.stringify({ type: "decimateMesh", meshId: decimateMesh.id, profile: decimateProfile })); const decimateCommandPointer = engine._malloc(decimateCommand.byteLength); engine.HEAPU8.set(decimateCommand, decimateCommandPointer); if (engine._web_engine_apply_command(handle, decimateCommandPointer, decimateCommand.byteLength) !== 0) { throw new Error(`decimateMesh command failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } engine._free(decimateCommandPointer); const decimateDataOut = engine._malloc(4); const decimateLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_snapshot(handle, decimateDataOut, decimateLengthOut) !== 0) { throw new Error("decimate snapshot request failed"); } const decimatePointer = engine.HEAPU32[decimateDataOut >>> 2]; const decimateLength = engine.HEAPU32[decimateLengthOut >>> 2]; const decimateSnapshot = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(decimatePointer, decimatePointer + decimateLength))); const decimatedMesh = decimateSnapshot.meshes.find((mesh) => mesh.id === decimateMesh.id); const decimateRevision = visibilityRevision + 1; if (decimateSnapshot.revision !== decimateRevision || !decimatedMesh || decimatedMesh.triangleCount <= 0 || decimatedMesh.triangleCount >= decimateMesh.triangleCount || decimatedMesh.vertexCount >= decimateMesh.vertexCount || decimatedMesh.positions?.length !== decimatedMesh.vertexCount * 3 || decimatedMesh.indices?.length !== decimatedMesh.triangleCount * 3 || decimatedMesh.indices.some((index) => index >= decimatedMesh.vertexCount) || decimatedMesh.normals?.length !== decimatedMesh.vertexCount * 3 || decimatedMesh.triangleCornerIndices?.length !== decimatedMesh.triangleCount * 3 || decimatedMesh.uvs?.length !== decimatedMesh.triangleCount * 6 || decimatedMesh.triangleMaterialIndices?.length !== decimatedMesh.triangleCount) { throw new Error(`unexpected decimated mesh: ${JSON.stringify(decimatedMesh)}`); } engine._free(decimateDataOut); engine._free(decimateLengthOut); engine._free(dataOut); engine._free(lengthOut); const decimateSaved = saveHandleBlend(handle); const decimateSavedHandle = engine._web_engine_create(); if (openFixture(decimateSavedHandle, decimateSaved) !== 0 || readHandleSnapshot(decimateSavedHandle).meshes.find((mesh) => mesh.id === decimateMesh.id)?.triangleCount !== decimatedMesh.triangleCount) { throw new Error("authoritative decimate did not survive save/reopen"); } engine._web_engine_destroy(decimateSavedHandle); if (engine._web_engine_undo(handle) !== 0) throw new Error("authoritative decimate undo failed"); const decimateUndone = readHandleSnapshot(handle); if (decimateUndone.meshes.find((mesh) => mesh.id === decimateMesh.id)?.triangleCount !== decimateMesh.triangleCount) { throw new Error("snapshot-only undo did not restore original geometry"); } saveHandleBlend(handle); if (engine._web_engine_redo(handle) !== 0) throw new Error("authoritative decimate redo failed"); const decimateRedone = readHandleSnapshot(handle); if (decimateRedone.meshes.find((mesh) => mesh.id === decimateMesh.id)?.triangleCount !== decimatedMesh.triangleCount) { throw new Error("snapshot-only redo did not restore decimated geometry"); } if (applyHandleCommand(handle, { type: "setFrame", frame: 2 }) !== 0) { throw new Error("Main command failed after authoritative decimate redo"); } saveHandleBlend(handle); const postHistoryRevision = decimateRevision + 3; const emptyAgainInput = engine._malloc(emptyBlend.byteLength); engine.HEAPU8.set(emptyBlend, emptyAgainInput); if (engine._web_engine_open_blend(handle, emptyAgainInput, emptyBlend.byteLength) !== 0) { throw new Error("second empty.blend open failed"); } engine._free(emptyAgainInput); const deltaDataOut = engine._malloc(4); const deltaLengthOut = engine._malloc(4); if (engine._web_engine_get_scene_delta(handle, deltaDataOut, deltaLengthOut) !== 0) { throw new Error("native SceneDelta request failed"); } const deltaPointer = engine.HEAPU32[deltaDataOut >>> 2]; const deltaLength = engine.HEAPU32[deltaLengthOut >>> 2]; const delta = JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(deltaPointer, deltaPointer + deltaLength))); if (delta.schemaVersion !== 1 || delta.baseRevision !== postHistoryRevision || delta.nextRevision !== postHistoryRevision + 1 || !delta.nodes?.removed?.includes("object:BasicCube")) { throw new Error(`unexpected native SceneDelta: ${JSON.stringify(delta)}`); } engine._free(deltaDataOut); engine._free(deltaLengthOut); for (const modeProfile of [ { mode: "UNSUBDIV", iterations: 1 }, { mode: "DISSOLVE_PLANAR", angleLimit: 0.08726646, useDissolveBoundaries: false, delimit: ["MATERIAL"] }, ]) { const modeHandle = engine._web_engine_create(); if (openFixture(modeHandle, blend) !== 0) throw new Error(`${modeProfile.mode} fixture open failed`); const modeSource = readHandleSnapshot(modeHandle); const modeMesh = modeSource.meshes[0]; const modeResult = applyHandleCommand(modeHandle, { type: "decimateMesh", meshId: modeMesh.id, profile: { schemaVersion: 1, sourceMeshRevision: modeSource.revision, attributePolicy: "PRESERVE", ...modeProfile }, }); if (modeResult !== 0) { throw new Error(`${modeProfile.mode} failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const modeSnapshot = readHandleSnapshot(modeHandle); const modeOutput = modeSnapshot.meshes[0]; if (modeSnapshot.revision !== modeSource.revision + 1 || !modeOutput || modeOutput.triangleCount <= 0 || modeOutput.positions?.length !== modeOutput.vertexCount * 3 || modeOutput.indices?.length !== modeOutput.triangleCount * 3 || modeOutput.normals?.length !== modeOutput.vertexCount * 3 || modeOutput.uvs?.length !== modeOutput.triangleCount * 6 || modeOutput.triangleMaterialIndices?.length !== modeOutput.triangleCount) { throw new Error(`unexpected ${modeProfile.mode} result: ${JSON.stringify(modeOutput)}`); } saveHandleBlend(modeHandle); engine._web_engine_destroy(modeHandle); } const modelingHandle = engine._web_engine_create(); if (openFixture(modelingHandle, emptyBlend) !== 0) throw new Error("modeling fixture open failed"); const modelingStart = readHandleSnapshot(modelingHandle); if (applyHandleCommand(modelingHandle, { type: "createPrimitive", primitive: "CUBE", name: "WebCube", location: [1, 2, 3], }) !== 0) { throw new Error(`createPrimitive failed: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } const created = readHandleSnapshot(modelingHandle); const cubeNode = created.nodes.find((node) => node.name === "WebCube"); const cubeMesh = created.meshes.find((mesh) => mesh.id === cubeNode?.dataId); if (created.revision !== modelingStart.revision + 1 || !cubeNode || !cubeMesh || cubeMesh.vertexCount !== 8 || cubeMesh.triangleCount !== 12 || JSON.stringify(cubeNode.transform.translation) !== JSON.stringify([1, 2, 3])) { throw new Error(`unexpected created primitive: ${JSON.stringify({ node: cubeNode, mesh: cubeMesh })}`); } if (applyHandleCommand(modelingHandle, { type: "setObjectTransform", objectId: cubeNode.id, translation: [2, 3, 4], rotationEuler: [0.1, 0.2, 0.3], scale: [1.5, 0.75, 2], }) !== 0) throw new Error("setObjectTransform failed"); if (applyHandleCommand(modelingHandle, { type: "translateMeshVertices", meshId: cubeMesh.id, vertexIndices: [0, 1], offset: [0, 0, 0.5], }) !== 0) throw new Error("translateMeshVertices failed"); if (applyHandleCommand(modelingHandle, { type: "duplicateObject", objectId: cubeNode.id, offset: [0.5, 0, 0], }) !== 0) throw new Error("duplicateObject failed"); const duplicated = readHandleSnapshot(modelingHandle); const duplicateNode = duplicated.nodes.find((node) => node.id !== cubeNode.id && node.name.startsWith("WebCube")); if (!duplicateNode || duplicated.nodes.length !== 2 || duplicated.meshes.length !== 2) { throw new Error(`duplicateObject did not create independent object/mesh IDs: ${JSON.stringify(duplicated.nodes)}`); } if (applyHandleCommand(modelingHandle, { type: "deleteObject", objectId: duplicateNode.id }) !== 0) { throw new Error("deleteObject failed"); } const deleted = readHandleSnapshot(modelingHandle); if (deleted.nodes.some((node) => node.id === duplicateNode.id)) throw new Error("deleteObject left a scene node"); if (engine._web_engine_undo(modelingHandle) !== 0 || !readHandleSnapshot(modelingHandle).nodes.some((node) => node.id === duplicateNode.id)) { throw new Error("modeling undo failed"); } if (engine._web_engine_redo(modelingHandle) !== 0 || readHandleSnapshot(modelingHandle).nodes.some((node) => node.id === duplicateNode.id)) { throw new Error("modeling redo failed"); } const modelingSaved = saveHandleBlend(modelingHandle); const modelingReopenHandle = engine._web_engine_create(); if (openFixture(modelingReopenHandle, modelingSaved) !== 0) throw new Error("modeling save/reopen failed"); const reopenedModel = readHandleSnapshot(modelingReopenHandle); const reopenedCube = reopenedModel.nodes.find((node) => node.name === "WebCube"); if (!reopenedCube || JSON.stringify(reopenedCube.transform.translation) !== JSON.stringify([2, 3, 4]) || reopenedModel.nodes.length !== 1) { throw new Error(`modeling edits did not survive save/reopen: ${JSON.stringify(reopenedModel.nodes)}`); } engine._web_engine_destroy(modelingReopenHandle); engine._web_engine_destroy(modelingHandle); engine._web_engine_destroy(handle); console.log(`web-engine-blend-ok handle=${handle} nodes=${snapshot.nodes.length} vertices=${snapshot.meshes[0].vertexCount} bytes=${authoritySavedLength} authoritativeDecimate=ok modeling=ok`);