furniture-3d-system-Wu/docker/oda-converter-server.py

405 lines
15 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.

"""
DWG 转换服务 REST API(ODA 方案)
使用 ODA File Converter 将 DWG 转换为 OBJ,再用 obj2gltf 转为 GLB
端点:
POST /convert - 上传 DWG 文件并转换
GET /health - 健康检查
依赖:
- ODA File Converter (需手动安装)
- obj2gltf (npm install -g obj2gltf)
"""
import os
import sys
import uuid
import json
import subprocess
import tempfile
import shutil
from pathlib import Path
from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 配置
UPLOAD_FOLDER = '/app/uploads'
OUTPUT_FOLDER = '/app/outputs'
ALLOWED_EXTENSIONS = {'dwg', 'stl', 'obj'}
MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB
# ODA File Converter 路径(Linux)
ODA_CONVERTER = os.environ.get('ODA_CONVERTER_PATH', '/usr/bin/ODAFileConverter')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
def allowed_file(filename):
"""检查文件扩展名"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def check_oda_available():
"""检查 ODA File Converter 是否可用"""
return os.path.exists(ODA_CONVERTER) or shutil.which('ODAFileConverter')
def convert_dwg_to_glb(input_path: str, output_path: str) -> dict:
"""
使用 ODA 将 DWG 转换为 GLB
流程: DWG -> DXF (ODA) -> 解析所有实体类型 -> OBJ -> GLB (obj2gltf)
"""
result = {
'success': False,
'error': None,
'stats': {}
}
temp_dir = tempfile.mkdtemp(prefix='dwg_convert_')
try:
input_dir = os.path.dirname(input_path)
input_name = os.path.basename(input_path)
# Step 1: DWG -> DXF (使用 ODA File Converter)
oda_path = ODA_CONVERTER if os.path.exists(ODA_CONVERTER) else shutil.which('ODAFileConverter')
if not oda_path:
result['error'] = 'ODA File Converter 未安装'
return result
cmd = [
oda_path,
input_dir,
temp_dir,
'ACAD2018',
'DXF',
'0',
'1',
input_name
]
print(f'[ODA] 执行命令: {" ".join(cmd)}')
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if proc.returncode != 0:
result['error'] = f'ODA 转换失败: {proc.stderr or proc.stdout}'
return result
# 查找生成的 DXF 文件
dxf_files = list(Path(temp_dir).glob('*.dxf'))
if not dxf_files:
result['error'] = 'ODA 未生成 DXF 文件'
return result
dxf_path = str(dxf_files[0])
print(f'[ODA] DXF 文件生成: {dxf_path}')
# Step 2: DXF -> OBJ (使用 ezdxf 提取所有 3D 实体)
obj_path = os.path.join(temp_dir, 'model.obj')
try:
import ezdxf
from ezdxf.addons import meshex
import trimesh
import numpy as np
doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()
all_meshes = []
entity_stats = {}
# 遍历所有实体
for entity in msp:
etype = entity.dxftype()
entity_stats[etype] = entity_stats.get(etype, 0) + 1
try:
if etype == '3DFACE':
pts = [entity.dxf.vtx0, entity.dxf.vtx1, entity.dxf.vtx2]
if hasattr(entity.dxf, 'vtx3') and entity.dxf.vtx3 != entity.dxf.vtx2:
pts.append(entity.dxf.vtx3)
verts = [[p[0], p[1], p[2] if len(p) > 2 else 0] for p in pts]
if len(pts) == 3:
faces = [[0, 1, 2]]
else:
faces = [[0, 1, 2], [0, 2, 3]]
mesh = trimesh.Trimesh(vertices=verts, faces=faces)
all_meshes.append(mesh)
elif etype == '3DSOLID':
# 3D 实体 - 尝试使用 ezdxf 的 meshex 提取
try:
from ezdxf.render import MeshBuilder
builder = MeshBuilder()
entity.render_mesh(builder)
if builder.vertices:
mesh = trimesh.Trimesh(
vertices=np.array(builder.vertices),
faces=np.array(builder.faces) if builder.faces else []
)
all_meshes.append(mesh)
except Exception as e:
print(f'[ODA] 3DSOLID 提取失败: {e}')
elif etype == 'MESH':
try:
verts = list(entity.vertices)
faces_data = list(entity.faces)
if verts and faces_data:
mesh = trimesh.Trimesh(
vertices=np.array(verts),
faces=np.array([f[:3] for f in faces_data if len(f) >= 3])
)
all_meshes.append(mesh)
except Exception as e:
print(f'[ODA] MESH 提取失败: {e}')
elif etype == 'POLYFACE':
# Polyface mesh
try:
from ezdxf.entities import Polyface
if hasattr(entity, 'vertices') and hasattr(entity, 'faces'):
verts = [list(v.dxf.location) for v in entity.vertices]
faces_data = [list(f.indices()) for f in entity.faces]
if verts and faces_data:
mesh = trimesh.Trimesh(
vertices=np.array(verts),
faces=np.array([f[:3] for f in faces_data if len(f) >= 3])
)
all_meshes.append(mesh)
except Exception as e:
print(f'[ODA] POLYFACE 提取失败: {e}')
elif etype in ('SURFACE', 'PLANESURFACE'):
# 曲面实体
try:
from ezdxf.render import MeshBuilder
builder = MeshBuilder()
entity.render_mesh(builder)
if builder.vertices:
mesh = trimesh.Trimesh(
vertices=np.array(builder.vertices),
faces=np.array(builder.faces) if builder.faces else []
)
all_meshes.append(mesh)
except Exception as e:
print(f'[ODA] SURFACE 提取失败: {e}')
except Exception as e:
print(f'[ODA] 实体 {etype} 处理异常: {e}')
continue
print(f'[ODA] 实体统计: {entity_stats}')
print(f'[ODA] 成功提取 {len(all_meshes)} 个网格')
if not all_meshes:
# 检查是否有 3DSOLID 实体
if entity_stats.get('3DSOLID', 0) > 0:
result['error'] = (
f'检测到 {entity_stats["3DSOLID"]} 个 3DSOLID 实体(布尔运算模型)。\n'
'3DSOLID 格式暂不支持自动转换,请在 AutoCAD 中执行以下操作:\n'
'1. 选择所有实体 → 输入命令 EXPORT → 选择 STL 或 OBJ 格式导出\n'
'2. 或使用 FACETRES 命令调整精度后导出\n'
'3. 然后上传导出的 STL/GLB 文件'
)
else:
result['error'] = f'DXF 文件中没有可转换的 3D 几何体。实体类型: {entity_stats}'
return result
# 合并所有网格
combined = trimesh.util.concatenate(all_meshes)
combined.export(obj_path)
print(f'[ODA] OBJ 文件生成: {obj_path}')
result['stats']['mesh_count'] = len(all_meshes)
result['stats']['vertex_count'] = len(combined.vertices)
result['stats']['face_count'] = len(combined.faces)
except ImportError as e:
result['error'] = f'依赖库未安装: {e}'
return result
except Exception as e:
result['error'] = f'DXF 解析失败: {str(e)}'
import traceback
print(traceback.format_exc())
return result
# Step 3: OBJ -> GLB (使用 obj2gltf)
obj2gltf_path = shutil.which('obj2gltf')
if not obj2gltf_path:
obj2gltf_path = 'npx'
cmd = [obj2gltf_path, 'obj2gltf', '-i', obj_path, '-o', output_path, '-b']
else:
cmd = [obj2gltf_path, '-i', obj_path, '-o', output_path, '-b']
print(f'[ODA] 执行 obj2gltf: {" ".join(cmd)}')
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if proc.returncode != 0 or not os.path.exists(output_path):
result['error'] = f'obj2gltf 转换失败: {proc.stderr or proc.stdout}'
return result
output_size = os.path.getsize(output_path)
result['stats']['output_size_bytes'] = output_size
result['stats']['output_size_mb'] = round(output_size / (1024 * 1024), 2)
result['success'] = True
print(f'[ODA] 转换成功: {output_path} ({result["stats"]["output_size_mb"]} MB)')
except subprocess.TimeoutExpired:
result['error'] = '转换超时'
except Exception as e:
result['error'] = f'转换过程出错: {str(e)}'
import traceback
print(traceback.format_exc())
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
return result
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查"""
oda_available = check_oda_available()
return jsonify({
'status': 'healthy' if oda_available else 'degraded',
'service': 'dwg-converter-oda',
'version': '1.0.0',
'oda_available': oda_available,
'oda_path': ODA_CONVERTER if os.path.exists(ODA_CONVERTER) else shutil.which('ODAFileConverter')
})
def convert_stl_obj_to_glb(input_path: str, output_path: str) -> dict:
"""
将 STL/OBJ 文件转换为 GLB
"""
result = {
'success': False,
'error': None,
'stats': {}
}
try:
file_ext = os.path.splitext(input_path)[1].lower()
# 使用 obj2gltf 转换
obj2gltf_path = shutil.which('obj2gltf')
if file_ext == '.stl':
# STL -> OBJ -> GLB (使用 trimesh)
import trimesh
mesh = trimesh.load(input_path)
temp_obj = input_path.replace('.stl', '.obj')
mesh.export(temp_obj)
if obj2gltf_path:
cmd = [obj2gltf_path, '-i', temp_obj, '-o', output_path, '-b']
else:
cmd = ['npx', 'obj2gltf', '-i', temp_obj, '-o', output_path, '-b']
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
os.remove(temp_obj)
elif file_ext == '.obj':
# OBJ -> GLB
if obj2gltf_path:
cmd = [obj2gltf_path, '-i', input_path, '-o', output_path, '-b']
else:
cmd = ['npx', 'obj2gltf', '-i', input_path, '-o', output_path, '-b']
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if os.path.exists(output_path):
output_size = os.path.getsize(output_path)
result['stats']['output_size_bytes'] = output_size
result['stats']['output_size_mb'] = round(output_size / (1024 * 1024), 2)
result['success'] = True
print(f'[Converter] 转换成功: {output_path}')
else:
result['error'] = f'转换失败: {proc.stderr or proc.stdout}'
except Exception as e:
result['error'] = f'转换出错: {str(e)}'
import traceback
print(traceback.format_exc())
return result
@app.route('/convert', methods=['POST'])
def convert():
"""上传 DWG/STL/OBJ 文件并转换为 GLB"""
# 检查文件
if 'file' not in request.files:
return jsonify({'error': '未提供文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '未选择文件'}), 400
if not allowed_file(file.filename):
return jsonify({'error': '不支持的文件格式,支持 .dwg/.stl/.obj'}), 400
# 生成文件路径
task_id = str(uuid.uuid4())
filename = secure_filename(file.filename)
input_path = os.path.join(UPLOAD_FOLDER, f'{task_id}_{filename}')
output_path = os.path.join(OUTPUT_FOLDER, f'{task_id}.glb')
# 保存上传文件
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
file.save(input_path)
file_ext = os.path.splitext(filename)[1].lower()
try:
# 根据文件类型选择转换方法
if file_ext == '.dwg':
# 检查 ODA 是否可用
if not check_oda_available():
return jsonify({
'error': 'ODA File Converter 未安装'
}), 503
result = convert_dwg_to_glb(input_path, output_path)
else:
# STL/OBJ 直接转换
result = convert_stl_obj_to_glb(input_path, output_path)
if result['success']:
return send_file(
output_path,
mimetype='model/gltf-binary',
as_attachment=True,
download_name='converted.glb'
)
else:
return jsonify({'error': result['error']}), 500
finally:
# 清理输入文件
if os.path.exists(input_path):
os.remove(input_path)
if __name__ == '__main__':
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
app.run(host='0.0.0.0', port=3001, debug=True)