Compare commits
2 Commits
dd215b6679
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acbf6f2db0 | ||
|
|
bd1bb1a77c |
41
occt-service/README.md
Normal file
41
occt-service/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# OCCT 转换服务
|
||||||
|
|
||||||
|
把 STEP 文件转成 glTF/GLB,供网页 3D 轻量化预览用。**只做转换,不做参数化建模**(升级 2 已决策不做)。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- `OCP`(OpenCASCADE Python 绑定,本机已验证可用)
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
|
```
|
||||||
|
start-service.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
或命令行:`python occt_service.py`(端口默认 8090,可用环境变量 `OCCT_PORT` 改)。
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
| 接口 | 方法 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `/health` | GET | 存活探测 → `{"ok":true,"port":8090}` |
|
||||||
|
| `/convert` | POST | 请求体 = STEP 文件二进制,query `deflection=0.5`(弦偏差,越大越轻)→ 返回 GLB 二进制 |
|
||||||
|
|
||||||
|
## 调用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST --data-binary @model.step "http://localhost:8090/convert?deflection=0.5" -o model.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计说明
|
||||||
|
|
||||||
|
- 零新依赖:HTTP 用标准库 `http.server`,几何用 OCP。
|
||||||
|
- 单线程:OCP 非线程安全,避免并发崩溃。
|
||||||
|
- 临时文件用完即删,不落盘。
|
||||||
|
- deflection 默认 0.5mm(屏幕预览够用,体积小);要更精细可调小到 0.1,要更小可调大到 2.0。
|
||||||
|
|
||||||
|
## 与主项目的关系
|
||||||
|
|
||||||
|
- 这是升级 1(网页 3D 轻量化)的转换后端,独立进程、独立端口,不依赖 NX。
|
||||||
|
- 生产部署时可放服务器(服务器无需装 NX,OCP 是纯 Python 库)。
|
||||||
82
occt-service/batch-convert-glb.py
Normal file
82
occt-service/batch-convert-glb.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
批量转换:把 catalog\step\ 下所有 STEP 转成 glTF/GLB(网页 3D 轻量化用)。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python batch-convert-glb.py <step目录> <gltf输出目录> [deflection]
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 走本地 OCCT 服务的 /convert 接口(需先启动 occt_service.py)
|
||||||
|
- 同名 .step → .glb,断点续传(已存在且非空的 .glb 跳过)
|
||||||
|
- 单线程、逐文件,避免给服务造成压力
|
||||||
|
- 默认 deflection=0.5(屏幕预览够用)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
OCCT_URL = os.environ.get("OCCT_URL", "http://localhost:8090")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_one(step_path, glb_path, deflection):
|
||||||
|
with open(step_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"%s/convert?deflection=%s" % (OCCT_URL, deflection),
|
||||||
|
data=data, method="POST",
|
||||||
|
headers={"Content-Type": "application/octet-stream"},
|
||||||
|
)
|
||||||
|
resp = urllib.request.urlopen(req, timeout=120)
|
||||||
|
out = resp.read()
|
||||||
|
if not out or out[:4] != b"glTF":
|
||||||
|
raise RuntimeError("转换失败(非法 glb): %s" % step_path)
|
||||||
|
# 原子写:先写临时再改名
|
||||||
|
tmp = glb_path + ".tmp"
|
||||||
|
with open(tmp, "wb") as f:
|
||||||
|
f.write(out)
|
||||||
|
os.replace(tmp, glb_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("用法: python batch-convert-glb.py <step目录> <gltf输出目录> [deflection]")
|
||||||
|
sys.exit(1)
|
||||||
|
step_dir = sys.argv[1]
|
||||||
|
out_dir = sys.argv[2]
|
||||||
|
deflection = float(sys.argv[3]) if len(sys.argv) > 3 else 0.5
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
steps = []
|
||||||
|
for root, _, files in os.walk(step_dir):
|
||||||
|
for fn in files:
|
||||||
|
if fn.lower().endswith((".step", ".stp")):
|
||||||
|
steps.append(os.path.join(root, fn))
|
||||||
|
steps.sort()
|
||||||
|
|
||||||
|
ok = 0
|
||||||
|
skip = 0
|
||||||
|
fail = 0
|
||||||
|
t0 = time.time()
|
||||||
|
for i, sp in enumerate(steps):
|
||||||
|
base = os.path.splitext(os.path.basename(sp))[0]
|
||||||
|
gp = os.path.join(out_dir, base + ".glb")
|
||||||
|
if os.path.exists(gp) and os.path.getsize(gp) > 0:
|
||||||
|
skip += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
convert_one(sp, gp, deflection)
|
||||||
|
ok += 1
|
||||||
|
if (ok + skip) % 50 == 0:
|
||||||
|
print("[%d/%d] ok=%d skip=%d fail=%d %.1fs" % (i + 1, len(steps), ok, skip, fail, time.time() - t0))
|
||||||
|
except Exception as e:
|
||||||
|
fail += 1
|
||||||
|
print("[FAIL] %s: %s" % (os.path.basename(sp), e))
|
||||||
|
|
||||||
|
print("完成: 总 %d, 转换 %d, 跳过 %d, 失败 %d, 耗时 %.1fs" % (len(steps), ok, skip, fail, time.time() - t0))
|
||||||
|
if fail:
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
165
occt-service/occt_service.py
Normal file
165
occt-service/occt_service.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
OCCT 转换服务(常驻 HTTP,默认端口 8090)
|
||||||
|
|
||||||
|
只做一件事:读 STEP 文件 → 三角化 → 导出 glTF/GLB(网页 3D 轻量化用)。
|
||||||
|
零新依赖:HTTP 用标准库 http.server,几何内核用 OCP(OpenCASCADE Python 绑定)。
|
||||||
|
|
||||||
|
接口:
|
||||||
|
GET /health 存活探测 → {"ok":true,"port":8090}
|
||||||
|
POST /convert 请求体 = STEP 文件二进制(raw body),
|
||||||
|
query 参数 deflection=弦偏差(默认 0.5) → 返回 GLB 二进制
|
||||||
|
失败返回 JSON {"error": "..."}
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 单线程 HTTPServer:OCP 是 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
|
||||||
|
# XCAF(glTF 导出用)
|
||||||
|
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()
|
||||||
5
occt-service/start-service.bat
Normal file
5
occt-service/start-service.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
echo Starting OCCT conversion service on port 8090 ...
|
||||||
|
python occt_service.py
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user