Advance Blender WebEngine N-015 through N-022 parity

This commit is contained in:
mes123456
2026-08-12 13:26:34 -04:00
parent 9fd26010f6
commit b3cefaeec5
51 changed files with 1809 additions and 171 deletions

View File

@@ -425,6 +425,37 @@ test("enforces the N-016 Grease Pencil layer/frame/drawing/stroke/point schema b
expect(result.budget).toBe("accepted-blocked-budget");
});
test("renders a bounded previous/next N-016 Grease Pencil onion-skin preview", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const { createGreasePencilObject } = await import("/src/three-adapter/grease-pencil.ts");
const point = (x: number) => ({ position: [x, 0, 0] as [number, number, number], radius: 0.1, opacity: 1, vertexColor: [0.2, 0.4, 0.8, 1] as [number, number, number, number] });
const drawing = (id: string, x: number) => ({ id, strokeCount: 1, pointCount: 2, strokes: [{ cyclic: false, pointCount: 2, materialIndex: 0, points: [point(x), point(x + 1)] }] });
const object = createGreasePencilObject({
id: "grease-pencil:onion",
name: "Onion",
geometryStatus: "available",
layerCount: 1,
frameCount: 3,
strokeCount: 3,
pointCount: 6,
layers: [{ id: "layer:1", name: "Lines", visible: true, locked: false, opacity: 1, onionSkinning: true, frames: [
{ frame: 1, drawing: drawing("drawing:1", -2) },
{ frame: 5, drawing: drawing("drawing:5", 0) },
{ frame: 9, drawing: drawing("drawing:9", 2) },
] }],
}, 5);
if (!object) return null;
return {
onion: object.userData.greasePencilOnionStrokeCount,
current: object.userData.greasePencilCurrentStrokeCount,
kinds: object.children.map((child) => child.userData.greasePencilOnion),
opacities: object.children.map((child) => "material" in child && !Array.isArray(child.material) ? (child.material as { opacity?: number }).opacity : undefined),
};
});
expect(result).toEqual({ onion: 2, current: 1, kinds: ["PREVIOUS", "NONE", "NEXT"], opacities: [0.28, 1, 0.28] });
});
for (const offscreen of [false, true]) {
test(`renders the N-016 Grease Pencil current-frame strokes in ${offscreen ? "OffscreenCanvas" : "main-thread"} Chromium`, async ({ page }) => {
await page.goto(offscreen ? "/?offscreen=1" : "/");
@@ -465,6 +496,33 @@ test("enforces the N-017 paint stroke, PBVH/UV hit and brush budgets", async ({
expect(result.budget).toContain("PAINT_BUDGET_EXCEEDED");
});
test("derives an N-017 source-face, barycentric and UV paint hit from a real raycast", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const three = await import("/src/vendor/three/three.module.js");
const { paintHitFromIntersection } = await import("/src/three-adapter/paint-hit.ts");
const geometry = new three.BufferGeometry();
geometry.setAttribute("position", new three.Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 0], 3));
geometry.setAttribute("uv", new three.Float32BufferAttribute([0, 0, 1, 0, 0, 1], 2));
geometry.setIndex([0, 1, 2]);
const mesh = new three.Mesh(geometry, new three.MeshBasicMaterial());
mesh.userData.blenderId = "object:paint";
mesh.userData.meshId = "mesh:paint";
mesh.userData.triangleFaceIndices = [17];
mesh.updateMatrixWorld(true);
const raycaster = new three.Raycaster(new three.Vector3(0.25, 0.25, 1), new three.Vector3(0, 0, -1));
const intersection = raycaster.intersectObject(mesh)[0];
return intersection ? paintHitFromIntersection(intersection, 0.75) : null;
});
expect(result?.objectId).toBe("object:paint");
expect(result?.dataId).toBe("mesh:paint");
expect(result?.faceIndex).toBe(17);
expect(result?.pressure).toBe(0.75);
expect(result?.barycentric).toEqual(expect.arrayContaining([0.5, 0.25, 0.25]));
expect(result?.uv).toEqual([0.25, 0.25]);
expect(result?.normal).toEqual([0, 0, 1]);
});
test("validates the N-018 physics capability and cache manifests without claiming solvers", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(() => new Promise<Record<string, unknown>>((resolve, reject) => {
@@ -642,11 +700,16 @@ test("keeps N-015 selection history bounded and rejects stale raycast hits", asy
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({});
}));
expect(result.history).toEqual([3, 1, [1, 3], "HANDLE_LEFT"]);
expect(result.history).toEqual([3, 1, [
["curve:1", [1, 3], "HANDLE_LEFT"],
["curve:2", [2], "HANDLE_RIGHT"],
]]);
expect(result.revision).toContain("REVISION_CONFLICT");
expect(result.budget).toContain("SELECTION_HISTORY_BUDGET_EXCEEDED");
expect(result.raycast).toContain("RAYCAST_HIT_INVALID");
expect(result.handleHit).toBe("HANDLE_RIGHT");
expect(result.handleHit).toEqual(["object:1", "HANDLE_RIGHT"]);
expect(result.rangePatch).toEqual([["curve:1", [1, 2, 4]]]);
expect(result.migrated).toEqual([2, "mesh:1"]);
expect(result.gates).toEqual(["READY", "READY", "BLOCKED"]);
});
@@ -895,6 +958,14 @@ test("persists and revalidates content-addressed Simulation caches across Worker
const restarted = new StorageClient();
const listed = await restarted.listSimulationCaches(projectId);
const read = await restarted.readSimulationCache(projectId, stored.cacheKey);
const frameRead = await restarted.readSimulationCacheFrame(projectId, stored.cacheKey, 2);
let missingFrameCode = "";
try {
await restarted.readSimulationCacheFrame(projectId, stored.cacheKey, 3);
}
catch (error) {
missingFrameCode = String((error as Error & { code?: string }).code ?? "");
}
let corruptCode = "";
try {
await restarted.putSimulationCache(projectId, { ...manifest, cacheSha256: "0".repeat(64) }, source.buffer.slice(0));
@@ -908,6 +979,10 @@ test("persists and revalidates content-addressed Simulation caches across Worker
path: stored.path,
listed: listed.caches.map((cache) => cache.cacheKey),
bytes: Array.from(new Uint8Array(read.data)),
frame: frameRead.frame,
frameOffset: frameRead.byteOffset,
frameBytes: Array.from(new Uint8Array(frameRead.data)),
missingFrameCode,
corruptCode,
};
});
@@ -915,6 +990,10 @@ test("persists and revalidates content-addressed Simulation caches across Worker
expect(result.path).toMatch(/^projects\/simulation-e2e-[0-9]+\/assets\/sha256\/[a-f0-9]{2}\/[a-f0-9]{64}$/);
expect(result.listed).toContain(result.cacheKey);
expect(result.bytes).toEqual([11, 12, 13, 21, 22, 23, 24]);
expect(result.frame).toBe(2);
expect(result.frameOffset).toBe(3);
expect(result.frameBytes).toEqual([21, 22, 23, 24]);
expect(result.missingFrameCode).toBe("SIMULATION_CACHE_MISSING");
expect(result.corruptCode).toBe("SIMULATION_CACHE_HASH_MISMATCH");
});