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)
502 lines
16 KiB
Python
502 lines
16 KiB
Python
"""
|
||
vde_bridge.py — VDE 子进程桥接模块
|
||
===================================
|
||
通过独立的 Python 子进程调用 VDE Python 绑定,避免 Blender 与 VDE 的环境冲突。
|
||
|
||
工作原理:
|
||
1. 将操作参数序列化为 JSON
|
||
2. 启动 Python 子进程,传入操作类型和参数
|
||
3. 子进程导入 VDE,执行操作,输出 JSON 结果
|
||
4. 解析结果,将几何数据转换为 Blender Mesh
|
||
|
||
对于不支持的操作(VDE 库未安装时),返回错误信息并提示安装。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import subprocess
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# VDE 内联执行脚本(使用 %s 占位符避免与 JSON 大括号冲突)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
_VDE_SCRIPT = r'''"""VDE bridge worker — 由 blender_addon 子进程调用。"""
|
||
import sys
|
||
import json
|
||
|
||
def error(msg):
|
||
print(json.dumps({"error": str(msg)}))
|
||
sys.exit(1)
|
||
|
||
# ── 定位 VDE 库 ──
|
||
vde_path = %s
|
||
|
||
if vde_path:
|
||
sys.path.insert(0, vde_path)
|
||
|
||
try:
|
||
from vde import brep, mesh, sdf as vde_sdf
|
||
except ImportError as e:
|
||
error("VDE 库未安装或路径不正确: " + str(e) +
|
||
"\n请检查 Blender 插件偏好设置中的 VDE 库路径。")
|
||
|
||
# ── 执行操作 ──
|
||
operation = %s
|
||
args = %s
|
||
|
||
try:
|
||
if operation == "import_step":
|
||
result = run_import_step(args)
|
||
elif operation == "export_step":
|
||
result = run_export_step(args)
|
||
elif operation == "create_primitive":
|
||
result = run_create_primitive(args)
|
||
elif operation == "boolean":
|
||
result = run_boolean(args)
|
||
elif operation == "sdf_to_mesh":
|
||
result = run_sdf_to_mesh(args)
|
||
else:
|
||
error("未知操作: " + operation)
|
||
print(json.dumps(result))
|
||
except Exception as e:
|
||
import traceback
|
||
print(json.dumps({
|
||
"error": str(e),
|
||
"traceback": traceback.format_exc()
|
||
}))
|
||
|
||
# ── 操作实现 ──
|
||
|
||
def brep_to_mesh_data(body):
|
||
"""将 BrepModel 转换为网格数据字典。"""
|
||
try:
|
||
he_mesh = body.to_mesh(0.01)
|
||
except Exception as e:
|
||
raise RuntimeError("B-Rep 转网格失败: " + str(e))
|
||
|
||
verts = []
|
||
for i in range(he_mesh.num_vertices()):
|
||
v = he_mesh.vertex(i)
|
||
verts.append([v.x(), v.y(), v.z()])
|
||
|
||
faces = []
|
||
for i in range(he_mesh.num_faces()):
|
||
fv = he_mesh.face_vertices(i)
|
||
faces.append(list(fv))
|
||
|
||
return {
|
||
"vertices": verts,
|
||
"faces": faces,
|
||
"num_vertices": len(verts),
|
||
"num_faces": len(faces),
|
||
}
|
||
|
||
|
||
def run_import_step(args):
|
||
"""导入 STEP 文件。"""
|
||
path = args.get("path", "")
|
||
if not path:
|
||
error("STEP 文件路径未指定")
|
||
|
||
# 使用 industrial_formats 或 brep 的 import_step
|
||
try:
|
||
body = brep.import_step(path)
|
||
except AttributeError:
|
||
try:
|
||
from vde._vde.foundation import import_step as vde_import_step
|
||
body = vde_import_step(path)
|
||
except (ImportError, AttributeError):
|
||
error("VDE 未编译 STEP 导入支持。请确保 industrial_formats 模块已编译。")
|
||
|
||
return brep_to_mesh_data(body)
|
||
|
||
|
||
def run_export_step(args):
|
||
"""导出选中对象为 STEP 文件。"""
|
||
path = args.get("path", "")
|
||
if not path:
|
||
error("导出路径未指定")
|
||
|
||
meshes = args.get("meshes", [])
|
||
bodies = []
|
||
|
||
for m in meshes:
|
||
verts = m.get("vertices", [])
|
||
faces = m.get("faces", [])
|
||
|
||
if not verts or not faces:
|
||
continue
|
||
|
||
# 从网格数据创建 HalfedgeMesh → BrepModel
|
||
hm = mesh.HalfedgeMesh()
|
||
hm.build_from_triangles(verts, faces)
|
||
body = brep.BrepModel.from_mesh(hm)
|
||
bodies.append(body)
|
||
|
||
if not bodies:
|
||
error("没有有效的网格数据可导出")
|
||
|
||
try:
|
||
brep.export_step(path, bodies)
|
||
except AttributeError:
|
||
try:
|
||
from vde._vde import io
|
||
io.export_step_ap242_file(path, bodies)
|
||
except (ImportError, AttributeError) as e:
|
||
error("VDE 未编译 STEP 导出支持: " + str(e))
|
||
|
||
return {"status": "ok", "path": path, "num_bodies": len(bodies)}
|
||
|
||
|
||
def run_create_primitive(args):
|
||
"""创建 B-Rep 图元。"""
|
||
prim_type = args.get("type", "box")
|
||
params = args.get("params", {})
|
||
|
||
if prim_type == "box":
|
||
w = params.get("width", 2.0)
|
||
h = params.get("height", 2.0)
|
||
d = params.get("depth", 2.0)
|
||
body = brep.make_box(w, h, d)
|
||
elif prim_type == "cylinder":
|
||
r = params.get("radius", 1.0)
|
||
h = params.get("height", 2.0)
|
||
body = brep.make_cylinder(r, h)
|
||
elif prim_type == "sphere":
|
||
r = params.get("radius", 1.0)
|
||
body = brep.make_sphere(r)
|
||
else:
|
||
error("未知图元类型: " + prim_type)
|
||
|
||
return brep_to_mesh_data(body)
|
||
|
||
|
||
def run_boolean(args):
|
||
"""布尔运算。"""
|
||
op_type = args.get("boolean_type", "union")
|
||
mesh_a = args.get("mesh_a", {})
|
||
mesh_b = args.get("mesh_b", {})
|
||
|
||
def mesh_to_brep(m):
|
||
hm = mesh.HalfedgeMesh()
|
||
hm.build_from_triangles(m.get("vertices", []), m.get("faces", []))
|
||
return brep.BrepModel.from_mesh(hm)
|
||
|
||
body_a = mesh_to_brep(mesh_a)
|
||
body_b = mesh_to_brep(mesh_b)
|
||
|
||
if op_type == "union":
|
||
result_body = brep.boolean_union(body_a, body_b)
|
||
elif op_type == "intersect":
|
||
result_body = brep.boolean_intersection(body_a, body_b)
|
||
elif op_type == "diff":
|
||
result_body = brep.boolean_difference(body_a, body_b)
|
||
else:
|
||
error("未知布尔操作: " + op_type)
|
||
|
||
return brep_to_mesh_data(result_body)
|
||
|
||
|
||
def run_sdf_to_mesh(args):
|
||
"""SDF → Mesh 转换。"""
|
||
sdf_type = args.get("sdf_type", "sphere")
|
||
params = args.get("params", {})
|
||
resolution = args.get("resolution", 64)
|
||
|
||
if sdf_type == "sphere":
|
||
sdf_node = vde_sdf.SdfNode.sphere(params.get("radius", 1.0))
|
||
elif sdf_type == "box":
|
||
sdf_node = vde_sdf.SdfNode.box(params.get("half_extents", [1.0, 1.0, 1.0]))
|
||
elif sdf_type == "cylinder":
|
||
sdf_node = vde_sdf.SdfNode.cylinder(
|
||
params.get("radius", 1.0), params.get("height", 2.0)
|
||
)
|
||
elif sdf_type == "torus":
|
||
sdf_node = vde_sdf.SdfNode.torus(
|
||
params.get("major_radius", 1.0), params.get("minor_radius", 0.3)
|
||
)
|
||
elif sdf_type == "custom":
|
||
sdf_node = build_custom_sdf(params)
|
||
else:
|
||
error("未知 SDF 类型: " + sdf_type)
|
||
|
||
mc_mesh = vde_sdf.sdf_to_mesh(sdf_node, resolution)
|
||
|
||
verts = [[v[0], v[1], v[2]] for v in mc_mesh.vertices]
|
||
faces = [[t[0], t[1], t[2]] for t in mc_mesh.triangles]
|
||
|
||
return {
|
||
"vertices": verts,
|
||
"faces": faces,
|
||
"num_vertices": len(verts),
|
||
"num_faces": len(faces),
|
||
}
|
||
|
||
|
||
def build_custom_sdf(params):
|
||
"""构建自定义 SDF CSG 树。"""
|
||
children = params.get("children", [])
|
||
operations = params.get("operations", [])
|
||
|
||
if not children:
|
||
return vde_sdf.SdfNode.sphere(1.0)
|
||
|
||
nodes = []
|
||
for child in children:
|
||
ct = child.get("type", "sphere")
|
||
cp = child.get("params", {})
|
||
if ct == "sphere":
|
||
nodes.append(vde_sdf.SdfNode.sphere(cp.get("radius", 1.0)))
|
||
elif ct == "box":
|
||
nodes.append(vde_sdf.SdfNode.box(
|
||
cp.get("half_extents", [1.0, 1.0, 1.0]))
|
||
)
|
||
elif ct == "cylinder":
|
||
nodes.append(vde_sdf.SdfNode.cylinder(
|
||
cp.get("radius", 1.0), cp.get("height", 2.0))
|
||
)
|
||
elif ct == "torus":
|
||
nodes.append(vde_sdf.SdfNode.torus(
|
||
cp.get("major_radius", 1.0), cp.get("minor_radius", 0.3))
|
||
)
|
||
|
||
result = nodes[0]
|
||
for i, op in enumerate(operations):
|
||
next_node = nodes[i + 1]
|
||
if op == "union":
|
||
result = vde_sdf.SdfNode.op_union(result, next_node)
|
||
elif op == "intersection":
|
||
result = vde_sdf.SdfNode.op_intersection(result, next_node)
|
||
elif op == "difference":
|
||
result = vde_sdf.SdfNode.op_difference(result, next_node)
|
||
elif op == "smooth_union":
|
||
result = vde_sdf.SdfNode.smooth_union(
|
||
result, next_node, params.get("blend_radius", 0.1))
|
||
elif op == "smooth_intersection":
|
||
result = vde_sdf.SdfNode.smooth_intersection(
|
||
result, next_node, params.get("blend_radius", 0.1))
|
||
elif op == "smooth_difference":
|
||
result = vde_sdf.SdfNode.smooth_difference(
|
||
result, next_node, params.get("blend_radius", 0.1))
|
||
|
||
# 应用变换
|
||
if params.get("translate"):
|
||
t = params["translate"]
|
||
result = vde_sdf.SdfNode.translate(result, t)
|
||
if params.get("scale"):
|
||
s = params["scale"]
|
||
result = vde_sdf.SdfNode.scale(result, s)
|
||
if params.get("round"):
|
||
result = vde_sdf.SdfNode.round(result, params["round"])
|
||
if params.get("onion"):
|
||
result = vde_sdf.SdfNode.onion(result, params["onion"])
|
||
|
||
return result
|
||
'''
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 桥接客户端
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class VDEBridge:
|
||
"""VDE 桥接客户端 — 通过子进程调用 VDE Python 绑定。"""
|
||
|
||
def __init__(self, vde_lib_path=None, python_executable=None):
|
||
self.vde_lib_path = vde_lib_path or ""
|
||
self.python_executable = python_executable or sys.executable
|
||
|
||
def execute(self, operation, **kwargs):
|
||
"""
|
||
通过子进程执行 VDE 操作。
|
||
|
||
Args:
|
||
operation: 操作类型 ("import_step", "export_step",
|
||
"create_primitive", "boolean", "sdf_to_mesh")
|
||
**kwargs: 操作参数
|
||
|
||
Returns:
|
||
dict: 操作结果,可能包含 "vertices" 和 "faces" 字段
|
||
|
||
Raises:
|
||
RuntimeError: VDE 操作失败
|
||
"""
|
||
vde_path_repr = repr(self.vde_lib_path)
|
||
op_repr = repr(operation)
|
||
args_json = json.dumps(kwargs, indent=2)
|
||
|
||
script = _VDE_SCRIPT % (vde_path_repr, op_repr, args_json)
|
||
|
||
try:
|
||
proc = subprocess.run(
|
||
[self.python_executable, "-c", script],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
universal_newlines=True,
|
||
timeout=120,
|
||
env={**os.environ, "PYTHONUNBUFFERED": "1"},
|
||
)
|
||
except subprocess.TimeoutExpired:
|
||
raise RuntimeError("VDE 操作超时 (120s)。请减小分辨率或检查模型复杂度。")
|
||
except FileNotFoundError:
|
||
raise RuntimeError(
|
||
f"Python 解释器未找到: {self.python_executable}。"
|
||
"请检查 VDE 库路径设置。"
|
||
)
|
||
|
||
stdout = proc.stdout.strip()
|
||
stderr = proc.stderr.strip()
|
||
|
||
if proc.returncode != 0:
|
||
error_msg = stdout or stderr or f"进程退出码: {proc.returncode}"
|
||
raise RuntimeError(f"VDE 子进程错误:\n{error_msg}")
|
||
|
||
try:
|
||
result = json.loads(stdout)
|
||
except json.JSONDecodeError:
|
||
raise RuntimeError(
|
||
f"VDE 子进程返回了无效 JSON。\n"
|
||
f"stdout: {stdout[:500]}\n"
|
||
f"stderr: {stderr[:500]}"
|
||
)
|
||
|
||
if "error" in result:
|
||
raise RuntimeError(f"VDE 操作错误: {result['error']}")
|
||
|
||
return result
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Blender Mesh 构建工具
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def bridge_result_to_blender(result, name="VDE_Mesh", collection=None):
|
||
"""
|
||
将 VDE 桥接结果转换为 Blender Mesh 对象。
|
||
|
||
Args:
|
||
result: VDE 操作返回的 dict,含 "vertices" 和 "faces"
|
||
name: 对象名称
|
||
collection: 目标集合(None 则使用活动集合)
|
||
|
||
Returns:
|
||
bpy.types.Object: 新创建的 Blender 对象
|
||
"""
|
||
import bpy
|
||
|
||
vertices = result.get("vertices", [])
|
||
faces = result.get("faces", [])
|
||
|
||
if not vertices or not faces:
|
||
raise RuntimeError("VDE 返回了空的网格数据。")
|
||
|
||
# 创建 Blender Mesh
|
||
mesh = bpy.data.meshes.new(name=name)
|
||
mesh.from_pydata(vertices, [], faces)
|
||
mesh.update(calc_edges=True)
|
||
mesh.validate()
|
||
|
||
# 创建 Object 并链接到场景
|
||
obj = bpy.data.objects.new(name=name, object_data=mesh)
|
||
if collection is None:
|
||
collection = bpy.context.collection
|
||
collection.objects.link(obj)
|
||
|
||
# 设为活动选中对象
|
||
bpy.context.view_layer.objects.active = obj
|
||
obj.select_set(True)
|
||
|
||
print(f"[VDE] 创建了 '{name}': "
|
||
f"{len(vertices)} 顶点, {len(faces)} 面")
|
||
|
||
return obj
|
||
|
||
|
||
def blender_mesh_to_bridge_data(obj):
|
||
"""
|
||
将 Blender 对象网格数据转换为桥接 JSON 格式。
|
||
|
||
Args:
|
||
obj: Blender Object (须为 MESH 类型)
|
||
|
||
Returns:
|
||
dict: {"vertices": [...], "faces": [...]}
|
||
"""
|
||
import bpy
|
||
|
||
if obj.type != 'MESH':
|
||
raise RuntimeError(f"对象 '{obj.name}' 不是 MESH 类型 (是 {obj.type})")
|
||
|
||
# 获取世界空间坐标
|
||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||
eval_obj = obj.evaluated_get(depsgraph)
|
||
mesh = eval_obj.to_mesh()
|
||
|
||
vertices = []
|
||
for v in mesh.vertices:
|
||
co = v.co
|
||
vertices.append([co.x, co.y, co.z])
|
||
|
||
# 三角剖分(布尔运算需要三角形网格)
|
||
faces = []
|
||
for poly in mesh.polygons:
|
||
if len(poly.vertices) == 3:
|
||
faces.append(list(poly.vertices))
|
||
elif len(poly.vertices) == 4:
|
||
faces.append([poly.vertices[0], poly.vertices[1], poly.vertices[2]])
|
||
faces.append([poly.vertices[0], poly.vertices[2], poly.vertices[3]])
|
||
else:
|
||
for i in range(1, len(poly.vertices) - 1):
|
||
faces.append([
|
||
poly.vertices[0],
|
||
poly.vertices[i],
|
||
poly.vertices[i + 1]
|
||
])
|
||
|
||
eval_obj.to_mesh_clear()
|
||
|
||
return {"vertices": vertices, "faces": faces}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# VDE 可用性检测
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def check_vde_available(prefs):
|
||
"""
|
||
检测 VDE Python 库是否可用。
|
||
|
||
Args:
|
||
prefs: 插件偏好设置(含 vde_lib_path)
|
||
|
||
Returns:
|
||
tuple: (available: bool, message: str)
|
||
"""
|
||
vde_path = prefs.vde_lib_path if prefs else ""
|
||
test_script = """import sys
|
||
vde_path = %r
|
||
if vde_path:
|
||
sys.path.insert(0, vde_path)
|
||
try:
|
||
from vde import brep, mesh, sdf
|
||
print("OK")
|
||
except ImportError as e:
|
||
print("ERROR: " + str(e))
|
||
"""
|
||
try:
|
||
proc = subprocess.run(
|
||
[sys.executable, "-c", test_script % vde_path],
|
||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||
universal_newlines=True, timeout=10,
|
||
env={**os.environ, "PYTHONUNBUFFERED": "1"},
|
||
)
|
||
output = proc.stdout.strip()
|
||
if output == "OK":
|
||
return True, "VDE 库可用"
|
||
else:
|
||
return False, output
|
||
except Exception as e:
|
||
return False, str(e)
|