#!/usr/bin/env python3 """Prepare the two Sculpt panel routes; remeshing remains a separate task.""" import hashlib import json import pathlib import subprocess from wbv2_execution_phase import implementation_only ROOT = pathlib.Path(__file__).resolve().parents[1] JOB = "WEBUI-SCULPT" PREPARATION_INPUTS = ["web/tests/e2e/sculpt-authoring-ui-production.spec.ts", "web/protocol/paint.ts"] def main(): context = json.loads(subprocess.check_output(["node", "tools/web/print-task-context.mjs"], cwd=ROOT, text=True)) if not implementation_only(ROOT): subprocess.run(["node", "tools/web/check-task-context.mjs"], cwd=ROOT, check=True, stdout=subprocess.DEVNULL) test_path = PREPARATION_INPUTS[0] sources = ["blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp", "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp"] changed = [test_path, "web/protocol/paint.ts", "my-tools/prepare_wbv2_sculpt_workflow.py"] deps = sorted(set(sources + changed + [ ".emscripten-web", "build_web_blender6/CMakeCache.txt", "my-tools/build_local_webengine.py", "tools/web/incremental-web-build.mjs", "tools/web/sync-web-engine-runtime.mjs", "tests/files/web/basic_scene.blend", "web/protocol/sculpt.ts", "web/protocol/web-engine.ts", "web/protocol/scene-ir.ts", "web/tests/fixtures/native-ui-workflow.ts", "web/app/src/workers/web-engine.worker.ts", "web/app/src/engine-client/WebEngineClient.ts", "web/app/src/app/App.tsx", "web/app/vite.config.ts", "web/tsconfig.json", "web/package.json", "web/playwright.config.ts", ])) missing = [path for path in deps if not (ROOT / path).is_file()] if missing: raise RuntimeError(f"Missing dependencies: {missing}") checks = [ {"id": "wasm-source-build", "argv": ["python3", "my-tools/build_local_webengine.py"], "cwd": ".", "kinds": ["source_build"], "timeoutSeconds": 1200, "continueOnFailure": True}, {"id": "web-build", "argv": ["node", "tools/web/incremental-web-build.mjs"], "cwd": ".", "kinds": ["typecheck"], "dependsOn": ["wasm-source-build"], "timeoutSeconds": 300, "continueOnFailure": True}, {"id": "authoring-lint", "argv": ["node", "node_modules/eslint/bin/eslint.js", "app/src/app/App.tsx", "protocol/paint.ts", test_path.removeprefix("web/")], "cwd": "web", "kinds": ["lint"], "timeoutSeconds": 90, "continueOnFailure": True}, ] kinds = ["positive", "negative", "undo_redo", "save_reopen", "determinism"] units = {"stroke": "web-command:sculptStroke", "attributes": "web-command:setSculptMeshAttributes"} cases = [] for route in ["panel", *units]: identifier = f"chromium-{route}" checks.append({"id": identifier, "argv": ["env", "WEB_TEST_PORT=5376", "node", "node_modules/@playwright/test/cli.js", "test", "--config", "playwright.config.ts", test_path.removeprefix("web/"), "--grep", f"sculpt {route}", "--workers=1", "--max-failures=1"], "cwd": "web", "kinds": ["chromium", *(kinds if route in units else [])], "dependsOn": ["wasm-source-build", "web-build", "authoring-lint"], "timeoutSeconds": 60, "continueOnFailure": True}) if route in units: cases.extend({"id": f"sculpt-{route}-{kind}", "kind": kind, "checkId": identifier, "receipt": f"unallocated/{route}-{kind}.json", "unitIds": [units[route]]} for kind in kinds) result = {"status": "ready", "summary": "Sculpt panel separates persisted face sets from transient active-face state", "workflow": "Apply a DRAW stroke; edit mask and face sets in the panel; independently verify each native route through rejection, history, save/reopen and replay", "sourceUnits": list(units.values()), "routeIds": list(units.values()), "sourceReferences": sources, "changedFiles": changed, "dependencies": deps, "artifacts": ["build_web_blender6/bin/web_engine.wasm", "build_web_blender6/bin/web_engine.js"], "checks": checks, "cases": cases} contract = {"schemaVersion": 1, "task": context["task"], "job": JOB, "parentSha256": hashlib.sha256((ROOT / context["sourceDocuments"]["parentManifest"]).read_bytes()).hexdigest(), "acceptance": "pending independent local validation", "result": result} target = ROOT / "my-tools/local-workflows" / f"{JOB}.json" temporary = target.with_suffix(".json.tmp") temporary.write_text(json.dumps(contract, indent=2) + "\n") temporary.replace(target) print(json.dumps({"job": JOB, "state": "prepared", "checks": len(checks), "cases": len(cases)})) if __name__ == "__main__": main()