import assert from "node:assert/strict"; import fs from "node:fs"; import factory from "../../web/app/src/vendor/blender/web_engine.js"; const root = new URL("../../", import.meta.url); const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root)); const fixture = fs.readFileSync(new URL("tests/files/web/modifier_grease_pencil_scene.blend", root)); function open(engine, handle, bytes) { const pointer = engine._malloc(bytes.byteLength); try { engine.HEAPU8.set(bytes, pointer); assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); } finally { engine._free(pointer); } } function output(engine, handle, fn, owned = false) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); try { assert.equal(fn(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; const bytes = engine.HEAPU8.slice(pointer, pointer + length); if (owned) engine._web_engine_free_buffer(pointer); return bytes; } finally { engine._free(dataOut); engine._free(lengthOut); } } function snapshot(engine, handle) { return JSON.parse(new TextDecoder().decode(output(engine, handle, engine._web_engine_get_scene_snapshot))); } function command(engine, handle, payload) { const bytes = new TextEncoder().encode(JSON.stringify(payload)); const pointer = engine._malloc(bytes.byteLength); try { engine.HEAPU8.set(bytes, pointer); assert.equal(engine._web_engine_apply_command(handle, pointer, bytes.byteLength), 0, `${payload.type}: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } finally { engine._free(pointer); } } function close(actual, expected, label) { assert.ok(Math.abs(actual - expected) <= 1e-6, `${label}: ${actual} != ${expected}`); } function assertFixture(scene) { const node = scene.nodes.find((candidate) => candidate.name === "GreasePencilObject"); const data = scene.greasePencils?.find((candidate) => candidate.name === "GreasePencilData"); assert.equal(node?.type, "GREASE_PENCIL"); assert.equal(node?.dataId, data?.id); assert.equal(data?.geometryStatus, "available"); assert.equal(data?.layerCount, 1); assert.equal(data?.frameCount, 1); assert.equal(data?.strokeCount, 1); assert.equal(data?.pointCount, 4); const layer = data.layers[0]; const stroke = layer.frames[0].drawing.strokes[0]; assert.equal(data.activeLayerId, layer.id); assert.equal(layer.name, "Lines"); assert.equal(layer.visible, true); assert.equal(layer.locked, false); assert.equal(stroke.cyclic, false); assert.equal(stroke.materialIndex, 0); assert.equal(stroke.id, "grease-pencil-stroke:GreasePencilData:0:0"); assert.deepEqual(stroke.points.map((point) => point.id), [ "grease-pencil-point:GreasePencilData:0:0:0", "grease-pencil-point:GreasePencilData:0:0:1", "grease-pencil-point:GreasePencilData:0:0:2", "grease-pencil-point:GreasePencilData:0:0:3", ]); assert.deepEqual(stroke.points[0].position, [-1.5, 0, 0]); close(stroke.points[0].radius, 0.05, "radius"); close(stroke.points[0].opacity, 0.9, "opacity"); assert.equal(data.attributes.length, 6); } const engine = await factory({ wasmBinary: wasmBinary.slice() }); const handle = engine._web_engine_create(); open(engine, handle, fixture); assertFixture(snapshot(engine, handle)); const original = snapshot(engine, handle).greasePencils[0]; const dataId = original.id; command(engine, handle, { type: "createGreasePencilLayer", dataId, name: "Web Drafts" }); let edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.layerCount, 2); const layerId = edited.layers.find((layer) => layer.name === "Web Drafts").id; command(engine, handle, { type: "moveGreasePencilLayer", dataId, layerId, direction: "BOTTOM", baseRevision: edited.revision }); edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.layers[0].id, layerId); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].layers[1].id, layerId); assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].layers[0].id, layerId); command(engine, handle, { type: "insertGreasePencilFrame", dataId, layerId, frame: 10, duration: 4 }); const webStrokes = [{ cyclic: true, materialIndex: 0, points: [ { position: [0, 0, 0], radius: 0.1, opacity: 0.7, vertexColor: [1, 0, 0, 1] }, { position: [1, 1, 0], radius: 0.2, opacity: 0.8, vertexColor: [0, 1, 0, 1] }, { position: [2, 0, 0], radius: 0.3, opacity: 0.9, vertexColor: [0, 0, 1, 1] }, ], }]; command(engine, handle, { type: "setGreasePencilStrokes", dataId, layerId, frame: 10, strokes: webStrokes }); edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.frameCount, 2); assert.equal(edited.strokeCount, 2); assert.equal(edited.pointCount, 7); assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].drawing.strokes[0].cyclic, true); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].strokeCount, 1); assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.pointCount, 7); const sourceFrame = edited.layers.find((layer) => layer.id === layerId).frames[0]; command(engine, handle, { type: "moveGreasePencilFrame", dataId, layerId, frame: 10, targetFrame: 12, drawingId: sourceFrame.drawing.id, baseRevision: snapshot(engine, handle).revision }); edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].frame, 12); assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].drawing.id, sourceFrame.drawing.id); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].layers.find((layer) => layer.id === layerId).frames[0].frame, 10); assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].layers.find((layer) => layer.id === layerId).frames[0].frame, 12); command(engine, handle, { type: "removeGreasePencilFrame", dataId, layerId, frame: 12 }); assert.equal(snapshot(engine, handle).greasePencils[0].frameCount, 1); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).greasePencils[0].frameCount, 2); command(engine, handle, { type: "removeGreasePencilLayer", dataId, layerId }); assert.equal(snapshot(engine, handle).greasePencils[0].layerCount, 1); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); edited = snapshot(engine, handle).greasePencils[0]; assert.equal(edited.layerCount, 2); assert.equal(edited.pointCount, 7); const saved = output(engine, handle, engine._web_engine_save_blend, true); engine._web_engine_destroy(handle); const reopened = engine._web_engine_create(); open(engine, reopened, saved); const reopenedScene = snapshot(engine, reopened); const reopenedData = reopenedScene.greasePencils[0]; assert.equal(reopenedData.layerCount, 2); assert.equal(reopenedData.frameCount, 2); assert.equal(reopenedData.strokeCount, 2); assert.equal(reopenedData.pointCount, 7); const reopenedStroke = reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].drawing.strokes[0]; assert.deepEqual(reopenedData.layers.map((layer) => layer.name), ["Web Drafts", "Lines"]); assert.equal(reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].frame, 12); assert.equal(reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].drawing.id, sourceFrame.drawing.id); assert.equal(reopenedStroke.cyclic, true); assert.deepEqual(reopenedStroke.points[2].position, [2, 0, 0]); close(reopenedStroke.points[2].radius, 0.3, "reopened radius"); assert.deepEqual( reopenedData.layers.find((layer) => layer.name === "Lines").frames[0].drawing.strokes[0].points.map((point) => point.id), original.layers[0].frames[0].drawing.strokes[0].points.map((point) => point.id), ); engine._web_engine_destroy(reopened); process.stdout.write("grease-pencil-roundtrip-ok layer-frame-stroke=passed undo-redo=passed save-reopen=passed\n");