95cfe0aa93
- 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>
233 lines
8.1 KiB
Python
233 lines
8.1 KiB
Python
"""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)
|