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:
2026-03-07 16:49:18 +01:00
parent 3eba7b2d37
commit 95cfe0aa93
20 changed files with 809 additions and 1301 deletions
+13 -68
View File
@@ -27,78 +27,30 @@ def parse_args() -> argparse.Namespace:
sys.exit(1)
rest = argv[argv.index("--") + 1:]
parser = argparse.ArgumentParser()
parser.add_argument("--stl_path", required=True)
parser.add_argument("--glb_path", required=True,
help="Geometry GLB from export_step_to_gltf.py (already in metres)")
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]
import math
for obj in bpy.data.objects:
if obj.type != "MESH":
continue
mesh = obj.data
# Blender 4.1+ removed use_auto_smooth — use shade_smooth_by_angle instead
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
try:
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(30))
except Exception:
pass # fallback: stay flat-shaded
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]
import math as _math
# Clean scene
bpy.ops.wm.read_factory_settings(use_empty=True)
# Import STL (bpy.ops.wm.stl_import is the Blender 4.0+ API)
bpy.ops.wm.stl_import(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)
# Import geometry GLB from export_step_to_gltf.py (already in metres, Y-up)
bpy.ops.import_scene.gltf(filepath=args.glb_path)
print(f"Imported geometry GLB: {args.glb_path} "
f"({len([o for o in bpy.data.objects if o.type == 'MESH'])} mesh objects)")
# Apply smooth shading with 30° angle threshold (Blender 4.1+ API)
import math as _math
for obj in bpy.data.objects:
if obj.type == "MESH":
bpy.context.view_layer.objects.active = obj
@@ -108,37 +60,30 @@ def main() -> None:
except Exception:
pass
# 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.
# link=False (append) is required for GLB export: the GLTF exporter can only
# traverse local (appended) Principled BSDF node trees to extract PBR values.
# Linked materials are external references whose node data is not accessible.
# link=False (append) is required: the GLTF exporter can only traverse
# local (appended) Principled BSDF node trees to extract PBR values.
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, link=False)
# Export GLB with full PBR material data
# Note: export_colors was removed in Blender 4.x — do not pass it.
# Export production GLB with full PBR material data
try:
bpy.ops.export_scene.gltf(
filepath=args.output_path,
export_format="GLB",
export_apply=True,
use_selection=False,
export_materials="EXPORT", # export all materials (Principled BSDF → glTF PBR)
export_image_format="AUTO", # embed textures (base color, normal, roughness maps)
export_materials="EXPORT",
export_image_format="AUTO",
)
except Exception as exc:
print(f"GLB export failed: {exc}", file=sys.stderr)
sys.exit(1)
print(f"GLB exported to {args.output_path}")
print(f"Production GLB exported to {args.output_path}")
try: