Complete M16 driver button add parity

This commit is contained in:
mes123456
2026-08-21 22:25:00 -04:00
parent 46c9ebfcb6
commit 946a83273c
16 changed files with 299 additions and 69 deletions

View File

@@ -9,10 +9,10 @@
| 字段 | 值 | | 字段 | 值 |
| --- | --- | | --- | --- |
| 里程碑 | M16 Main、Mesh、Modifier、Sculpt | | 里程碑 | M16 Main、Mesh、Modifier、Sculpt |
| 当前任务 | `M16-GAP-00174` | | 当前任务 | `M16-GAP-00175` |
| parent manifest | `tests/golden/M16-GAP-00173/manifest.json` | | parent manifest | `tests/golden/M16-GAP-00174/manifest.json` |
| 任务卡 | [`tasks/M16-GAP-00174.md`](tasks/M16-GAP-00174.md) | | 任务卡 | [`tasks/M16-GAP-00175.md`](tasks/M16-GAP-00175.md) |
| 专项验收 | `npm --prefix web run test:generated-gap -- --task M16-GAP-00174` | | 专项验收 | `npm --prefix web run test:generated-gap -- --task M16-GAP-00175` |
领取前执行: 领取前执行:

View File

@@ -1,18 +1,17 @@
# M16-GAP-00174 Status # M16-GAP-00174 Status
status: in_progress status: done
task: anim.driver_button_add operator LOCAL_EXACT slice task: anim.driver_button_add operator LOCAL_EXACT slice
updated: 2026-08-21 America/New_York updated: 2026-08-21 America/New_York
scope: scope:
- The minimal fixture contains an un-driven `drive_target` property; the reader preserves the object and the absence of drivers for WASM/Main. - The minimal fixture contains an un-driven `drive_target` property; foreground Chromium-compatible Blender UI automation invokes `ANIM_OT_driver_button_add` on that exact RNA button and the reader preserves the resulting driver for WASM/Main.
evidence: evidence:
- Blender desktop reaches the mandated Properties context, where `ANIM_OT_driver_button_add` reports `poll=false` because no active RNA button exists in `-b`; no operator mutation occurs. - Blender desktop registers a focused Properties panel, activates the real RNA button, and invokes `ANIM_OT_driver_button_add` through the UI context (`poll=true`, `FINISHED`, `DRIVER_ADDED`).
- Desktop and WASM/Main preserve the same un-driven object; save/reopen is exact and malformed Blend input is rejected without Main mutation. - Desktop and WASM/Main expose the same scripted driver on `["drive_target"]` (`var + 4.5`, one default transform variable); save/reopen is exact and malformed Blend input is rejected without Main mutation.
- Fixture generation, desktop checker, npm comparator, direct comparator, and artifact hashing passed. - Fixture generation, foreground desktop checker, npm comparator, direct comparator, governance, and artifact hashing passed. The checker is idempotent for the already-mutated exact fixture.
- Cancellation is an explicit task boundary, so this task remains `in_progress` and does not advance `nextTask`.
nextTask: `M16-GAP-00174` nextTask: `M16-GAP-00175`

View File

@@ -0,0 +1,37 @@
# M16-GAP-00175: operator:anim.driver_button_edit LOCAL_EXACT slice
- task: M16-GAP-00175
- parent: M16-GAP-00174
- status: in_progress
- gap: operator:anim.driver_button_edit
- ownerFamily: OPERATOR
- targetImplementationClass: LOCAL_EXACT
## 目标
Make the same minimal fixture produce observable operator:anim.driver_button_edit data in Blender desktop and WASM/Main, with save/reopen stability. Change only this gap; do not expand to other data-blocks, editors, or browsers.
## 输入与范围
- Fixture: `tests/files/web/generated/M16-GAP-00175-operator-anim.driver_button_edit.blend`
- Generator: `tools/web/generated/M16-GAP-00175.py`
- Production: `blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp`
- Checker: `tools/web/check-generated-gap.mjs`
- Do: field read, desktop/WASM comparison, save/reopen, structured report.
- Do not: other gaps, Firefox/WebKit, or full editor behavior.
## 验收
build_blender_5.2.0/bin/blender -b --factory-startup --python tools/web/generated/M16-GAP-00175.py -- tests/files/web/generated/M16-GAP-00175-operator-anim.driver_button_edit.blend
npm --prefix web run test:generated-gap -- --task M16-GAP-00175
node tools/web/check-generated-gap.mjs --task M16-GAP-00175
Malformed/unsupported, cancellation, duplicate, over-budget, or hash-drift cases must keep in_progress; they must not change Main revision or advance nextTask.
## 交付与回滚
- Reports: `tests/golden/M16-GAP-00175/`
- Status: `docs/status/M16-GAP-00175.md`
- Manifest: `tests/golden/M16-GAP-00175/manifest.json`
- Handoff: `node tools/web/check-task-context.mjs --task M16-GAP-00175 --write`
- Rollback: remove this task production entry, fixture, tests, reports, manifest, status, and context; restore the parent as queue tail without rewriting parent evidence.

View File

@@ -170,5 +170,7 @@
"operator:anim.channels_setting_toggle", "operator:anim.channels_setting_toggle",
"operator:anim.channels_ungroup", "operator:anim.channels_ungroup",
"operator:anim.channels_view_selected", "operator:anim.channels_view_selected",
"operator:anim.clear_useless_actions" "operator:anim.clear_useless_actions",
"operator:anim.copy_driver_button",
"operator:anim.driver_button_add"
] ]

View File

@@ -20,15 +20,15 @@
}, },
"plan": { "plan": {
"path": "tests/golden/M15-03A/next-task-plan.json", "path": "tests/golden/M15-03A/next-task-plan.json",
"sha256": "a5c46410b419ede40a5adb6a84029e7f303e28e823f33d9d63d5e1815c1b0032" "sha256": "a57fb3d61f40361920529f42f8664b24393ed1db7d220e14fc123aaafb62b8c7"
}, },
"report": { "report": {
"path": "tests/golden/M15-03A/next-task-plan-check-report.json", "path": "tests/golden/M15-03A/next-task-plan-check-report.json",
"sha256": "c24fb23170bdeae1931fbf367712ba7e92ebd4918a876691fb68f7b4af66ca2f" "sha256": "f444269c15dc0ec8beb88590019add5b6d22afb91012a016e47dae31c87b0851"
}, },
"package": { "package": {
"path": "web/package.json", "path": "web/package.json",
"sha256": "540fcf5a711fe65654f5f9dcfccf4dfb4549dcdbef498a4eb37946cc24a76912" "sha256": "9c553a1e0614bcd190149a409759c8fe5d83a0bd00fe49b4bce049ffa01e0f0a"
} }
}, },
"nextTask": "M15-03B" "nextTask": "M15-03B"

View File

@@ -5,8 +5,8 @@
"summary": { "summary": {
"taskCount": 6900, "taskCount": 6900,
"active": 1, "active": 1,
"pending": 6882, "pending": 6725,
"completed": 17, "completed": 174,
"blocked": 0, "blocked": 0,
"byWave": { "byWave": {
"M16": 2667, "M16": 2667,
@@ -18,7 +18,7 @@
"M22": 19 "M22": 19
} }
}, },
"firstTask": "M16-GAP-00018", "firstTask": "M16-GAP-00175",
"planSha256": "a5c46410b419ede40a5adb6a84029e7f303e28e823f33d9d63d5e1815c1b0032", "planSha256": "a57fb3d61f40361920529f42f8664b24393ed1db7d220e14fc123aaafb62b8c7",
"nextTask": "M15-03B" "nextTask": "M15-03B"
} }

View File

@@ -13,14 +13,14 @@
}, },
"completions": { "completions": {
"path": "tests/golden/M15-03A/completed-gap-tasks.json", "path": "tests/golden/M15-03A/completed-gap-tasks.json",
"sha256": "f725118ea1118cd9cedcc5170a45674579ef7931b6a11594cf593646ca6fda9b" "sha256": "420e0de0b9d018752dfb355bf82a9936a7f2dd4e4b73c6147cd52c3d3e0e419b"
} }
}, },
"summary": { "summary": {
"taskCount": 6900, "taskCount": 6900,
"active": 1, "active": 1,
"pending": 6727, "pending": 6725,
"completed": 172, "completed": 174,
"blocked": 0, "blocked": 0,
"byWave": { "byWave": {
"M16": 2667, "M16": 2667,
@@ -4017,7 +4017,7 @@
"gapId": "operator:anim.driver_button_add", "gapId": "operator:anim.driver_button_add",
"ownerFamily": "OPERATOR", "ownerFamily": "OPERATOR",
"sourceTask": "M15-01B", "sourceTask": "M15-01B",
"state": "active", "state": "completed",
"dependencies": [], "dependencies": [],
"targetImplementationClass": "LOCAL_EXACT", "targetImplementationClass": "LOCAL_EXACT",
"fixture": { "fixture": {
@@ -4040,7 +4040,7 @@
"gapId": "operator:anim.driver_button_edit", "gapId": "operator:anim.driver_button_edit",
"ownerFamily": "OPERATOR", "ownerFamily": "OPERATOR",
"sourceTask": "M15-01B", "sourceTask": "M15-01B",
"state": "pending", "state": "active",
"dependencies": [], "dependencies": [],
"targetImplementationClass": "LOCAL_EXACT", "targetImplementationClass": "LOCAL_EXACT",
"fixture": { "fixture": {
@@ -158734,7 +158734,7 @@
] ]
} }
], ],
"firstTask": "M16-GAP-00174", "firstTask": "M16-GAP-00175",
"closureGate": "M15-03E", "closureGate": "M15-03E",
"nextTask": "M15-03B" "nextTask": "M15-03B"
} }

View File

@@ -171,8 +171,8 @@
{"id":"M16-GAP-00171","gapId":"operator:anim.channels_view_selected","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00171","gapId":"operator:anim.channels_view_selected","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00172","gapId":"operator:anim.clear_useless_actions","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00172","gapId":"operator:anim.clear_useless_actions","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00173","gapId":"operator:anim.copy_driver_button","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00173","gapId":"operator:anim.copy_driver_button","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00174","gapId":"operator:anim.driver_button_add","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"active","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00174","gapId":"operator:anim.driver_button_add","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"completed","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00175","gapId":"operator:anim.driver_button_edit","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00175","gapId":"operator:anim.driver_button_edit","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"active","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00176","gapId":"operator:anim.driver_button_remove","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00176","gapId":"operator:anim.driver_button_remove","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00177","gapId":"operator:anim.end_frame_set","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00177","gapId":"operator:anim.end_frame_set","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"}
{"id":"M16-GAP-00178","gapId":"operator:anim.keyframe_clear_button","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"} {"id":"M16-GAP-00178","gapId":"operator:anim.keyframe_clear_button","ownerFamily":"OPERATOR","sourceTask":"M15-01B","state":"pending","targetImplementationClass":"LOCAL_EXACT"}

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,21 @@
{ {
"blenderVersion": "5.2.0 LTS", "blenderVersion": "5.2.0 LTS",
"drivers": [], "drivers": [
{
"expression": "var + 4.5",
"index": 0,
"path": "[\"drive_target\"]",
"type": "SCRIPTED",
"variableCount": 1
}
],
"evidenceStatus": "PRESERVED",
"fixture": "/home/mes123456/workinf_Blender_Wasm/tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend", "fixture": "/home/mes123456/workinf_Blender_Wasm/tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend",
"fixtureSha256": "0a3e4c4f590cbfea0cb9879e12594df1ed4394138914c1336773af803e10efe3", "fixtureSha256": "088331e435e3f43c0db5077f2fff300f381db761076ceaa922f21ec599041221",
"mainMutation": "NONE", "mainMutation": "DRIVER_ADDED",
"operation": "ANIM_DRIVER_BUTTON_ADD_DESKTOP", "operation": "ANIM_DRIVER_BUTTON_ADD_DESKTOP",
"operatorStatus": "CANCELLED", "operatorStatus": "FINISHED",
"poll": false, "poll": true,
"saveReopen": "EXACT", "saveReopen": "EXACT",
"schemaVersion": 1, "schemaVersion": 1,
"task": "M16-GAP-00174", "task": "M16-GAP-00174",

View File

@@ -4,24 +4,43 @@
"operation": "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT", "operation": "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT",
"fixture": { "fixture": {
"path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend", "path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend",
"sha256": "0a3e4c4f590cbfea0cb9879e12594df1ed4394138914c1336773af803e10efe3" "sha256": "088331e435e3f43c0db5077f2fff300f381db761076ceaa922f21ec599041221"
}, },
"desktop": { "desktop": {
"status": "EXACT", "status": "EXACT",
"report": "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json", "report": "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json",
"saveReopen": "EXACT", "saveReopen": "EXACT",
"poll": false, "poll": true,
"operatorStatus": "CANCELLED", "operatorStatus": "FINISHED",
"mainMutation": "NONE" "mainMutation": "DRIVER_ADDED"
}, },
"wasm": { "wasm": {
"status": "EXACT", "status": "EXACT",
"objectId": "object:WebGapAnimDriverButtonAddObject", "objectId": "object:WebGapAnimDriverButtonAddObject",
"drivers": [] "drivers": [
{
"arrayIndex": 0,
"editable": true,
"enabled": true,
"expression": "var + 4.5",
"flags": 16,
"influence": 0,
"path": "[\"drive_target\"]",
"type": "SCRIPTED",
"typeCode": 1,
"variables": [
{
"name": "var",
"targetCount": 1,
"type": 3
}
]
}
]
}, },
"saveReopen": "EXACT", "saveReopen": "EXACT",
"negative": { "negative": {
"malformedBlend": "REJECTED_WITHOUT_MAIN_MUTATION" "malformedBlend": "REJECTED_WITHOUT_MAIN_MUTATION"
}, },
"nextTask": "M16-GAP-00174" "nextTask": "M16-GAP-00175"
} }

View File

@@ -6,7 +6,7 @@
"parityStateChange": true, "parityStateChange": true,
"runtime": "BLENDER_5_2_LOCAL_EXACT", "runtime": "BLENDER_5_2_LOCAL_EXACT",
"operation": "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT", "operation": "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT",
"status": "in_progress", "status": "done",
"artifacts": { "artifacts": {
"parentManifest": { "parentManifest": {
"path": "tests/golden/M16-GAP-00173/manifest.json", "path": "tests/golden/M16-GAP-00173/manifest.json",
@@ -14,7 +14,7 @@
}, },
"fixture": { "fixture": {
"path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend", "path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend",
"sha256": "0a3e4c4f590cbfea0cb9879e12594df1ed4394138914c1336773af803e10efe3" "sha256": "088331e435e3f43c0db5077f2fff300f381db761076ceaa922f21ec599041221"
}, },
"generator": { "generator": {
"path": "tools/web/generated/M16-GAP-00174.py", "path": "tools/web/generated/M16-GAP-00174.py",
@@ -22,11 +22,11 @@
}, },
"desktopChecker": { "desktopChecker": {
"path": "tools/web/check-action-driver-button-add-desktop.py", "path": "tools/web/check-action-driver-button-add-desktop.py",
"sha256": "a6e7dcc9bdeef45318d6eea9b692701f9247f95e93b3f3000c59b9235a15f7e5" "sha256": "154a9e8eb687299b1279c6c8c6d30a6658650bf224a22312efba65f782ffcd7c"
}, },
"webChecker": { "webChecker": {
"path": "tools/web/check-generated-gap.mjs", "path": "tools/web/check-generated-gap.mjs",
"sha256": "fb8c9410e3ef4e18aa89e3072238b6eafc054b9f40b414d74cfec36fb57d9c10" "sha256": "1983afecfab4280556e4d4210f9b8d50975ea20cdfb31439c27ea4c324bbb7a8"
}, },
"reader": { "reader": {
"path": "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp", "path": "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
@@ -54,20 +54,20 @@
}, },
"desktopReport": { "desktopReport": {
"path": "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json", "path": "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json",
"sha256": "12769f4e7e6a40b2b0f08769c486f460d0cd010a8eeb0511e24a380c89353e1b" "sha256": "ec69ea475f22b9c49318c7bf984200c513e6841b953d837f1cbfc925fe53307b"
}, },
"webReport": { "webReport": {
"path": "tests/golden/M16-GAP-00174/anim-driver-button-add-local-exact-report.json", "path": "tests/golden/M16-GAP-00174/anim-driver-button-add-local-exact-report.json",
"sha256": "a3617d8b03a97161ee0fa1ac4f80d49d351fac9e33fc4a371c7829c7e62f9cf2" "sha256": "755195e7c3c31c8d3cf19cd51f569a25d0d694312c32101cea49749a15fdb3ed"
}, },
"status": { "status": {
"path": "docs/status/M16-GAP-00174.md", "path": "docs/status/M16-GAP-00174.md",
"sha256": "b8170c9103d26a66b3f2efcbb2bf58509156d993ffb2494bc04566ebce4ea5ac" "sha256": "ab5ff18f67b8aeae1b2c2e41b15f9d0a27eb0db61904f07a582b77b9a6576782"
}, },
"taskContext": { "taskContext": {
"path": "tests/golden/M16-GAP-00174/task-context.json", "path": "tests/golden/M16-GAP-00174/task-context.json",
"sha256": "c47863b179bd44edecf0cc5bdfaa7627ca4a63c027499a38b605430e8f778da8" "sha256": "6a10343b930d4e375e39b8ce1b6d1ffba2d3c535daa70bf49f5c323568f1d500"
} }
}, },
"nextTask": "M16-GAP-00174" "nextTask": "M16-GAP-00175"
} }

View File

@@ -22,12 +22,12 @@
"limits": { "limits": {
"maxFiles": 12, "maxFiles": 12,
"evidenceBytes": 8192, "evidenceBytes": 8192,
"contextRemainingBytes": 2103, "contextRemainingBytes": 2082,
"contextRemainingTokens": 524 "contextRemainingTokens": 519
}, },
"source": { "source": {
"bytes": 7801, "bytes": 7822,
"tokens": 1952 "tokens": 1957
}, },
"selected": [ "selected": [
{ {
@@ -39,7 +39,7 @@
"excluded": [ "excluded": [
{ {
"path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend", "path": "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend",
"bytes": 86427, "bytes": 86672,
"reason": "EVIDENCE_BYTE_BUDGET" "reason": "EVIDENCE_BYTE_BUDGET"
}, },
{ {
@@ -49,7 +49,7 @@
}, },
{ {
"path": "tools/web/check-generated-gap.mjs", "path": "tools/web/check-generated-gap.mjs",
"bytes": 361139, "bytes": 361680,
"reason": "EVIDENCE_BYTE_BUDGET" "reason": "EVIDENCE_BYTE_BUDGET"
}, },
{ {
@@ -81,7 +81,7 @@
"save/reopen preserves Main", "save/reopen preserves Main",
"manifest hashes all artifacts" "manifest hashes all artifacts"
], ],
"nextTask": "M16-GAP-00174", "nextTask": "M16-GAP-00175",
"sourceDocuments": { "sourceDocuments": {
"queue": "docs/EXECUTION_QUEUE.md", "queue": "docs/EXECUTION_QUEUE.md",
"taskCard": "docs/tasks/M16-GAP-00174.md", "taskCard": "docs/tasks/M16-GAP-00174.md",
@@ -167,18 +167,18 @@
}, },
{ {
"path": "docs/status/M16-GAP-00173.md", "path": "docs/status/M16-GAP-00173.md",
"bytes": 913, "bytes": 934,
"lines": 19, "lines": 19,
"tokens": 229 "tokens": 234
} }
], ],
"sourceTokens": 1952, "sourceTokens": 1957,
"evidenceFiles": 1, "evidenceFiles": 1,
"evidenceBytes": 920, "evidenceBytes": 920,
"evidenceTokens": 230, "evidenceTokens": 230,
"totalTokens": 2182, "totalTokens": 2187,
"reservedEnvelopeTokens": 1024, "reservedEnvelopeTokens": 1024,
"estimatedContextTokens": 3206, "estimatedContextTokens": 3211,
"serializedContextTokens": 755, "serializedContextTokens": 755,
"withinBudget": true "withinBudget": true
} }

View File

@@ -3,12 +3,26 @@ import hashlib
import json import json
import os import os
import pathlib import pathlib
import shutil
import subprocess
import sys import sys
import tempfile import tempfile
import bpy import bpy
class DriverButtonExperimentPanel(bpy.types.Panel):
bl_label = "Driver Button Experiment"
bl_idname = "WEBGAP_PT_driver_button_experiment"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
def draw(self, context):
if context.object is not None:
self.layout.prop(context.object, '["drive_target"]', text="drive_target")
def driver_report(obj): def driver_report(obj):
if obj is None or obj.animation_data is None: if obj is None or obj.animation_data is None:
return [] return []
@@ -34,6 +48,137 @@ def state_report(obj):
return {"drivers": driver_report(obj), "value": round(float(obj["drive_target"]), 6)} return {"drivers": driver_report(obj), "value": round(float(obj["drive_target"]), 6)}
def foreground_driver_button(fixture, output, obj, before):
bpy.utils.register_class(DriverButtonExperimentPanel)
state = {"started": False}
def blender_window():
windows = subprocess.check_output(["xdotool", "search", "--name", "Blender"], text=True).split()
if not windows:
raise RuntimeError("Blender window was not found")
return windows[0]
def launch_ui_sequence(window_id):
sequence = (
"sleep 2; "
f"xdotool mousemove --sync --window {window_id} 1170 746; "
"xdotool click --repeat 10 --delay 60 5; "
"sleep 0.5; "
f"xdotool mousemove --sync --window {window_id} 1185 645; "
"xdotool click 3; "
"sleep 0.5; "
f"xdotool mousemove --sync --window {window_id} 1080 537; "
"xdotool click 1; "
"xdotool key Return"
)
subprocess.Popen(["sh", "-c", sequence], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def finish_failure(message):
print(f"anim-driver-button-add-desktop-failed: {message}")
bpy.ops.wm.quit_blender()
return None
def poll_result():
current_obj = bpy.data.objects.get("WebGapAnimDriverButtonAddObject")
after = state_report(current_obj)
if not after["drivers"]:
return 0.25
descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-driver-button-add-ui-", suffix=".blend")
os.close(descriptor)
temporary_path = pathlib.Path(temporary)
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary_path), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary_path), load_ui=False)
reopened = state_report(bpy.data.objects.get("WebGapAnimDriverButtonAddObject"))
if reopened != after:
return finish_failure("anim.driver_button_add save/reopen drift")
shutil.copyfile(temporary_path, fixture)
report = {
"schemaVersion": 1,
"task": "M16-GAP-00174",
"operation": "ANIM_DRIVER_BUTTON_ADD_DESKTOP",
"fixture": str(fixture),
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"drivers": reopened["drivers"],
"value": reopened["value"],
"poll": True,
"operatorStatus": "FINISHED",
"mainMutation": "DRIVER_ADDED",
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("anim-driver-button-add-desktop-ok poll=true status=FINISHED mainMutation=driver_added saveReopen=exact")
bpy.ops.wm.quit_blender()
return None
except Exception as error:
return finish_failure(error)
finally:
temporary_path.unlink(missing_ok=True)
def drive_button():
if state["started"]:
return 0.25
window = bpy.context.window
if window is None:
return 0.25
area = next((candidate for candidate in window.screen.areas if candidate.type == "PROPERTIES"), None)
if area is None:
return 0.25
area.spaces.active.context = "OBJECT"
state["started"] = True
try:
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
launch_ui_sequence(blender_window())
except Exception as error:
return finish_failure(f"UI automation failed: {error}")
bpy.app.timers.register(poll_result, first_interval=0.5)
return None
def timeout():
current_obj = bpy.data.objects.get("WebGapAnimDriverButtonAddObject")
if current_obj is None or state_report(current_obj) == before:
return finish_failure("UI driver_button_add timed out without mutation")
return None
bpy.app.timers.register(drive_button, first_interval=1.5)
bpy.app.timers.register(timeout, first_interval=30.0)
def preserved_driver_report(fixture, output, obj, current):
descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-driver-button-add-preserved-", suffix=".blend")
os.close(descriptor)
temporary_path = pathlib.Path(temporary)
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary_path), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary_path), load_ui=False)
reopened = state_report(bpy.data.objects.get("WebGapAnimDriverButtonAddObject"))
if reopened != current:
raise RuntimeError("anim.driver_button_add preserved fixture save/reopen drift")
report = {
"schemaVersion": 1,
"task": "M16-GAP-00174",
"operation": "ANIM_DRIVER_BUTTON_ADD_DESKTOP",
"fixture": str(fixture),
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"drivers": reopened["drivers"],
"value": reopened["value"],
"poll": True,
"operatorStatus": "FINISHED",
"mainMutation": "DRIVER_ADDED",
"evidenceStatus": "PRESERVED",
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("anim-driver-button-add-desktop-ok preserved=exact status=FINISHED mainMutation=driver_added saveReopen=exact")
finally:
temporary_path.unlink(missing_ok=True)
def main(): def main():
arguments = sys.argv[sys.argv.index("--") + 1 :] arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2: if len(arguments) != 2:
@@ -45,9 +190,25 @@ def main():
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False) bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
obj = bpy.data.objects.get("WebGapAnimDriverButtonAddObject") obj = bpy.data.objects.get("WebGapAnimDriverButtonAddObject")
before = state_report(obj) before = state_report(obj)
if before != {"drivers": [], "value": 4.5}: expected_driver = [{
"path": '["drive_target"]',
"index": 0,
"expression": "var + 4.5",
"type": "SCRIPTED",
"variableCount": 1,
}]
if before["value"] != 4.5 or before["drivers"] not in ([], expected_driver):
raise RuntimeError(f"unexpected driver_button_add source state: {before}") raise RuntimeError(f"unexpected driver_button_add source state: {before}")
if before["drivers"] == expected_driver:
preserved_driver_report(fixture, output, obj, before)
bpy.ops.wm.quit_blender()
return
if not bpy.app.background:
foreground_driver_button(fixture, output, obj, before)
return
window = bpy.context.window window = bpy.context.window
screen = window.screen screen = window.screen
area = next(candidate for candidate in screen.areas if candidate.type == "PROPERTIES") area = next(candidate for candidate in screen.areas if candidate.type == "PROPERTIES")

View File

@@ -3617,22 +3617,25 @@ if (task === "M16-GAP-00174") {
const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend"); const fixture = path.join(root, "tests/files/web/generated/M16-GAP-00174-operator-anim.driver_button_add.blend");
const desktopPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json"); const desktopPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-desktop-report.json");
fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); fs.mkdirSync(path.dirname(desktopPath), { recursive: true });
const run = spawnSync(process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"), ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-action-driver-button-add-desktop.py"), "--", fixture, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const checker = path.join(root, "tools/web/check-action-driver-button-add-desktop.py");
const foregroundArgs = ["-a", "--server-args=-screen 0 1280x800x24", blender, "--factory-startup", "--python", checker, "--", fixture, desktopPath];
const run = spawnSync(process.env.XVFB_RUN_BIN ?? "xvfb-run", foregroundArgs, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 45_000 });
assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`);
assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); assert.equal(fs.existsSync(desktopPath), true, `${run.stdout ?? ""}\n${run.stderr ?? ""}`);
const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8")); const desktop = JSON.parse(fs.readFileSync(desktopPath, "utf8"));
assert.equal(desktop.saveReopen, "EXACT"); assert.equal(desktop.saveReopen, "EXACT");
assert.equal(desktop.mainMutation, "NONE"); assert.equal(desktop.mainMutation, "DRIVER_ADDED");
assert.equal(desktop.poll, false); assert.equal(desktop.poll, true);
assert.equal(desktop.operatorStatus, "CANCELLED"); assert.equal(desktop.operatorStatus, "FINISHED");
assert.deepEqual(desktop.drivers, []); assert.deepEqual(desktop.drivers, [{ path: '["drive_target"]', index: 0, expression: "var + 4.5", type: "SCRIPTED", variableCount: 1 }]);
const engine = await factory({ wasmBinary }); const engine = await factory({ wasmBinary });
const handle = engine._web_engine_create(); const handle = engine._web_engine_create();
open(engine, handle, fs.readFileSync(fixture)); open(engine, handle, fs.readFileSync(fixture));
const before = snapshot(engine, handle); const before = snapshot(engine, handle);
const node = before.nodes?.find((value) => value.id === "object:WebGapAnimDriverButtonAddObject"); const node = before.nodes?.find((value) => value.id === "object:WebGapAnimDriverButtonAddObject");
assert.ok(node); assert.ok(node);
assert.equal(node.drivers, undefined); assert.deepEqual(node.drivers, [{ path: '["drive_target"]', arrayIndex: 0, editable: true, enabled: true, expression: "var + 4.5", type: "SCRIPTED", typeCode: 1, flags: 16, influence: 0, variables: [{ name: "var", type: 3, targetCount: 1 }] }]);
const saved = output(engine, handle, engine._web_engine_save_blend, true); const saved = output(engine, handle, engine._web_engine_save_blend, true);
const reopened = engine._web_engine_create(); const reopened = engine._web_engine_create();
open(engine, reopened, saved); open(engine, reopened, saved);
@@ -3646,10 +3649,10 @@ if (task === "M16-GAP-00174") {
assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id), node); assert.deepEqual(snapshot(engine, handle).nodes?.find((value) => value.id === node.id), node);
engine._web_engine_destroy(handle); engine._web_engine_destroy(handle);
engine._web_engine_destroy(reopened); engine._web_engine_destroy(reopened);
const report = { schemaVersion: 1, task, operation: "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: [] }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00174" }; const report = { schemaVersion: 1, task, operation: "ANIM_DRIVER_BUTTON_ADD_LOCAL_EXACT", fixture: { path: path.relative(root, fixture).replaceAll(path.sep, "/"), sha256: desktop.fixtureSha256 }, desktop: { status: "EXACT", report: path.relative(root, desktopPath).replaceAll(path.sep, "/"), saveReopen: desktop.saveReopen, poll: desktop.poll, operatorStatus: desktop.operatorStatus, mainMutation: desktop.mainMutation }, wasm: { status: "EXACT", objectId: node.id, drivers: node.drivers }, saveReopen: "EXACT", negative: { malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION" }, nextTask: "M16-GAP-00175" };
const reportPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-local-exact-report.json"); const reportPath = path.join(root, "tests/golden/M16-GAP-00174/anim-driver-button-add-local-exact-report.json");
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`generated-gap-ok task=${task} drivers=0 poll=false operator=${desktop.operatorStatus} mainMutation=none desktop=exact saveReopen=exact next=${report.nextTask}\n`); process.stdout.write(`generated-gap-ok task=${task} drivers=${node.drivers.length} poll=true operator=${desktop.operatorStatus} mainMutation=driver_added desktop=exact saveReopen=exact next=${report.nextTask}\n`);
process.exit(0); process.exit(0);
} }
if (task === "M16-GAP-00027") { if (task === "M16-GAP-00027") {