当前位置: 首页 > news >正文

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()
http://www.jsqmd.com/news/366627/

相关文章:

  • 2026 南通英语雅思培训教育机构推荐;雅思培训课程中心权威口碑榜单 - 老周说教育
  • 应用日志收集与 logrotate 切割配置
  • 大模型推理,不再是“一根筋”
  • 2026 南昌英语雅思培训教育机构推荐:雅思培训课程中心权威口碑榜单 - 老周说教育
  • 推理性能PK,华为+DeepSeek>英伟达?
  • 想让AI更智能?收藏这篇,小白也能学会调用工具的Agent智能体!
  • 2026 厦门英语雅思培训教育机构推荐:雅思培训课程中心权威口碑榜单 - 老周说教育
  • (2026年最新)AI大模型学习路线图详解:从入门到精通,你的完整学习指南!_大模型学习路线
  • 【课程设计/毕业设计】基于SpringBoot框架的食品安全服务系统基于springboot的食品安全监测及风险预警系统的设计与实现【附源码、数据库、万字文档】
  • 2026年IT圈最火话题:AI智能体与鸿蒙生态大爆发!你抓住机会了吗?
  • LLM 联网搜索,到底是咋回事?
  • 编写摄影交友APP,根据用户摄影水平,摄影类型(风景,人物,美食),匹配同城摄影爱好者,推荐摄影地点,活动,共享摄影作品,技巧,提升摄影能力。
  • 2026陕西医院标识设计行业指南:核心要素解析与Top5企业榜单 - 深度智识库
  • 2026 台州英语雅思培训教育机构推荐/雅思培训课程中心权威口碑榜单 - 老周说教育
  • 三重for循环应用
  • 实现PC与使用EtherNet/IP协议的PLC通信
  • Cirrus项目
  • 保姆级教程 | 人工智能应用开发平台 Coze
  • 2026 厦门英语雅思培训教育机构推荐,雅思培训课程中心权威口碑榜单 - 老周说教育
  • 2026 南通英语雅思培训教育机构推荐,雅思培训课程中心权威口碑榜单 - 老周说教育
  • 2026最新PE管厂家最新推荐,实力品牌深度解析采购无忧之选 - 深度智识库
  • 2026 厦门英语雅思培训教育机构推荐;雅思培训课程中心权威口碑榜单 - 老周说教育
  • 前端安全防护方案
  • 抢占洛阳本地流量新高地!专业团购代运营赋能商户全域增长 - 野榜数据排行
  • 范式重构:当编程变成“意图”——黄仁勋眼中的AI工厂与智能体未来
  • (12)GetPlayerPawn(..) 与 GetPlayerCharacter(..) 这俩函数啥区别 ?
  • 并网式+分布式全覆盖!2026光伏气象站厂家实力TOP5揭晓 - 品牌推荐大师
  • 2026 哈尔滨英语雅思培训教育机构推荐:雅思培训课程中心权威口碑榜单 - 老周说教育
  • Cirrus-be安装部署
  • ABB张力3BSE004166R1压力传感器