""" panels.py — VDE Blender 侧边栏面板 =================================== 在 3D Viewport 的 N 面板(右侧工具栏)添加 VDE 选项卡, 包含 Create、Modify、SDF 三个子面板。 """ import bpy from bpy.props import ( FloatProperty, IntProperty, EnumProperty, StringProperty, CollectionProperty, BoolProperty, ) # ═══════════════════════════════════════════════════════════════ # SDF CSG 配置属性(存储在场景级别) # ═══════════════════════════════════════════════════════════════ SDF_PRIMITIVE_ITEMS = [ ('sphere', "球体", "球体 SDF"), ('box', "Box", "Box SDF"), ('cylinder', "圆柱体", "圆柱体 SDF"), ('torus', "环面", "环面 SDF"), ] SDF_OPERATION_ITEMS = [ ('union', "Union (并集)", ""), ('intersection', "Intersection (交集)", ""), ('difference', "Difference (差集)", ""), ('smooth_union', "Smooth Union (光滑并集)", ""), ('smooth_intersection', "Smooth Intersection (光滑交集)", ""), ('smooth_difference', "Smooth Difference (光滑差集)", ""), ] class VDE_SdfChildItem(bpy.types.PropertyGroup): """SDF CSG 子节点定义。""" sdf_type: EnumProperty( name="类型", items=SDF_PRIMITIVE_ITEMS, default='sphere', ) radius: FloatProperty(name="半径", default=1.0, min=0.01, max=50.0) half_x: FloatProperty(name="半长 X", default=1.0, min=0.01, max=50.0) half_y: FloatProperty(name="半长 Y", default=1.0, min=0.01, max=50.0) half_z: FloatProperty(name="半长 Z", default=1.0, min=0.01, max=50.0) cyl_radius: FloatProperty(name="半径", default=1.0, min=0.01, max=50.0) cyl_height: FloatProperty(name="高度", default=2.0, min=0.01, max=100.0) torus_major: FloatProperty(name="主半径", default=1.0, min=0.1, max=50.0) torus_minor: FloatProperty(name="管半径", default=0.3, min=0.01, max=25.0) class VDE_SdfOpItem(bpy.types.PropertyGroup): """SDF CSG 操作定义。""" operation: EnumProperty( name="操作", items=SDF_OPERATION_ITEMS, default='union', ) # ═══════════════════════════════════════════════════════════════ # 面板: VDE 主面板 # ═══════════════════════════════════════════════════════════════ class VIEW3D_PT_vde_main(bpy.types.Panel): """VDE 主面板容器。""" bl_label = "VDE" bl_idname = "VIEW3D_PT_vde_main" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VDE" bl_description = "ViewDesignEngine 计算几何引擎" def draw(self, context): layout = self.layout prefs = context.preferences.addons[__package__].preferences # 状态指示器 row = layout.row(align=True) available, msg = self._check_available(prefs) if available: row.label(text="● VDE 就绪", icon='CHECKMARK') else: row.label(text="○ VDE 未连接", icon='ERROR') row = layout.row() row.label(text=msg, icon='INFO') # 快捷设置 box = layout.box() box.label(text="快捷操作:", icon='TOOL_SETTINGS') col = box.column(align=True) col.operator("vde.import_step", text="导入 STEP...", icon='IMPORT') col.operator("vde.export_step", text="导出 STEP...", icon='EXPORT') col.operator("vde.sdf_to_mesh", text="SDF → Mesh", icon='MESH_DATA') def _check_available(self, prefs): """缓存检查以避免每次重绘都调用子进程。""" try: return (prefs.vde_available, prefs.vde_status_message) except Exception: return (False, "检查失败") # ═══════════════════════════════════════════════════════════════ # 子面板: Create — 图元创建 # ═══════════════════════════════════════════════════════════════ class VIEW3D_PT_vde_create(bpy.types.Panel): """创建 B-Rep 图元子面板。""" bl_label = "创建" bl_parent_id = "VIEW3D_PT_vde_main" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VDE" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout # Box box = layout.box() box.label(text="Box", icon='MESH_CUBE') col = box.column(align=True) op = col.operator("vde.create_box", text="创建 Box") col.prop( bpy.types.VDE_OT_create_box.bl_rna.properties['width'], "default", text="宽" ) col.prop( bpy.types.VDE_OT_create_box.bl_rna.properties['height'], "default", text="高" ) col.prop( bpy.types.VDE_OT_create_box.bl_rna.properties['depth'], "default", text="深" ) # Cylinder box = layout.box() box.label(text="圆柱体", icon='MESH_CYLINDER') col = box.column(align=True) op = col.operator("vde.create_cylinder", text="创建圆柱体") col.prop( bpy.types.VDE_OT_create_cylinder.bl_rna.properties['radius'], "default", text="半径" ) col.prop( bpy.types.VDE_OT_create_cylinder.bl_rna.properties['height'], "default", text="高" ) # Sphere box = layout.box() box.label(text="球体", icon='MESH_UVSPHERE') col = box.column(align=True) op = col.operator("vde.create_sphere", text="创建球体") col.prop( bpy.types.VDE_OT_create_sphere.bl_rna.properties['radius'], "default", text="半径" ) # ═══════════════════════════════════════════════════════════════ # 子面板: Modify — 布尔/变形 # ═══════════════════════════════════════════════════════════════ class VIEW3D_PT_vde_modify(bpy.types.Panel): """布尔运算和修改子面板。""" bl_label = "修改" bl_parent_id = "VIEW3D_PT_vde_main" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VDE" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout # 布尔运算 box = layout.box() box.label(text="布尔运算", icon='MOD_BOOLEAN') box.label(text="选中两个 Mesh 对象后执行:", icon='INFO') col = box.column(align=True) col.operator("vde.boolean_union", text="并集 (Union)", icon='ADD') col.operator("vde.boolean_intersect", text="交集 (Intersect)", icon='SELECT_INTERSECT') col.operator("vde.boolean_diff", text="差集 (A − B)", icon='REMOVE') # 使用提示 box = layout.box() box.label(text="使用说明", icon='HELP') col = box.column(align=True) col.label(text="1. Shift+点击选中两个对象") col.label(text="2. 最后选中的为「活动对象」") col.label(text="3. 点击布尔运算按钮") # ═══════════════════════════════════════════════════════════════ # 子面板: SDF — 隐式建模 # ═══════════════════════════════════════════════════════════════ class VIEW3D_PT_vde_sdf(bpy.types.Panel): """SDF 隐式建模子面板。""" bl_label = "SDF 建模" bl_parent_id = "VIEW3D_PT_vde_main" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VDE" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout scene = context.scene # SDF 类型选择 box = layout.box() box.label(text="基础 SDF", icon='MESH_DATA') col = box.column(align=True) # 直接用操作符属性绘制 op_props = bpy.types.VDE_OT_sdf_to_mesh.bl_rna.properties col.prop(op_props['sdf_type'], "default", text="类型") # 根据类型显示参数 sdf_type = op_props['sdf_type'].default if sdf_type == 'sphere': col.prop(op_props['sphere_radius'], "default", text="半径") elif sdf_type == 'box': col.prop(op_props['box_hx'], "default", text="半长 X") col.prop(op_props['box_hy'], "default", text="半长 Y") col.prop(op_props['box_hz'], "default", text="半长 Z") elif sdf_type == 'cylinder': col.prop(op_props['cyl_radius'], "default", text="半径") col.prop(op_props['cyl_height'], "default", text="高度") elif sdf_type == 'torus': col.prop(op_props['torus_major'], "default", text="主半径") col.prop(op_props['torus_minor'], "default", text="管半径") col.prop(op_props['resolution'], "default", text="分辨率") row = col.row() row.operator("vde.sdf_to_mesh", text="生成网格", icon='PLAY') # 自定义 CSG box = layout.box() box.label(text="自定义 CSG 组合", icon='NODETREE') # 子节点列表 row = box.row() row.label(text="子节点:") row.operator("vde.sdf_add_child", text="", icon='ADD') row.operator("vde.sdf_remove_child", text="", icon='REMOVE') for i, child in enumerate(scene.vde_sdf_children): sub_box = box.box() sub_box.label(text=f"节点 {i + 1}:") sub_box.prop(child, "sdf_type", text="类型") if child.sdf_type == 'sphere': sub_box.prop(child, "radius", text="半径") elif child.sdf_type == 'box': sub_box.prop(child, "half_x", text="半长 X") sub_box.prop(child, "half_y", text="半长 Y") sub_box.prop(child, "half_z", text="半长 Z") elif child.sdf_type == 'cylinder': sub_box.prop(child, "cyl_radius", text="半径") sub_box.prop(child, "cyl_height", text="高度") elif child.sdf_type == 'torus': sub_box.prop(child, "torus_major", text="主半径") sub_box.prop(child, "torus_minor", text="管半径") # CSG 操作列表 if len(scene.vde_sdf_children) > 1: row = box.row() row.label(text="操作:") row.operator("vde.sdf_add_operation", text="", icon='ADD') row.operator("vde.sdf_remove_operation", text="", icon='REMOVE') for i, op_item in enumerate(scene.vde_sdf_operations): sub_box = box.box() sub_box.prop(op_item, "operation", text=f"节点{i}→节点{i + 1}") # 额外变换 col = box.column(align=True) col.prop(scene, "vde_sdf_blend_radius", text="光滑混合半径") col.prop(scene, "vde_sdf_round", text="圆角半径") # 生成按钮 row = box.row() row.scale_y = 1.5 row.operator("vde.sdf_to_mesh", text="▶ CSG → Mesh", icon='OUTLINER_OB_MESH').sdf_type = 'custom' # ═══════════════════════════════════════════════════════════════ # SDF CSG 管理操作符 # ═══════════════════════════════════════════════════════════════ class VDE_OT_sdf_add_child(bpy.types.Operator): """添加 SDF CSG 子节点。""" bl_idname = "vde.sdf_add_child" bl_label = "添加节点" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): item = context.scene.vde_sdf_children.add() item.name = f"Node_{len(context.scene.vde_sdf_children)}" return {'FINISHED'} class VDE_OT_sdf_remove_child(bpy.types.Operator): """移除最后一个 SDF CSG 子节点。""" bl_idname = "vde.sdf_remove_child" bl_label = "移除节点" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): children = context.scene.vde_sdf_children ops = context.scene.vde_sdf_operations if len(children) > 0: children.remove(len(children) - 1) # 保持操作列表与节点数一致 while len(ops) >= len(children): ops.remove(len(ops) - 1) return {'FINISHED'} class VDE_OT_sdf_add_operation(bpy.types.Operator): """添加 SDF CSG 操作。""" bl_idname = "vde.sdf_add_operation" bl_label = "添加操作" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): children = context.scene.vde_sdf_children ops = context.scene.vde_sdf_operations if len(ops) < len(children) - 1: ops.add() return {'FINISHED'} class VDE_OT_sdf_remove_operation(bpy.types.Operator): """移除最后一个 SDF CSG 操作。""" bl_idname = "vde.sdf_remove_operation" bl_label = "移除操作" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): ops = context.scene.vde_sdf_operations if len(ops) > 0: ops.remove(len(ops) - 1) return {'FINISHED'} # ═══════════════════════════════════════════════════════════════ # 注册/注销 # ═══════════════════════════════════════════════════════════════ PANEL_CLASSES = [ VIEW3D_PT_vde_main, VIEW3D_PT_vde_create, VIEW3D_PT_vde_modify, VIEW3D_PT_vde_sdf, ] SDF_CLASSES = [ VDE_SdfChildItem, VDE_SdfOpItem, VDE_OT_sdf_add_child, VDE_OT_sdf_remove_child, VDE_OT_sdf_add_operation, VDE_OT_sdf_remove_operation, ] SCENE_PROPS = [ # SDF CSG 配置 ("vde_sdf_children", CollectionProperty(type=VDE_SdfChildItem)), ("vde_sdf_operations", CollectionProperty(type=VDE_SdfOpItem)), ("vde_sdf_blend_radius", FloatProperty( name="光滑混合半径", default=0.1, min=0.0, max=10.0, precision=3, )), ("vde_sdf_round", FloatProperty( name="圆角半径", default=0.0, min=0.0, max=10.0, precision=3, )), ] def register(): for cls in SDF_CLASSES: bpy.utils.register_class(cls) for cls in PANEL_CLASSES: bpy.utils.register_class(cls) # 注册场景属性 for name, prop in SCENE_PROPS: setattr(bpy.types.Scene, name, prop) def unregister(): for cls in reversed(PANEL_CLASSES): bpy.utils.unregister_class(cls) for cls in reversed(SDF_CLASSES): bpy.utils.unregister_class(cls) # 清理场景属性 for name, _ in SCENE_PROPS: if hasattr(bpy.types.Scene, name): delattr(bpy.types.Scene, name)