Advance Blender WebEngine N-015 through N-022 parity
This commit is contained in:
57
tools/web/check-compositor-main-reader.mjs
Normal file
57
tools/web/check-compositor-main-reader.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
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/compositor_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 snapshot(engine, handle) {
|
||||
const dataOut = engine._malloc(4);
|
||||
const lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2];
|
||||
const length = engine.HEAPU32[lengthOut >>> 2];
|
||||
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
|
||||
}
|
||||
finally {
|
||||
engine._free(dataOut);
|
||||
engine._free(lengthOut);
|
||||
}
|
||||
}
|
||||
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
open(engine, handle, fixture);
|
||||
const scene = snapshot(engine, handle).scenes.find((candidate) => candidate.name === "CompositorScene");
|
||||
assert.equal(scene?.compositorStatus, "AVAILABLE");
|
||||
const graph = scene.compositorGraph;
|
||||
assert.equal(graph.schemaVersion, 1);
|
||||
assert.equal(graph.nodes.length, 4);
|
||||
assert.equal(graph.links.length, 2);
|
||||
const color = graph.nodes.find((node) => node.name === "WebConstantColor");
|
||||
assert.equal(color.type, "CONSTANT_COLOR");
|
||||
assert.deepEqual(color.properties.color, [0.125, 0.25, 0.5, 0.75]);
|
||||
assert.equal(graph.nodes.find((node) => node.name === "WebViewer").type, "VIEWER");
|
||||
const composite = graph.nodes.find((node) => node.name === "WebComposite");
|
||||
assert.equal(composite.type, "COMPOSITE");
|
||||
assert.equal(graph.outputNodeId, composite.id);
|
||||
const unsupported = graph.nodes.find((node) => node.name === "PreservedUnsupportedGlare");
|
||||
assert.equal(unsupported.type, "UNSUPPORTED");
|
||||
assert.equal(unsupported.blenderType, "CompositorNodeGlare");
|
||||
assert.ok(graph.links.every((link) => graph.nodes.some((node) => node.id === link.fromNodeId) &&
|
||||
graph.nodes.some((node) => node.id === link.toNodeId)));
|
||||
engine._web_engine_destroy(handle);
|
||||
process.stdout.write("compositor-main-reader-ok graph-structure=passed socket-links=passed unsupported-preserved=passed\n");
|
||||
@@ -93,8 +93,22 @@ function assertEdited(scene) {
|
||||
const light = scene.lights.find((item) => item.id === "light:Area");
|
||||
const world = scene.worlds.find((item) => item.id === "world:World");
|
||||
const definition = scene.scenes.find((item) => item.id === "scene:Scene");
|
||||
assert.equal(camera.projection, "PERSPECTIVE");
|
||||
assert.equal(camera.depthOfField.enabled, false);
|
||||
assert.equal(camera.projection, "ORTHOGRAPHIC");
|
||||
close(camera.lensMm, 35, "camera lens");
|
||||
close(camera.sensorWidthMm, 32, "camera sensor width");
|
||||
close(camera.sensorHeightMm, 18, "camera sensor height");
|
||||
assert.equal(camera.sensorFit, 1);
|
||||
close(camera.shift[0], 0.1, "camera shift x");
|
||||
close(camera.shift[1], -0.2, "camera shift y");
|
||||
close(camera.near, 0.2, "camera near");
|
||||
close(camera.far, 500, "camera far");
|
||||
close(camera.orthoScale, 8, "camera ortho scale");
|
||||
assert.equal(camera.depthOfField.enabled, true);
|
||||
close(camera.depthOfField.focusDistance, 4.5, "camera focus distance");
|
||||
close(camera.depthOfField.apertureFStop, 1.8, "camera aperture f-stop");
|
||||
assert.equal(camera.depthOfField.apertureBlades, 7);
|
||||
close(camera.depthOfField.apertureRotation, 0.25, "camera aperture rotation");
|
||||
close(camera.depthOfField.apertureRatio, 1.2, "camera aperture ratio");
|
||||
close(light.energy, 400, "light energy");
|
||||
close(light.exposure, 1, "light exposure");
|
||||
assert.equal(light.castsShadow, false);
|
||||
@@ -120,6 +134,21 @@ open(engine, handle, fixture);
|
||||
const before = assertLighting(snapshot(engine, handle));
|
||||
command(engine, handle, { type: "setObjectVisibility", objectId: "object:BasicCube", visible: true });
|
||||
assert.equal(snapshot(engine, handle).scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "AVAILABLE");
|
||||
reject(engine, handle, { type: "setCameraProperties", dataId: before.camera.id, properties: { near: 10, far: 1 } }, "RENDER_PROPERTY_INVALID");
|
||||
assert.equal(snapshot(engine, handle).cameras.find((item) => item.id === before.camera.id).near, before.camera.near);
|
||||
command(engine, handle, { type: "setCameraProperties", dataId: before.camera.id, properties: {
|
||||
projection: "ORTHOGRAPHIC", lensMm: 35, sensorWidthMm: 32, sensorHeightMm: 18,
|
||||
sensorFit: 1, shift: [0.1, -0.2], near: 0.2, far: 500, orthoScale: 8,
|
||||
depthOfField: { enabled: true, focusDistance: 4.5, apertureFStop: 1.8, apertureBlades: 7, apertureRotation: 0.25, apertureRatio: 1.2 },
|
||||
} });
|
||||
let cameraEdited = snapshot(engine, handle);
|
||||
assert.equal(cameraEdited.scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "BLOCKED");
|
||||
assert.equal(cameraEdited.cameras.find((item) => item.id === before.camera.id).projection, "ORTHOGRAPHIC");
|
||||
assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).cameras.find((item) => item.id === before.camera.id).projection, "PERSPECTIVE");
|
||||
assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
cameraEdited = snapshot(engine, handle);
|
||||
assert.equal(cameraEdited.cameras.find((item) => item.id === before.camera.id).projection, "ORTHOGRAPHIC");
|
||||
command(engine, handle, { type: "setLightProperties", dataId: before.light.id, properties: {
|
||||
color: [0.25, 0.5, 0.75], energy: 400, exposure: 1, castsShadow: false,
|
||||
temperature: 5000, useTemperature: true,
|
||||
@@ -143,4 +172,4 @@ open(engine, reopened, saved);
|
||||
assertEdited(snapshot(engine, reopened));
|
||||
engine._web_engine_destroy(reopened);
|
||||
|
||||
process.stdout.write("lighting-roundtrip-ok camera-dof=passed light-shadow-exposure=passed world-mist=passed color-management-reader=passed white-balance-gate=passed undo-redo=passed save-reopen=passed\n");
|
||||
process.stdout.write("lighting-roundtrip-ok camera-writer-dof=passed light-shadow-exposure=passed world-mist=passed color-management-reader=passed white-balance-gate=passed undo-redo=passed save-reopen=passed\n");
|
||||
|
||||
64
tools/web/check-mask-main-reader.mjs
Normal file
64
tools/web/check-mask-main-reader.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
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/mask_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 snapshot(engine, handle) {
|
||||
const dataOut = engine._malloc(4), lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2], length = engine.HEAPU32[lengthOut >>> 2];
|
||||
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
|
||||
}
|
||||
finally { engine._free(dataOut); engine._free(lengthOut); }
|
||||
}
|
||||
|
||||
const close = (actual, expected, label) => assert.ok(Math.abs(actual - expected) < 1e-6,
|
||||
`${label}: ${actual} != ${expected}`);
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
open(engine, handle, fixture);
|
||||
const scene = snapshot(engine, handle);
|
||||
assert.equal(scene.trackingMaskStatus, "AVAILABLE");
|
||||
const project = scene.trackingMasks;
|
||||
assert.equal(project.schemaVersion, 1);
|
||||
assert.equal(project.clips.length, 0);
|
||||
assert.equal(project.bindings.length, 0);
|
||||
assert.equal(project.masks.length, 1);
|
||||
const mask = project.masks[0];
|
||||
assert.equal(mask.id, "mask:WebMask");
|
||||
assert.equal(mask.layers.length, 1);
|
||||
const layer = mask.layers[0];
|
||||
assert.equal(layer.name, "WebMaskLayer");
|
||||
assert.equal(layer.locked, true);
|
||||
close(layer.opacity, 0.625, "layer opacity");
|
||||
const spline = layer.splines[0];
|
||||
assert.equal(spline.cyclic, true);
|
||||
assert.equal(spline.fill, false);
|
||||
assert.equal(spline.points.length, 3);
|
||||
assert.deepEqual(spline.points.map((point) => point.handleType), ["ALIGNED", "VECTOR", "FREE"]);
|
||||
for (const [label, actual, expected] of [
|
||||
["point co", spline.points[0].co, [0.1, 0.2]],
|
||||
["left handle", spline.points[0].handleLeft, [0.2, 0.2]],
|
||||
["right handle", spline.points[0].handleRight, [0, 0.2]],
|
||||
]) {
|
||||
actual.forEach((value, index) => close(value, expected[index], `${label}[${index}]`));
|
||||
}
|
||||
close(spline.points[2].feather, 0.75, "point feather");
|
||||
assert.deepEqual(spline.points.map((point) => point.selected), [true, false, true]);
|
||||
engine._web_engine_destroy(handle);
|
||||
process.stdout.write("mask-main-reader-ok layers-splines=passed handles-feather=passed selection=passed\n");
|
||||
54
tools/web/check-sequencer-main-reader.mjs
Normal file
54
tools/web/check-sequencer-main-reader.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
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/sequencer_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 snapshot(engine, handle) {
|
||||
const dataOut = engine._malloc(4), lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2], length = engine.HEAPU32[lengthOut >>> 2];
|
||||
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
|
||||
}
|
||||
finally { engine._free(dataOut); engine._free(lengthOut); }
|
||||
}
|
||||
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
open(engine, handle, fixture);
|
||||
const scene = snapshot(engine, handle).scenes.find((candidate) => candidate.name === "SequencerScene");
|
||||
assert.equal(scene?.sequencerStatus, "AVAILABLE");
|
||||
const timeline = scene.sequencerTimeline;
|
||||
assert.equal(timeline.schemaVersion, 1);
|
||||
assert.equal(timeline.fpsNumerator, 24000);
|
||||
assert.equal(timeline.fpsDenominator, 1001);
|
||||
assert.equal(timeline.strips.length, 4);
|
||||
const sound = timeline.strips.find((strip) => strip.name === "WebSound");
|
||||
assert.equal(sound.type, "SOUND");
|
||||
assert.equal(sound.sourcePath, "//media/sequencer-silence.wav");
|
||||
assert.equal(sound.muted, true);
|
||||
assert.equal(sound.locked, true);
|
||||
const image = timeline.strips.find((strip) => strip.name === "WebImage");
|
||||
assert.equal(image.type, "IMAGE");
|
||||
assert.equal(image.sourcePath, "//media/sequencer-frame.png");
|
||||
const cross = timeline.strips.find((strip) => strip.name === "WebCross");
|
||||
assert.equal(cross.type, "EFFECT");
|
||||
assert.equal(cross.effectType, "CROSS");
|
||||
assert.equal(cross.inputStripIds.length, 2);
|
||||
assert.ok(cross.inputStripIds.every((id) => timeline.strips.some((strip) => strip.id === id)));
|
||||
engine._web_engine_destroy(handle);
|
||||
process.stdout.write("sequencer-main-reader-ok media-paths=passed fps=passed effect-dependencies=passed\n");
|
||||
39
tools/web/generate-compositor-fixture.py
Normal file
39
tools/web/generate-compositor-fixture.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def main(output_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
scene = bpy.context.scene
|
||||
scene.name = "CompositorScene"
|
||||
tree = bpy.data.node_groups.new("WebCompositorTree", "CompositorNodeTree")
|
||||
scene.compositing_node_group = tree
|
||||
tree.interface.new_socket(name="Image", in_out="OUTPUT", socket_type="NodeSocketColor")
|
||||
|
||||
color = tree.nodes.new("CompositorNodeRGB")
|
||||
color.name = "WebConstantColor"
|
||||
color.outputs["Color"].default_value = (0.125, 0.25, 0.5, 0.75)
|
||||
|
||||
viewer = tree.nodes.new("CompositorNodeViewer")
|
||||
viewer.name = "WebViewer"
|
||||
composite = tree.nodes.new("NodeGroupOutput")
|
||||
composite.name = "WebComposite"
|
||||
glare = tree.nodes.new("CompositorNodeGlare")
|
||||
glare.name = "PreservedUnsupportedGlare"
|
||||
|
||||
tree.links.new(color.outputs["Color"], viewer.inputs["Image"])
|
||||
tree.links.new(color.outputs["Color"], composite.inputs["Image"])
|
||||
|
||||
path = pathlib.Path(output_path).resolve()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(path), compress=False)
|
||||
print(f"compositor-fixture-generated path={path} nodes={len(tree.nodes)} links={len(tree.links)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:]
|
||||
if len(arguments) != 1:
|
||||
raise SystemExit("usage: blender -b --python generate-compositor-fixture.py -- output.blend")
|
||||
main(arguments[0])
|
||||
42
tools/web/generate-mask-fixture.py
Normal file
42
tools/web/generate-mask-fixture.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def main(output_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
mask = bpy.data.masks.new("WebMask")
|
||||
layer = mask.layers.new(name="WebMaskLayer")
|
||||
layer.alpha = 0.625
|
||||
layer.hide_select = True
|
||||
spline = layer.splines.new()
|
||||
spline.use_cyclic = True
|
||||
spline.use_fill = False
|
||||
spline.points.add(2)
|
||||
|
||||
values = [
|
||||
((0.1, 0.2), (0.0, 0.2), (0.2, 0.2), "ALIGNED", 0.25, True),
|
||||
((0.5, 0.7), (0.4, 0.7), (0.6, 0.7), "VECTOR", 0.5, False),
|
||||
((0.8, 0.3), (0.7, 0.3), (0.9, 0.3), "FREE", 0.75, True),
|
||||
]
|
||||
for point, value in zip(spline.points, values):
|
||||
co, left, right, handle_type, weight, selected = value
|
||||
point.co = co
|
||||
point.handle_left = left
|
||||
point.handle_right = right
|
||||
point.handle_type = handle_type
|
||||
point.weight = weight
|
||||
point.select = selected
|
||||
|
||||
path = pathlib.Path(output_path).resolve()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(path), compress=False)
|
||||
print(f"mask-fixture-generated path={path} points={len(spline.points)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:]
|
||||
if len(arguments) != 1:
|
||||
raise SystemExit("usage: blender -b --python generate-mask-fixture.py -- output.blend")
|
||||
main(arguments[0])
|
||||
65
tools/web/generate-sequencer-fixture.py
Normal file
65
tools/web/generate-sequencer-fixture.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import pathlib
|
||||
import sys
|
||||
import wave
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def write_silence(path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with wave.open(str(path), "wb") as stream:
|
||||
stream.setnchannels(1)
|
||||
stream.setsampwidth(2)
|
||||
stream.setframerate(8000)
|
||||
stream.writeframes(b"\0\0" * 8000)
|
||||
|
||||
|
||||
def main(output_path):
|
||||
output = pathlib.Path(output_path).resolve()
|
||||
media = output.parent / "media"
|
||||
audio_path = media / "sequencer-silence.wav"
|
||||
image_path = media / "sequencer-frame.png"
|
||||
write_silence(audio_path)
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("SequencerFrame", width=8, height=8, alpha=True)
|
||||
image.generated_color = (0.2, 0.4, 0.8, 1.0)
|
||||
image.filepath_raw = str(image_path)
|
||||
image.file_format = "PNG"
|
||||
image.save()
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.name = "SequencerScene"
|
||||
scene.render.fps = 24
|
||||
scene.render.fps_base = 1.001
|
||||
editor = scene.sequence_editor_create()
|
||||
|
||||
sound = editor.strips.new_sound("WebSound", str(audio_path), channel=1, frame_start=10)
|
||||
sound.mute = True
|
||||
sound.lock = True
|
||||
sound.sound.filepath = "//media/sequencer-silence.wav"
|
||||
|
||||
image_strip = editor.strips.new_image("WebImage", str(image_path), channel=2, frame_start=12)
|
||||
image_strip.directory = "//media/"
|
||||
image_strip.elements[0].filename = "sequencer-frame.png"
|
||||
image_strip.frame_final_duration = 20
|
||||
|
||||
second_image = editor.strips.new_image("WebImageB", str(image_path), channel=3, frame_start=12)
|
||||
second_image.directory = "//media/"
|
||||
second_image.elements[0].filename = "sequencer-frame.png"
|
||||
second_image.frame_final_duration = 20
|
||||
cross = editor.strips.new_effect(
|
||||
"WebCross", "CROSS", channel=4, frame_start=12, length=20, input1=image_strip, input2=second_image)
|
||||
cross.mute = True
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(output), compress=False)
|
||||
print(f"sequencer-fixture-generated path={output} strips={len(editor.strips_all)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:]
|
||||
if len(arguments) != 1:
|
||||
raise SystemExit("usage: blender -b --python generate-sequencer-fixture.py -- output.blend")
|
||||
main(arguments[0])
|
||||
Reference in New Issue
Block a user