Files
ViewDesignEngine/blender_addon/vde_bridge.py
T
茂之钳 5e2812a6f3
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 35s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
feat(v6.2): Blender addon + .NET/C# bindings + developer docs + plugin system
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)
2026-07-26 22:24:40 +08:00

502 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)