Files
OnebotCatalog/occt-service/occt_service.py

166 lines
5.8 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.

# -*- coding: utf-8 -*-
"""
OCCT 转换服务(常驻 HTTP默认端口 8090
只做一件事:读 STEP 文件 → 三角化 → 导出 glTF/GLB网页 3D 轻量化用)。
零新依赖HTTP 用标准库 http.server几何内核用 OCPOpenCASCADE Python 绑定)。
接口:
GET /health 存活探测 → {"ok":true,"port":8090}
POST /convert 请求体 = STEP 文件二进制raw body
query 参数 deflection=弦偏差(默认 0.5) → 返回 GLB 二进制
失败返回 JSON {"error": "..."}
说明:
- 单线程 HTTPServerOCP 是 C++ 绑定、非线程安全,避免并发导致崩溃。
- 每次转换写临时文件,转换完清理。
- 本服务只做「STEP→glTF」转换不做参数化建模升级2 已决策不做)。
"""
import os
import sys
import time
import tempfile
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from OCP.STEPControl import STEPControl_Reader
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.IFSelect import IFSelect_RetDone
# XCAFglTF 导出用)
from OCP.XCAFApp import XCAFApp_Application
from OCP.TCollection import TCollection_ExtendedString
from OCP.TDocStd import TDocStd_Document
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool
from OCP.RWGltf import RWGltf_CafWriter
from OCP.TColStd import TColStd_IndexedDataMapOfStringString
from OCP.Message import Message_ProgressRange
PORT = int(os.environ.get("OCCT_PORT", "8090"))
def read_step(path):
"""读 STEP 文件 → TopoDS_Shape"""
reader = STEPControl_Reader()
status = reader.ReadFile(path)
if status != IFSelect_RetDone:
raise RuntimeError("STEP 读取失败 status=%s" % status)
reader.TransferRoots()
return reader.OneShape()
def tessellate(shape, deflection):
"""三角化(弦偏差 deflection 控制精度/体积平衡)"""
mesh = BRepMesh_IncrementalMesh(shape, deflection, False, 0.5, True)
mesh.Perform()
return shape
def export_glb(shape, out_path, deflection=0.5):
"""shape → 二进制 GLB经 XCAF 文档承载)"""
tessellate(shape, deflection)
app = XCAFApp_Application.GetApplication_s()
doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))
app.InitDocument(doc)
st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
lbl = st.NewShape()
st.SetShape(lbl, shape)
w = RWGltf_CafWriter(out_path, True) # True = 二进制 GLB
info = TColStd_IndexedDataMapOfStringString()
prog = Message_ProgressRange()
return w.Perform(doc, info, prog)
class Handler(BaseHTTPRequestHandler):
server_version = "occt-service/1.0"
def log_message(self, fmt, *args):
sys.stderr.write("[%s] %s\n" % (time.strftime("%H:%M:%S"), fmt % args))
def _json(self, code, obj):
import json
body = json.dumps(obj).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _send_glb(self, path):
size = os.path.getsize(path)
self.send_response(200)
self.send_header("Content-Type", "model/gltf-binary")
self.send_header("Content-Disposition", 'attachment; filename="model.glb"')
self.send_header("Content-Length", str(size))
self.end_headers()
with open(path, "rb") as f:
while True:
chunk = f.read(65536)
if not chunk:
break
self.wfile.write(chunk)
def do_GET(self):
path = urlparse(self.path).path
if path == "/health":
self._json(200, {"ok": True, "port": PORT})
else:
self._json(404, {"error": "not_found"})
def do_POST(self):
parsed = urlparse(self.path)
if parsed.path != "/convert":
self._json(404, {"error": "not_found"})
return
qs = parse_qs(parsed.query)
deflection = 0.5
if "deflection" in qs:
try:
deflection = float(qs["deflection"][0])
deflection = max(0.05, min(10.0, deflection))
except ValueError:
pass
# 读 raw body整个请求体 = STEP 文件)
length = int(self.headers.get("Content-Length", "0"))
if length == 0:
self._json(400, {"error": "empty_body"})
return
data = self.rfile.read(length)
tmp_step = tempfile.mktemp(suffix=".step")
tmp_glb = tempfile.mktemp(suffix=".glb")
try:
with open(tmp_step, "wb") as f:
f.write(data)
t0 = time.time()
shape = read_step(tmp_step)
ok = export_glb(shape, tmp_glb, deflection)
if not ok:
self._json(500, {"error": "gltf_export_failed"})
return
elapsed = time.time() - t0
self.log_message("convert OK %.2fs deflection=%.2f size=%d", elapsed, deflection, os.path.getsize(tmp_glb))
self._send_glb(tmp_glb)
except Exception as e:
self._json(500, {"error": str(e)[:200]})
finally:
for p in (tmp_step, tmp_glb):
try:
if os.path.exists(p):
os.remove(p)
except OSError:
pass
def main():
server = HTTPServer(("0.0.0.0", PORT), Handler)
sys.stderr.write("OCCT 转换服务已启动: http://0.0.0.0:%d (/convert, /health)\n" % PORT)
sys.stderr.flush()
try:
server.serve_forever()
except KeyboardInterrupt:
sys.stderr.write("\n服务已停止\n")
if __name__ == "__main__":
main()