news 2026/6/10 19:24:04

poll() Missing ‘window‘ in context File解决办法 blender插件开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
poll() Missing ‘window‘ in context File解决办法 blender插件开发

报错1:'Context' object has no attribute 'selected_objects'

报错来源:bpy.ops.mmd_tools.import_model(filepath=filepath)

报错2:poll() Missing 'window' in context File

报错来源:bpy.ops.mmd_tools.import_model('INVOKE_DEFAULT')

这两种报错都能解决

执行成功:

问题原因 bpy.ops.mmd_tools.import_model 的 poll() 方法要求上下文中存在 window,但你的 MCP 工具运行时上下文缺少 window,导致操作符无法执行 info_quickstart.rst:256-268 。这通常发生在后台线程或无头环境调用操作符时。 解决方案 1 使用上下文覆盖(推荐) 在调用操作符前用 temp_override 确保有 window、screen、area 等必要上下文成员 bl_run_operators.py:55-79 。 import bpy from bpy.context import temp_override @Blendermcp.tool() def import_pmx_file(filepath: str = ""): window = bpy.context.window_manager.windows[0] screen = window.screen area = next((a for a in screen.areas if a.type == 'VIEW_3D'), None) region = area.regions[-1] if area else None with temp_override(window=window, screen=screen, area=area, region=region): bpy.ops.mmd_tools.import_model(filepath=filepath) # 改用 EXEC_DEFAULT 并传路径 return f"PMX文件导入成功: {os.path.basename(filepath)}" 2 改用 EXEC_DEFAULT 并传递文件路径 INVOKE_DEFAULT 常用于需要交互(如文件浏览器)的操作符。在后台应使用 EXEC_DEFAULT 并直接传入 filepath 参数 WM_api.hh:1081-1085 。 3 确保在主线程执行 Blender 的操作符必须在主线程运行。你的 MCP 服务器若在后台线程运行,需将操作符调用调度到主线程(例如通过 bpy.app.timers.register)。 4 检查 mmd_tools 插件 确保 mmd_tools 插件已启用,且其 import_model 操作符的 poll 方法仅检查 window 存在,而非其他特定区域类型。 为什么需要 window Blender 的操作符 poll 方法常检查上下文是否在有效窗口中,以避免在无 UI 环境执行需要交互的操作 info_quickstart.rst:256-268 。上下文覆盖可临时提供这些成员 bl_run_operators.py:55-79 。 Notes 如果当前没有窗口(如纯后台模式),可创建一个临时窗口或使用 bpy.ops.wm.read_factory_settings() 初始化基础窗口 bl_run_operators.py:40-49 。 你的其他工具(如 delete_all_objects)同样依赖上下文,建议统一使用 temp_override 包装操作符调用。 错误堆栈显示调用来自 blender_api.py 的 _blender_call,确保该函数在主线程执行或通过定时器调度到主线程。

将操作符调用移到主线程执行,使用bpy.app.timers调度,并通过线程安全队列传递结果。以下是修改后的完整代码:

Search | DeepWiki

"""Blender MCP Server Plugin Blender MCP服务器插件,允许通过MCP协议安全地控制Blender """ import bpy import sys import os from bpy.props import IntProperty, StringProperty, BoolProperty from bpy.types import AddonPreferences, Operator, Panel import subprocess import importlib import threading import queue import time from typing import Callable, Any, Dict # 将当前目录添加到Python路径 sys.path.append(os.path.dirname(os.path.abspath(__file__))) from fastmcp import FastMCP # 初始化MCP服务器 Blendermcp = FastMCP("BlenderMCP") # 用于主线程与后台线程通信的队列(工具名 -> (event, result)) _timer_results: Dict[str, Any] = {} _timer_lock = threading.Lock() # 服务器线程变量 server_thread = None def _run_in_main_thread(tool_name: str, func: Callable[[], Any]) -> Any: """将函数调度到主线程执行并等待结果""" event = threading.Event() result_container = {} def timer_func(): try: result_container['value'] = func() except Exception as e: result_container['error'] = str(e) finally: with _timer_lock: _timer_results[tool_name] = (event, result_container) event.set() return None # 不重复执行 # 注册定时器(主线程执行) bpy.app.timers.register(timer_func) # 等待主线程执行完成 event.wait() # 清理并返回结果 with _timer_lock: _timer_results.pop(tool_name, None) if 'error' in result_container: raise RuntimeError(result_container['error']) return result_container['value'] @Blendermcp.tool() def import_pmx_file(filepath: str = ""): """导入PMX文件""" def _do_import(): # 获取窗口、屏幕、区域、上下文覆盖 window = bpy.context.window_manager.windows[0] screen = window.screen area = next((a for a in screen.areas if a.type == 'VIEW_3D'), None) region = area.regions[-1] if area else None with bpy.context.temp_override(window=window, screen=screen, area=area, region=region): # 使用 EXEC_DEFAULT 并传入文件路径 bpy.ops.mmd_tools.import_model('INVOKE_DEFAULT') return f"PMX文件导入成功: {os.path.basename(filepath)}" return _run_in_main_thread('import_pmx_file', _do_import) @Blendermcp.tool() def fix_model(): """执行 Fix Model 操作""" def _do_fix(): bpy.ops.cats_armature.fix() return "模型修复操作执行成功" return _run_in_main_thread('fix_model', _do_fix) @Blendermcp.tool() def delete_all_objects(): """删除所有对象和集合""" def _do_delete(): bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() # 删除所有集合(非操作符,可在后台线程执行) for collection in bpy.data.collections: bpy.data.collections.remove(collection) return "所有对象和集合已删除" return _run_in_main_thread('delete_all_objects', _do_delete) @Blendermcp.tool() def delete_objects_by_name(name_pattern: str): """删除名称包含指定模式的物体""" objects_to_delete = [] for obj in bpy.context.scene.objects: if name_pattern in obj.name and obj.type == 'MESH': objects_to_delete.append(obj) deleted_count = 0 for obj in objects_to_delete: bpy.data.objects.remove(obj, do_unlink=True) deleted_count += 1 return f"已删除 {deleted_count} 个包含 '{name_pattern}' 的物体" @Blendermcp.tool() def delete_object(object_name: str): """删除指定名称的物体""" def _do_delete(): if object_name in bpy.context.scene.objects: obj = bpy.context.scene.objects[object_name] bpy.context.view_layer.objects.active = obj bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.ops.object.delete() return f"已删除物体: {object_name}" else: return f"物体 '{object_name}' 不存在" return _run_in_main_thread('delete_object', _do_delete) @Blendermcp.tool() def parent_object_to_armature(): """将选中的对象设置为骨骼绑定父级""" def _do_parent(): if bpy.context.selected_objects: bpy.ops.object.parent_set(type='ARMATURE_NAME') return "对象已设置为骨骼绑定父级" else: return "错误: 请先选择要绑定的对象" return _run_in_main_thread('parent_object_to_armature', _do_parent) @Blendermcp.tool() def clear_parent_keep_transform(): """清除选中对象的父级关系,但保持变换(位置、旋转、缩放)""" def _do_clear(): if bpy.context.selected_objects: bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') return "已清除选中对象的父级关系(保持变换)" else: return "错误: 请先选择要清除父级的对象" return _run_in_main_thread('clear_parent_keep_transform', _do_clear) @Blendermcp.tool() def set_parent_bone(object_name: str): """将选中的对象设置为骨骼绑定父级""" def _do_set(): if bpy.context.selected_objects and bpy.context.active_object: bpy.ops.object.parent_set(type='BONE') return "对象已设置为骨骼父级" else: return "错误: 请先选择要绑定的对象并确保有一个活动对象" return _run_in_main_thread('set_parent_bone', _do_set) @Blendermcp.tool() def switch_pose_mode(object_name: str): """切换到姿态模式并应用选中的骨架,自动查找名称包含ObjectName的物体""" def _do_switch(): found_object = None for obj in bpy.context.scene.objects: if object_name in obj.name: found_object = obj break if found_object is None: return f"错误: 没有找到名称包含'{object_name}'的物体" bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = found_object found_object.select_set(True) bpy.ops.object.posemode_toggle() bpy.ops.pose.armature_apply(selected=True) bpy.ops.object.posemode_toggle() return f"已对物体 '{found_object.name}' 应用骨架姿态" return _run_in_main_thread('switch_pose_mode', _do_switch) @Blendermcp.tool() def apply_armature_pose(): """切换到姿态模式并应用选中的骨架""" def _do_apply(): found_object = None for obj in bpy.context.scene.objects: if "ObjectName" in obj.name: found_object = obj break if found_object is None: return "错误: 没有找到名称包含'ObjectName'的物体" bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = found_object found_object.select_set(True) bpy.ops.object.posemode_toggle() bpy.ops.pose.armature_apply(selected=True) bpy.ops.object.posemode_toggle() return f"已对物体 '{found_object.name}' 应用骨架姿态" return _run_in_main_thread('apply_armature_pose', _do_apply) @Blendermcp.tool() def set_blender_scale_settings(): """设置Blender场景的单位比例和当前对象的缩放""" bpy.context.scene.unit_settings.scale_length = 0.01 if bpy.context.object: bpy.context.object.scale[0] = 100 bpy.context.object.scale[1] = 100 bpy.context.object.scale[2] = 100 try: for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': for space in area.spaces: if space.type == 'VIEW_3D': space.clip_end = 300000 break except Exception as e: print(f"设置3D视口裁剪平面时出错: {e}") return "Blender缩放设置已应用" @Blendermcp.tool() def scale_the_objectname(): """将ObjectName物体及其姿势缩放到与其他非ObjectName物体相同的尺寸""" import mathutils def get_object_dimensions(obj): if obj.type == 'MESH': bbox = obj.bound_box if bbox: world_bbox = [obj.matrix_world @ mathutils.Vector(co) for co in bbox] x_coords = [v.x for v in world_bbox] y_coords = [v.y for v in world_bbox] z_coords = [v.z for v in world_bbox] x_size = max(x_coords) - min(x_coords) y_size = max(y_coords) - min(y_coords) z_size = max(z_coords) - min(z_coords) return (x_size, y_size, z_size) return (0.0, 0.0, 0.0) objectname_dimensions = None non_objectname_dimensions = None for obj in bpy.context.scene.objects: if obj.type in ['MESH']: if 'ObjectName' in obj.name: objectname_dimensions = get_object_dimensions(obj) else: non_objectname_dimensions = get_object_dimensions(obj) if objectname_dimensions and non_objectname_dimensions: scale_factor_x = non_objectname_dimensions[0] / objectname_dimensions[0] scale_factor_y = non_objectname_dimensions[1] / objectname_dimensions[1] scale_factor_z = non_objectname_dimensions[2] / objectname_dimensions[2] for obj in bpy.context.scene.objects: if 'ObjectName' in obj.name: obj.scale[0] *= scale_factor_x obj.scale[1] *= scale_factor_y obj.scale[2] *= scale_factor_z return f"所有ObjectName物体已缩放至目标尺寸: ({round(non_objectname_dimensions[0], 6)}, {round(non_objectname_dimensions[1], 6)}, {round(non_objectname_dimensions[2], 6)})" else: return "错误: 未找到ObjectName或非ObjectName物体" @Blendermcp.tool() def import_psk_file(): """导入PSK文件(外部选择文件路径)""" try: import bpy from io_scene_psk_psa.psk.reader import read_psk from io_scene_psk_psa.psk.importer import import_psk, PskImportOptions options = PskImportOptions() options.name = 'ObjectName' options.should_import_mesh = True options.should_import_skeleton = True options.scale = 1.0 psk = read_psk('E:/blender/SK_W_MainChar_01.psk') result = import_psk(psk, bpy.context, options) return f"PSK文件导入成功: {result}" except Exception as e: return f"PSK文件导入失败: {str(e)}" # 插件UI和启动逻辑保持不变 bl_info = { "name": "Blender MCP Server", "author": "Your Name", "version": (1, 0, 0), "blender": (2, 80, 0), "location": "View3D > Sidebar > MCP Server", "description": "提供MCP协议接口来安全控制Blender", "warning": "", "doc_url": "", "category": "Development", } class BLENDER_MCP_PT_server_panel(Panel): """MCP服务器控制面板""" bl_label = "MCP Server" bl_idname = "BLENDER_MCP_PT_server_panel" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'MCP Server' def draw(self, context): layout = self.layout scene = context.scene col = layout.column() col.label(text="MCP Server Status: Running", icon='REC') col.separator() col.label(text="Server Info:") col.label(text="Protocol: MCP") col.label(text="Port: 3000") col.label(text="Auto-started on plugin registration") col.label(text="Available Tools:") col.label(text=" - import_pmx_file") col.label(text=" - fix_model") col.label(text=" - delete_all_objects") col.label(text=" - delete_objects_by_name") col.label(text=" - many more...") def run_mcp_server(): """运行MCP服务器""" try: Blendermcp.run(transport="http", host="0.0.0.0", port=3000) except Exception as e: print(f"MCP Server Error: {e}") def register(): """注册插件""" bpy.utils.register_class(BLENDER_MCP_PT_server_panel) # 注册后立即启动MCP服务器 global server_thread import threading # 创建并启动服务器线程 server_thread = threading.Thread(target=run_mcp_server, daemon=True) server_thread.start() print("MCP Server started automatically on plugin registration") def unregister(): """注销插件""" global server_thread # 如果服务器正在运行,尝试停止它 if server_thread and server_thread.is_alive(): print("Stopping MCP Server...") # 可根据需要添加适当的关闭逻辑 bpy.utils.unregister_class(BLENDER_MCP_PT_server_panel) if __name__ == "__main__": register()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 3:44:43

大模型推理,不再是“一根筋”

没有人不在期待大模型能够成为下一个电动车,作为代表中国的新兴产业,在世界范围内掀起狂澜。 然而主流的MoE架构大模型,却苦于其结构上的“先天不足”:巨大的硬件成本与多重拖累效率的环节,使得中国企业在这场芯片堆砌…

作者头像 李华
网站建设 2026/6/10 2:04:15

HarmonyOS 从移动到 PC,难点在哪里

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名) 大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚…

作者头像 李华
网站建设 2026/6/10 15:59:18

西门子6FC5447-0AA10-0AA0数控软件

西门子6FC5447-0AA10-0AA0作为SINUMERIK数控系统的核心组件,专为工业自动化领域的高精度机床控制设计,其性能特点与应用范围体现了西门子在数字化制造中的技术领先地位。该软件通过模块化架构与智能算法,实现了从单机控制到系统集成的全流程优…

作者头像 李华
网站建设 2026/6/10 18:01:28

ASP.NET环境下如何实现大文件断点续传上传功能?

2023年XX月XX日 外包项目攻坚日志 - 20G级文件传输系统开发实录 (关键词:信创环境兼容/海量文件存储/企业级断点续传/简历镀金项目) 凌晨3点:需求风暴会议复盘 客户作为省级档案数字化服务商,提出的变态需求&#xff…

作者头像 李华
网站建设 2026/6/10 10:56:56

金融终端如何用wangEditor插件实现Excel动态图表Web渲染?

Word图片一键转存功能开发全记录 技术调研与选型 作为项目前端负责人,我近期专注于解决Word文档粘贴到wangEditor时图片自动转存的问题。经过对同类方案的对比分析,确定以下技术路线: 前端技术栈 Vue2.6.14 wangEditor 4.7.15Axios 0.21…

作者头像 李华
网站建设 2026/6/10 1:23:36

Kafka从入门到上天系列第五篇:一文吃透ZooKeeper的ZNode:定义、物理形态、作用及Watch机制详解

在ZooKeeper(简称ZK)的学习和使用中,ZNode是最基础也是最核心的概念——无论是分布式协调、服务注册发现,还是分布式锁实现,都离不开ZNode。 本文将用最直白、不绕弯的语言,结合实例和流程图,一次性讲清ZNode的核心知识点,涵盖定义、物理形态、作用、Watch机制、节点类…

作者头像 李华