refactor: replace STL intermediary with OCC-native STEP→GLB pipeline
- export_step_to_gltf.py: STEP→GLB via RWGltf_CafWriter + BRepBuilderAPI_Transform (mm→m pre-scaling, XCAFDoc_ShapeTool.GetComponents_s static method) - Blender scripts (blender_render.py, still_render.py, turntable_render.py, export_gltf.py, export_blend.py): import GLB instead of STL, remove _scale_mm_to_m - step_tasks.py: add generate_gltf_production_task, remove generate_stl_cache, replace _bbox_from_stl with _bbox_from_glb (trimesh), auto-queue geometry GLB after thumbnail render - render_blender.py: replace _stl_from_cache_or_convert with _glb_from_step, remove convert_step_to_stl and export_per_part_stls - domains/rendering/tasks.py: update render_turntable_task, export_gltf/blend tasks to use GLB instead of STL - cad.py: remove STL download/generate endpoints, add generate-gltf-production - admin.py: generate-missing-stls → generate-missing-geometry-glbs - Frontend: replace STL cache UI with GLB generate buttons, remove stl_cached field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""OCC-native STEP → GLB export script.
|
||||
|
||||
Reads a STEP file via OCP/XCAF (preserving part names and embedded colors),
|
||||
tessellates with BRepMesh, optionally applies per-part hex colors, and writes
|
||||
a binary GLB in meters (Y-up, glTF convention).
|
||||
|
||||
No Blender required. Uses the same OCP bindings that cadquery ships with.
|
||||
|
||||
Usage:
|
||||
python3 export_step_to_gltf.py \
|
||||
--step_path /path/to/file.stp \
|
||||
--output_path /path/to/output.glb \
|
||||
[--linear_deflection 0.1] \
|
||||
[--angular_deflection 0.5] \
|
||||
[--color_map '{"RingInner": "#4C9BE8", "RingOuter": "#E85B4C"}']
|
||||
|
||||
Exit 0 on success, exit 1 on failure.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
PALETTE_HEX = [
|
||||
"#4C9BE8", "#E85B4C", "#4CBE72", "#E8A84C", "#A04CE8",
|
||||
"#4CD4E8", "#E84CA8", "#7EC850", "#E86B30", "#5088C8",
|
||||
]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--step_path", required=True)
|
||||
parser.add_argument("--output_path", required=True)
|
||||
parser.add_argument(
|
||||
"--linear_deflection", type=float, default=0.1,
|
||||
help="OCC linear deflection for tessellation (mm). Smaller = finer mesh. Default 0.1",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--angular_deflection", type=float, default=0.5,
|
||||
help="OCC angular deflection (radians). Default 0.5",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--color_map", default="{}",
|
||||
help='JSON dict mapping part name → hex color, e.g. \'{"Ring": "#4C9BE8"}\'',
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _hex_to_occ_color(hex_color: str):
|
||||
"""Convert '#RRGGBB' → Quantity_Color (linear float)."""
|
||||
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
|
||||
h = hex_color.lstrip("#")
|
||||
if len(h) < 6:
|
||||
return Quantity_Color(0.7, 0.7, 0.7, Quantity_TOC_RGB)
|
||||
r = int(h[0:2], 16) / 255.0
|
||||
g = int(h[2:4], 16) / 255.0
|
||||
b = int(h[4:6], 16) / 255.0
|
||||
return Quantity_Color(r, g, b, Quantity_TOC_RGB)
|
||||
|
||||
|
||||
def _apply_color_map(shape_tool, color_tool, free_labels, color_map: dict) -> None:
|
||||
"""Apply hex colors from color_map to matching shapes by name (case-insensitive substring)."""
|
||||
from OCP.TDF import TDF_LabelSequence
|
||||
from OCP.TDataStd import TDataStd_Name
|
||||
from OCP.XCAFDoc import XCAFDoc_ShapeTool
|
||||
|
||||
# XCAFDoc_ColorType: XCAFDoc_ColorGen=0, XCAFDoc_ColorSurf=1, XCAFDoc_ColorCurv=2
|
||||
try:
|
||||
from OCP.XCAFDoc import XCAFDoc_ColorSurf as COLOR_SURF
|
||||
except ImportError:
|
||||
COLOR_SURF = 1 # integer fallback
|
||||
|
||||
def _visit(label) -> None:
|
||||
name_attr = TDataStd_Name()
|
||||
name = ""
|
||||
if label.FindAttribute(TDataStd_Name.GetID_s(), name_attr):
|
||||
name = name_attr.Get().ToExtString()
|
||||
|
||||
if name:
|
||||
for part_name, hex_color in color_map.items():
|
||||
if part_name.lower() in name.lower() or name.lower() in part_name.lower():
|
||||
color_tool.SetColor(label, _hex_to_occ_color(hex_color), COLOR_SURF)
|
||||
break
|
||||
|
||||
components = TDF_LabelSequence()
|
||||
XCAFDoc_ShapeTool.GetComponents_s(label, components)
|
||||
for i in range(1, components.Length() + 1):
|
||||
_visit(components.Value(i))
|
||||
|
||||
for i in range(1, free_labels.Length() + 1):
|
||||
_visit(free_labels.Value(i))
|
||||
|
||||
|
||||
def _apply_palette_colors(shape_tool, color_tool, free_labels) -> None:
|
||||
"""Assign palette colors to leaf shapes when no color_map is provided."""
|
||||
from OCP.TDF import TDF_LabelSequence
|
||||
from OCP.XCAFDoc import XCAFDoc_ShapeTool
|
||||
|
||||
try:
|
||||
from OCP.XCAFDoc import XCAFDoc_ColorSurf as COLOR_SURF
|
||||
except ImportError:
|
||||
COLOR_SURF = 1
|
||||
|
||||
leaves: list = []
|
||||
|
||||
def _collect(label) -> None:
|
||||
components = TDF_LabelSequence()
|
||||
XCAFDoc_ShapeTool.GetComponents_s(label, components)
|
||||
if components.Length() == 0:
|
||||
leaves.append(label)
|
||||
else:
|
||||
for i in range(1, components.Length() + 1):
|
||||
_collect(components.Value(i))
|
||||
|
||||
for i in range(1, free_labels.Length() + 1):
|
||||
_collect(free_labels.Value(i))
|
||||
|
||||
for idx, label in enumerate(leaves):
|
||||
occ_color = _hex_to_occ_color(PALETTE_HEX[idx % len(PALETTE_HEX)])
|
||||
color_tool.SetColor(label, occ_color, COLOR_SURF)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
color_map: dict = json.loads(args.color_map)
|
||||
|
||||
from OCP.STEPCAFControl import STEPCAFControl_Reader
|
||||
from OCP.TDocStd import TDocStd_Document
|
||||
from OCP.XCAFApp import XCAFApp_Application
|
||||
from OCP.XCAFDoc import XCAFDoc_DocumentTool
|
||||
from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString
|
||||
from OCP.TDF import TDF_LabelSequence
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.IFSelect import IFSelect_RetDone
|
||||
from OCP.Message import Message_ProgressRange
|
||||
|
||||
# --- Init XDE document ---
|
||||
app = XCAFApp_Application.GetApplication_s()
|
||||
doc = TDocStd_Document(TCollection_ExtendedString("MDTV-CAF"))
|
||||
app.InitDocument(doc)
|
||||
|
||||
# --- Read STEP into XDE (preserves part names + embedded colors) ---
|
||||
reader = STEPCAFControl_Reader()
|
||||
reader.SetNameMode(True)
|
||||
reader.SetColorMode(True)
|
||||
reader.SetLayerMode(True)
|
||||
status = reader.ReadFile(args.step_path)
|
||||
if status != IFSelect_RetDone:
|
||||
print(f"ERROR: STEPCAFControl_Reader failed (status={status})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
reader.Transfer(doc)
|
||||
|
||||
shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
|
||||
color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
|
||||
|
||||
# --- Tessellate all free shapes ---
|
||||
free_labels = TDF_LabelSequence()
|
||||
shape_tool.GetFreeShapes(free_labels)
|
||||
print(f"Found {free_labels.Length()} root shape(s), tessellating "
|
||||
f"(linear={args.linear_deflection}mm, angular={args.angular_deflection}rad) …")
|
||||
|
||||
for i in range(1, free_labels.Length() + 1):
|
||||
shape = shape_tool.GetShape_s(free_labels.Value(i))
|
||||
if not shape.IsNull():
|
||||
BRepMesh_IncrementalMesh(
|
||||
shape,
|
||||
args.linear_deflection,
|
||||
False, # isRelative
|
||||
args.angular_deflection,
|
||||
True, # isInParallel
|
||||
)
|
||||
|
||||
# --- Apply colors ---
|
||||
if color_map:
|
||||
_apply_color_map(shape_tool, color_tool, free_labels, color_map)
|
||||
print(f"Applied color_map ({len(color_map)} entries)")
|
||||
else:
|
||||
_apply_palette_colors(shape_tool, color_tool, free_labels)
|
||||
print("Applied palette colors (no color_map provided)")
|
||||
|
||||
# --- Scale shapes mm → m before GLB export ---
|
||||
# RWMesh_CoordinateSystemConverter is not wrapped in OCP Python bindings.
|
||||
# Pre-scale each free shape by 0.001 (mm → m) using BRepBuilderAPI_Transform.
|
||||
from OCP.gp import gp_Trsf
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||||
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetScaleFactor(0.001)
|
||||
|
||||
for i in range(1, free_labels.Length() + 1):
|
||||
label = free_labels.Value(i)
|
||||
orig_shape = shape_tool.GetShape_s(label)
|
||||
if not orig_shape.IsNull():
|
||||
scaled = BRepBuilderAPI_Transform(orig_shape, trsf, True).Shape()
|
||||
shape_tool.SetShape(label, scaled)
|
||||
|
||||
print("Shapes scaled mm → m")
|
||||
|
||||
# --- Export GLB via RWGltf_CafWriter ---
|
||||
from OCP.RWGltf import RWGltf_CafWriter
|
||||
|
||||
writer = RWGltf_CafWriter(TCollection_AsciiString(args.output_path), True) # True = binary GLB
|
||||
# Z-up → Y-up rotation is applied by RWGltf_CafWriter by default (OCC 7.6+).
|
||||
|
||||
# Perform export
|
||||
try:
|
||||
from OCP.TColStd import TColStd_IndexedDataMapOfStringString
|
||||
metadata = TColStd_IndexedDataMapOfStringString()
|
||||
ok = writer.Perform(doc, metadata, Message_ProgressRange())
|
||||
except TypeError:
|
||||
# Older API without metadata dict
|
||||
ok = writer.Perform(doc, Message_ProgressRange())
|
||||
|
||||
out = Path(args.output_path)
|
||||
if not ok or not out.exists() or out.stat().st_size == 0:
|
||||
print(f"ERROR: RWGltf_CafWriter.Perform returned ok={ok}, file exists={out.exists()}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"GLB exported: {out.name} ({out.stat().st_size // 1024} KB)")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user