Keep the interface in sync with data
Declare readable state and fields, query or subscribe as needed, and notify listeners when data changes.
Declare fields and provide a query
State represents current facts. Define the query in a bpy-importing module and register once after binding. It returns scene name/frame without modifying the scene.
def _scene_snapshot(query, context):
scene = bpy.context.scene
return {"name": scene.name, "frame": scene.frame_current}Add this after bind succeeds in _get_plugin, not at module top level.
_plugin.bridge().state(
"plugin.my_addon.scene", _scene_snapshot, shape="item",
fields={"name": {"type": "string"}, "frame": {"type": "integer"}},
)Declare each returned field's type. Use list/item shapes with matching return structures, not merely a changed shape flag.
Read once or subscribe
async function readScene(output) {
try {
const { snapshot } = await window.acquireNexusWebUI().state.get(
"plugin.my_addon.scene", null
);
output.textContent = snapshot?.item
? `${snapshot.item.name} · 第 ${snapshot.item.frame} 帧` : "暂无数据";
} catch (error) {
output.textContent = `读取失败:${error.message || String(error)}`;
}
}output is an existing message element. Call readScene(output) from a button or initialization. Read snapshot.item for single state and snapshot.items for lists. get returns a wrapper containing snapshot, not raw application data.
Subscribe on mount and release on exit
Call mountScene(output) from your component, retain its synchronously returned cleanup function, and await it on unmount, using existing error handling for failures. It covers unmount before subscription creation completes.
function mountScene(output) {
const webui = window.acquireNexusWebUI();
let active = true;
const pending = webui.state.subscribe("plugin.my_addon.scene", null, (snapshot, meta) => {
if (!active) return;
if (meta.error) {
output.textContent = `订阅读取失败:${meta.error.message}`;
return;
}
output.textContent = snapshot?.item
? `${snapshot.item.name} · 第 ${snapshot.item.frame} 帧` : "暂无数据";
});
pending.catch((error) => {
if (active) output.textContent = `订阅读取失败:${error.message || String(error)}`;
});
return async function unmountScene() {
active = false;
const handle = await pending;
await webui.state.unsubscribe(handle);
};
}Persistent panels use subscribe(key, query, callback); callbacks receive snapshots directly. Save the awaited handle and await webui.state.unsubscribe(handle) when no longer needed.
Creation is asynchronous. Release late handles after unmount to avoid accumulated listeners. Dispose the shared SDK only when the whole page ends. For query arguments/results, see State API reference.
Why data changed but the page did not
_plugin.bridge().state.invalidate("plugin.my_addon.scene")The call above requests a reread. For timeline changes, notify from your existing frame-change callback and remove it on unload. Registering a query does not observe Blender automatically. Page actions can also declare invalidates.
Query lists according to need
Custom plugin.* queries accept fields, filter, sort, and limit, not cursor/offset/page. Declare allowed fields and limits; Python handlers perform filtering/sorting while the framework validates, projects, and truncates.
Lists may be truncated; returned count is not total count. Generic cursor fields apply only to states explicitly supporting them. See State declarations and query scopes.
Return declared fields only and choose sensitive/large data deliberately. Do not expose every Blender internal. On resync_required, synchronize before deciding to retry rather than blindly repeating writes.
Check both directions
- Initial snapshots show real data.
- Page actions update associated state.
- Blender-native changes update through notifications too.
- Reopening does not duplicate callbacks, and invalid targets give clear results.