Files
HartOMat/render-worker/scripts/export_gltf.py
T
Hartmut bfd58e3419 fix: media thumbnails, product dimensions, inline 3D viewer, GLB export
Bug A: Media Library thumbnails were gray because <img src> cannot send
JWT auth headers. Added useAuthBlob() hook (fetch + createObjectURL) in
MediaBrowser.tsx. Also fixed publish_asset Celery task to populate
product_id + cad_file_id on MediaAsset for thumbnail fallback resolution.

Bug B: Product dimensions now shown in Product Details card with Ruler
icon and "from CAD" label when cad_mesh_attributes.dimensions_mm exists.

Bug C: Replaced 128×128 CAD thumbnail with InlineCadViewer component.
Queries gltf_geometry MediaAssets, fetches GLB via auth fetch → blob URL
→ Three.js Canvas with OrbitControls. Falls back to thumbnail + "Load 3D
Model" button. Polling when GLB generation is in progress.

Bug D: trimesh was in [cad] optional extra but Dockerfile only installed
[dev]. Changed to pip install -e ".[dev,cad]" — trimesh now available in
backend container, GLB + Colors export works.

Also added bbox extraction (STL-first numpy parsing) in render_step_thumbnail
and admin "Re-extract CAD Metadata" bulk endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 13:27:46 +01:00

127 lines
4.1 KiB
Python

"""Blender headless script: export a STEP-derived scene as a production GLB.
Usage:
blender --background --python export_gltf.py -- \\
--stl_path /path/to/file.stl \\
--output_path /path/to/output.glb \\
[--asset_library_blend /path/to/library.blend] \\
[--material_map '{"SrcMat": "LibMat"}']
The script:
1. Imports the STL file (with mm→m scale).
2. Optionally applies asset library materials from a .blend.
3. Exports as GLB (Draco-compressed if available, otherwise standard).
"""
from __future__ import annotations
import argparse
import json
import sys
import traceback
def parse_args() -> argparse.Namespace:
argv = sys.argv
if "--" not in argv:
print("No arguments after --", file=sys.stderr)
sys.exit(1)
rest = argv[argv.index("--") + 1:]
parser = argparse.ArgumentParser()
parser.add_argument("--stl_path", required=True)
parser.add_argument("--output_path", required=True)
parser.add_argument("--asset_library_blend", default=None)
parser.add_argument("--material_map", default="{}")
parser.add_argument("--sharp_edges_json", default="[]",
help="JSON array of [x, y, z] midpoints (mm) to mark as sharp edges")
return parser.parse_args(rest)
def mark_sharp_edges_by_proximity(midpoints_mm: list, threshold_mm: float = 1.0) -> None:
"""Mark Blender mesh edges as sharp based on proximity to OCC-derived midpoints.
midpoints_mm: list of [x, y, z] in mm (from OCC coordinate space).
After STL import + scale-apply (mm→m), Blender vertices are in meters, so we
convert the edge midpoint back to mm before comparing.
threshold_mm: snap distance in mm (default 1.0 mm).
"""
if not midpoints_mm:
return
import bpy # type: ignore[import]
for obj in bpy.data.objects:
if obj.type != "MESH":
continue
mesh = obj.data
mesh.use_auto_smooth = True
mw = obj.matrix_world
for edge in mesh.edges:
v1 = mw @ mesh.vertices[edge.vertices[0]].co
v2 = mw @ mesh.vertices[edge.vertices[1]].co
# Convert Blender meters → mm for comparison
mid_mm = [
(v1.x + v2.x) / 2 * 1000,
(v1.y + v2.y) / 2 * 1000,
(v1.z + v2.z) / 2 * 1000,
]
for hint in midpoints_mm:
dist_sq = sum((a - b) ** 2 for a, b in zip(mid_mm, hint))
if dist_sq < threshold_mm ** 2:
edge.use_edge_sharp = True
break
def main() -> None:
args = parse_args()
material_map: dict = json.loads(args.material_map)
sharp_edge_midpoints: list = json.loads(args.sharp_edges_json)
import bpy # type: ignore[import]
# Clean scene
bpy.ops.wm.read_factory_settings(use_empty=True)
# Import STL
bpy.ops.import_mesh.stl(filepath=args.stl_path)
# Scale mm → m
for obj in bpy.context.selected_objects:
obj.scale = (0.001, 0.001, 0.001)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(scale=True)
# Mark sharp edges for better UV seams
if sharp_edge_midpoints:
mark_sharp_edges_by_proximity(sharp_edge_midpoints)
print(f"Marked sharp edges from {len(sharp_edge_midpoints)} hint points")
# Apply asset library materials if provided
if args.asset_library_blend and material_map:
import os
sys.path.insert(0, os.path.dirname(__file__))
from asset_library import apply_asset_library_materials
apply_asset_library_materials(args.asset_library_blend, material_map)
# Export GLB
try:
bpy.ops.export_scene.gltf(
filepath=args.output_path,
export_format="GLB",
export_apply=True,
use_selection=False,
)
except Exception as exc:
print(f"GLB export failed: {exc}", file=sys.stderr)
sys.exit(1)
print(f"GLB exported to {args.output_path}")
try:
main()
except SystemExit:
raise
except Exception:
traceback.print_exc()
sys.exit(1)