5e2812a6f3
v6.2.1 — Blender Integration Addon: - 5 files, 1,732 lines: __init__, operators (9 ops), panels (4 panels) - preferences, vde_bridge (subprocess CLI bridge) - Import/Export STEP, primitives, boolean, SDF→Mesh v6.2.2 — .NET/C# Bindings (VdeSharp): - 8 files: NativeMethods (50+ P/Invoke), BrepModel, MeshData, SdfEngine, Assembly - C API extended with 20 new functions (STEP I/O, Boolean, SDF, Assembly) - vde_capi rebuilt as SHARED library - Example: import STEP → boolean → export v6.2.3 — Developer Docs + Plugin System: - 7 docs: architecture, contributing, API overview, building, testing, plugin-system - plugin_system.h/.cpp: PluginInterface, PluginManager, dlopen/LoadLibrary - README.md updated with v6 features - Syntax-check passed (GCC 10.2.1, -Wall -Wextra -Wpedantic)
571 lines
18 KiB
Python
571 lines
18 KiB
Python
"""
|
||
operators.py — VDE Blender 操作符
|
||
=================================
|
||
所有 VDE 操作符定义,可在 Blender 中搜索和调用。
|
||
|
||
操作符列表:
|
||
- VDE_OT_import_step — 导入 STEP 文件
|
||
- VDE_OT_export_step — 导出选中对象为 STEP
|
||
- VDE_OT_create_box — 创建 Box 图元
|
||
- VDE_OT_create_cylinder — 创建 Cylinder 图元
|
||
- VDE_OT_create_sphere — 创建 Sphere 图元
|
||
- VDE_OT_boolean_union — 布尔并集
|
||
- VDE_OT_boolean_intersect— 布尔交集
|
||
- VDE_OT_boolean_diff — 布尔差集
|
||
- VDE_OT_sdf_to_mesh — SDF→Mesh 转换
|
||
"""
|
||
|
||
import os
|
||
|
||
import bpy
|
||
from bpy.props import (
|
||
StringProperty, FloatProperty, IntProperty,
|
||
BoolProperty, EnumProperty, PointerProperty,
|
||
)
|
||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||
|
||
from . import vde_bridge
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 辅助函数
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def get_prefs():
|
||
"""获取插件偏好设置。"""
|
||
return bpy.context.preferences.addons[__package__].preferences
|
||
|
||
|
||
def get_bridge():
|
||
"""获取 VDE 桥接实例。"""
|
||
prefs = get_prefs()
|
||
return vde_bridge.VDEBridge(
|
||
vde_lib_path=prefs.vde_lib_path,
|
||
python_executable=prefs.python_path or None,
|
||
)
|
||
|
||
|
||
def report_and_raise(self, error_msg):
|
||
"""报告错误并设置操作符状态。"""
|
||
self.report({'ERROR'}, error_msg)
|
||
return {'CANCELLED'}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 导入/导出操作符
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class VDE_OT_import_step(bpy.types.Operator, ImportHelper):
|
||
"""从 STEP 文件导入几何体为 Blender Mesh。"""
|
||
bl_idname = "vde.import_step"
|
||
bl_label = "导入 STEP"
|
||
bl_description = "使用 VDE 引擎从 STEP/STP 文件导入 B-Rep 几何体"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
filename_ext = ".stp;.step"
|
||
filter_glob: StringProperty(
|
||
default="*.stp;*.step",
|
||
options={'HIDDEN'},
|
||
)
|
||
|
||
heal_import: BoolProperty(
|
||
name="自动修复",
|
||
description="导入后自动修复拓扑缺陷(合并顶点、闭合缝隙)",
|
||
default=True,
|
||
)
|
||
|
||
deflection: FloatProperty(
|
||
name="网格精度",
|
||
description="B-Rep 离散化为网格的弦高偏差 (越小越精细)",
|
||
default=0.01,
|
||
min=0.001,
|
||
max=1.0,
|
||
precision=4,
|
||
subtype='FACTOR',
|
||
)
|
||
|
||
def execute(self, context):
|
||
bridge = get_bridge()
|
||
self.report({'INFO'}, f"正在导入: {self.filepath}")
|
||
|
||
try:
|
||
result = bridge.execute("import_step", path=self.filepath)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
name = os.path.basename(self.filepath).rsplit(".", 1)[0]
|
||
try:
|
||
obj = vde_bridge.bridge_result_to_blender(result, name=name)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report({'INFO'}, f"导入成功: {name} "
|
||
f"({result.get('num_vertices', 0)} 顶点, "
|
||
f"{result.get('num_faces', 0)} 面)")
|
||
return {'FINISHED'}
|
||
|
||
|
||
class VDE_OT_export_step(bpy.types.Operator, ExportHelper):
|
||
"""将选中对象导出为 STEP 文件。"""
|
||
bl_idname = "vde.export_step"
|
||
bl_label = "导出 STEP"
|
||
bl_description = "使用 VDE 引擎将选中网格导出为 STEP AP242 格式"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
filename_ext = ".stp"
|
||
filter_glob: StringProperty(
|
||
default="*.stp;*.step",
|
||
options={'HIDDEN'},
|
||
)
|
||
|
||
include_pmi: BoolProperty(
|
||
name="包含 PMI",
|
||
description="在 STEP 文件中包含产品和制造信息",
|
||
default=False,
|
||
)
|
||
|
||
def execute(self, context):
|
||
bridge = get_bridge()
|
||
|
||
selected = [o for o in context.selected_objects if o.type == 'MESH']
|
||
if not selected:
|
||
return report_and_raise(self, "请先选中至少一个 Mesh 对象。")
|
||
|
||
meshes = []
|
||
for obj in selected:
|
||
try:
|
||
mesh_data = vde_bridge.blender_mesh_to_bridge_data(obj)
|
||
meshes.append(mesh_data)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, f"对象 '{obj.name}': {e}")
|
||
|
||
self.report({'INFO'}, f"正在导出 {len(meshes)} 个对象到: {self.filepath}")
|
||
|
||
try:
|
||
result = bridge.execute(
|
||
"export_step",
|
||
path=self.filepath,
|
||
meshes=meshes,
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report({'INFO'}, f"导出成功: {self.filepath}")
|
||
return {'FINISHED'}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 图元创建操作符
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class VDE_OT_create_box(bpy.types.Operator):
|
||
"""创建 Box B-Rep 图元。"""
|
||
bl_idname = "vde.create_box"
|
||
bl_label = "Box"
|
||
bl_description = "使用 VDE 创建 Box 图元"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
width: FloatProperty(
|
||
name="宽度 (X)",
|
||
default=2.0,
|
||
min=0.01,
|
||
max=100.0,
|
||
)
|
||
height: FloatProperty(
|
||
name="高度 (Y)",
|
||
default=2.0,
|
||
min=0.01,
|
||
max=100.0,
|
||
)
|
||
depth: FloatProperty(
|
||
name="深度 (Z)",
|
||
default=2.0,
|
||
min=0.01,
|
||
max=100.0,
|
||
)
|
||
|
||
def execute(self, context):
|
||
bridge = get_bridge()
|
||
try:
|
||
result = bridge.execute("create_primitive", type="box", params={
|
||
"width": self.width,
|
||
"height": self.height,
|
||
"depth": self.depth,
|
||
})
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
try:
|
||
vde_bridge.bridge_result_to_blender(
|
||
result,
|
||
name=f"VDE_Box_{self.width:.1f}x{self.height:.1f}x{self.depth:.1f}"
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report({'INFO'}, f"创建 Box ({self.width}×{self.height}×{self.depth})")
|
||
return {'FINISHED'}
|
||
|
||
|
||
class VDE_OT_create_cylinder(bpy.types.Operator):
|
||
"""创建 Cylinder B-Rep 图元。"""
|
||
bl_idname = "vde.create_cylinder"
|
||
bl_label = "Cylinder"
|
||
bl_description = "使用 VDE 创建圆柱体图元"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
radius: FloatProperty(
|
||
name="半径",
|
||
default=1.0,
|
||
min=0.01,
|
||
max=50.0,
|
||
)
|
||
height: FloatProperty(
|
||
name="高度",
|
||
default=2.0,
|
||
min=0.01,
|
||
max=100.0,
|
||
)
|
||
|
||
def execute(self, context):
|
||
bridge = get_bridge()
|
||
try:
|
||
result = bridge.execute("create_primitive", type="cylinder", params={
|
||
"radius": self.radius,
|
||
"height": self.height,
|
||
})
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
try:
|
||
vde_bridge.bridge_result_to_blender(
|
||
result,
|
||
name=f"VDE_Cylinder_r{self.radius:.1f}_h{self.height:.1f}"
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report({'INFO'}, f"创建 Cylinder (r={self.radius}, h={self.height})")
|
||
return {'FINISHED'}
|
||
|
||
|
||
class VDE_OT_create_sphere(bpy.types.Operator):
|
||
"""创建 Sphere B-Rep 图元。"""
|
||
bl_idname = "vde.create_sphere"
|
||
bl_label = "Sphere"
|
||
bl_description = "使用 VDE 创建球体图元"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
radius: FloatProperty(
|
||
name="半径",
|
||
default=1.0,
|
||
min=0.01,
|
||
max=50.0,
|
||
)
|
||
|
||
def execute(self, context):
|
||
bridge = get_bridge()
|
||
try:
|
||
result = bridge.execute("create_primitive", type="sphere", params={
|
||
"radius": self.radius,
|
||
})
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
try:
|
||
vde_bridge.bridge_result_to_blender(
|
||
result,
|
||
name=f"VDE_Sphere_r{self.radius:.1f}"
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report({'INFO'}, f"创建 Sphere (r={self.radius})")
|
||
return {'FINISHED'}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 布尔运算操作符
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class VDE_OT_boolean_union(bpy.types.Operator):
|
||
"""布尔并集 — 合并两个选中对象。"""
|
||
bl_idname = "vde.boolean_union"
|
||
bl_label = "布尔并集 (Union)"
|
||
bl_description = "使用 VDE 引擎计算两个网格的布尔并集"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
def execute(self, context):
|
||
return _execute_boolean(self, context, "union", "Union")
|
||
|
||
|
||
class VDE_OT_boolean_intersect(bpy.types.Operator):
|
||
"""布尔交集 — 保留两对象重叠部分。"""
|
||
bl_idname = "vde.boolean_intersect"
|
||
bl_label = "布尔交集 (Intersect)"
|
||
bl_description = "使用 VDE 引擎计算两个网格的布尔交集"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
def execute(self, context):
|
||
return _execute_boolean(self, context, "intersect", "Intersect")
|
||
|
||
|
||
class VDE_OT_boolean_diff(bpy.types.Operator):
|
||
"""布尔差集 — 从第一个对象减去第二个对象。"""
|
||
bl_idname = "vde.boolean_diff"
|
||
bl_label = "布尔差集 (Difference)"
|
||
bl_description = "使用 VDE 引擎计算两个网格的布尔差集 (A - B)"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
def execute(self, context):
|
||
return _execute_boolean(self, context, "diff", "Diff")
|
||
|
||
|
||
def _execute_boolean(operator, context, op_type, label):
|
||
"""内部布尔运算执行函数。"""
|
||
selected = [o for o in context.selected_objects if o.type == 'MESH']
|
||
if len(selected) < 2:
|
||
return report_and_raise(operator, "请选中恰好两个 Mesh 对象。")
|
||
|
||
obj_a = context.active_object
|
||
obj_b = selected[0] if selected[0] != obj_a else selected[1]
|
||
|
||
if obj_a is None or obj_b is None:
|
||
return report_and_raise(operator, "请设置活动对象并选中另一个对象。")
|
||
|
||
operator.report(
|
||
{'INFO'},
|
||
f"正在计算布尔{label}: {obj_a.name} vs {obj_b.name}"
|
||
)
|
||
|
||
bridge = get_bridge()
|
||
try:
|
||
mesh_a = vde_bridge.blender_mesh_to_bridge_data(obj_a)
|
||
mesh_b = vde_bridge.blender_mesh_to_bridge_data(obj_b)
|
||
except RuntimeError as e:
|
||
return report_and_raise(operator, str(e))
|
||
|
||
try:
|
||
result = bridge.execute(
|
||
"boolean",
|
||
boolean_type=op_type,
|
||
mesh_a=mesh_a,
|
||
mesh_b=mesh_b,
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(operator, str(e))
|
||
|
||
try:
|
||
vde_bridge.bridge_result_to_blender(
|
||
result,
|
||
name=f"VDE_Boolean_{label}_{obj_a.name}"
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(operator, str(e))
|
||
|
||
operator.report(
|
||
{'INFO'},
|
||
f"布尔{label}完成: "
|
||
f"{result.get('num_vertices', 0)} 顶点, "
|
||
f"{result.get('num_faces', 0)} 面"
|
||
)
|
||
return {'FINISHED'}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# SDF → Mesh 操作符
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class VDE_OT_sdf_to_mesh(bpy.types.Operator):
|
||
"""将 SDF 隐式曲面转换为三角网格。"""
|
||
bl_idname = "vde.sdf_to_mesh"
|
||
bl_label = "SDF → Mesh"
|
||
bl_description = "使用 VDE 引擎将 SDF 隐式几何体转为三角网格"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
sdf_type: EnumProperty(
|
||
name="SDF 类型",
|
||
description="要生成的隐式曲面类型",
|
||
items=[
|
||
('sphere', "球体", "球体 SDF"),
|
||
('box', "Box", "Box SDF"),
|
||
('cylinder', "圆柱体", "圆柱体 SDF"),
|
||
('torus', "环面", "环面 SDF"),
|
||
('custom', "自定义 CSG", "自定义 SDF CSG 组合树"),
|
||
],
|
||
default='sphere',
|
||
)
|
||
|
||
resolution: IntProperty(
|
||
name="分辨率",
|
||
description="Marching Cubes 网格分辨率 (越高越精细)",
|
||
default=64,
|
||
min=16,
|
||
max=512,
|
||
)
|
||
|
||
# ══ 球体参数 ══
|
||
sphere_radius: FloatProperty(
|
||
name="半径",
|
||
default=1.0,
|
||
min=0.01,
|
||
max=50.0,
|
||
)
|
||
|
||
# ══ Box 参数 ══
|
||
box_hx: FloatProperty(
|
||
name="半长 (X)",
|
||
default=1.0,
|
||
min=0.01,
|
||
max=50.0,
|
||
)
|
||
box_hy: FloatProperty(
|
||
name="半长 (Y)",
|
||
default=1.0,
|
||
min=0.01,
|
||
max=50.0,
|
||
)
|
||
box_hz: 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,
|
||
)
|
||
|
||
def draw(self, context):
|
||
layout = self.layout
|
||
layout.prop(self, "sdf_type")
|
||
layout.prop(self, "resolution")
|
||
|
||
box = layout.box()
|
||
if self.sdf_type == 'sphere':
|
||
box.prop(self, "sphere_radius")
|
||
elif self.sdf_type == 'box':
|
||
box.prop(self, "box_hx")
|
||
box.prop(self, "box_hy")
|
||
box.prop(self, "box_hz")
|
||
elif self.sdf_type == 'cylinder':
|
||
box.prop(self, "cyl_radius")
|
||
box.prop(self, "cyl_height")
|
||
elif self.sdf_type == 'torus':
|
||
box.prop(self, "torus_major")
|
||
box.prop(self, "torus_minor")
|
||
elif self.sdf_type == 'custom':
|
||
box.label(text="自定义 CSG 通过面板配置")
|
||
|
||
def execute(self, context):
|
||
params = {}
|
||
node_name = "SDF"
|
||
|
||
if self.sdf_type == 'sphere':
|
||
params = {"radius": self.sphere_radius}
|
||
node_name = f"VDE_SDF_Sphere_r{self.sphere_radius:.1f}"
|
||
elif self.sdf_type == 'box':
|
||
params = {"half_extents": [self.box_hx, self.box_hy, self.box_hz]}
|
||
node_name = (f"VDE_SDF_Box_"
|
||
f"{self.box_hx:.1f}x{self.box_hy:.1f}x{self.box_hz:.1f}")
|
||
elif self.sdf_type == 'cylinder':
|
||
params = {"radius": self.cyl_radius, "height": self.cyl_height}
|
||
node_name = f"VDE_SDF_Cyl_r{self.cyl_radius:.1f}_h{self.cyl_height:.1f}"
|
||
elif self.sdf_type == 'torus':
|
||
params = {"major_radius": self.torus_major,
|
||
"minor_radius": self.torus_minor}
|
||
node_name = (f"VDE_SDF_Torus_"
|
||
f"R{self.torus_major:.1f}_r{self.torus_minor:.1f}")
|
||
elif self.sdf_type == 'custom':
|
||
# 从场景属性读取自定义 CSG 配置
|
||
scene = context.scene
|
||
params = {
|
||
"children": list(scene.vde_sdf_children),
|
||
"operations": list(scene.vde_sdf_operations),
|
||
}
|
||
if scene.vde_sdf_blend_radius > 0:
|
||
params["blend_radius"] = scene.vde_sdf_blend_radius
|
||
if scene.vde_sdf_round > 0:
|
||
params["round"] = scene.vde_sdf_round
|
||
node_name = "VDE_SDF_CSG"
|
||
|
||
bridge = get_bridge()
|
||
self.report(
|
||
{'INFO'},
|
||
f"SDF→Mesh: {self.sdf_type}, res={self.resolution}"
|
||
)
|
||
|
||
try:
|
||
result = bridge.execute(
|
||
"sdf_to_mesh",
|
||
sdf_type=self.sdf_type,
|
||
params=params,
|
||
resolution=self.resolution,
|
||
)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
try:
|
||
vde_bridge.bridge_result_to_blender(result, name=node_name)
|
||
except RuntimeError as e:
|
||
return report_and_raise(self, str(e))
|
||
|
||
self.report(
|
||
{'INFO'},
|
||
f"SDF→Mesh 完成: "
|
||
f"{result.get('num_vertices', 0)} 顶点, "
|
||
f"{result.get('num_faces', 0)} 面"
|
||
)
|
||
return {'FINISHED'}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 注册/注销
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
CLASSES = [
|
||
VDE_OT_import_step,
|
||
VDE_OT_export_step,
|
||
VDE_OT_create_box,
|
||
VDE_OT_create_cylinder,
|
||
VDE_OT_create_sphere,
|
||
VDE_OT_boolean_union,
|
||
VDE_OT_boolean_intersect,
|
||
VDE_OT_boolean_diff,
|
||
VDE_OT_sdf_to_mesh,
|
||
]
|
||
|
||
|
||
def register():
|
||
for cls in CLASSES:
|
||
bpy.utils.register_class(cls)
|
||
|
||
|
||
def unregister():
|
||
for cls in reversed(CLASSES):
|
||
bpy.utils.unregister_class(cls)
|