Files
HartOMat/.claude/commands/render-pipeline.md
T
Hartmut 9c6ae18b28 chore(agents): add three new specialist agents
/usd-export — USD authoring specialist
  - Full pxr API reference (Stage, Mesh, Primvars, MaterialBinding, Override layers)
  - XCAF traversal pattern for partKey generation
  - Coordinate system (OCC Z-up mm → USD Y-up mm, no scaling needed)
  - FlattenLayerStack delivery pattern
  - Test commands + common errors table
  - Failure protocol linking to /plan

/render-pipeline — Render script chain specialist
  - Full script chain (export_step_to_gltf → export_gltf → still_render → turntable_render)
  - GPU activation 6-step order (critical, open_mainfile resets compute_device_type)
  - AF suffix stripping for material matching
  - GLB extras round-trip documentation
  - GCPnts_UniformAbscissa requirement (Polygon3D_s returns None in XCAF)
  - Parameter propagation rule (admin.py → export_glb.py → script → Blender)
  - Direct subprocess test commands

/tenant-audit — RLS correctness specialist
  - HTTP + Celery layer audit steps
  - Live cross-tenant leak test pattern (SET LOCAL + count comparison)
  - Fix patterns for middleware and task-side set_tenant_context
  - Role permission matrix
  - Tables requiring RLS policies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:04:00 +01:00

212 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Render Pipeline Agent
You are a specialist for the render script chain in the Schaeffler Automat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container.
## Pipeline Overview
```
Celery task: render_step_thumbnail [queue: thumbnail_rendering]
├─ subprocess: export_step_to_gltf.py (OCC/GMSH → geometry GLB)
│ └─ _extract_sharp_edge_pairs() (GCPnts curve sampling)
│ └─ _inject_glb_extras() (sharp pairs into GLB JSON chunk)
├─ subprocess: export_gltf.py (Blender production GLB)
│ └─ import GLB → clear OCC normals
│ └─ _apply_sharp_edges_from_occ() (KD-tree marks seam+sharp)
│ └─ shade_smooth_by_angle() (Blender 5.0 geometry node)
│ └─ append materials from .blend library
│ └─ export production GLB
└─ subprocess: still_render.py (Blender PNG still)
└─ import production GLB
└─ _activate_gpu() × 3 (before file, after file, after engine)
└─ Cycles render → PNG thumbnail
Celery task: render_order_line_task [queue: thumbnail_rendering]
├─ subprocess: still_render.py (order-line PNG)
└─ subprocess: turntable_render.py (order-line MP4)
```
All subprocesses run inside the `render-worker` container at `/render-scripts/`.
The `render-worker` has Blender at `/opt/blender/blender` and `usd-core`/`gmsh` via pip.
## Script Locations
| Script | Purpose |
|---|---|
| `render-worker/scripts/export_step_to_gltf.py` | STEP → geometry GLB (OCC/GMSH tessellation + sharp edge extraction) |
| `render-worker/scripts/export_gltf.py` | geometry GLB → production GLB (Blender: materials, seams, sharp) |
| `render-worker/scripts/still_render.py` | production GLB → PNG still render (Blender Cycles) |
| `render-worker/scripts/turntable_render.py` | production GLB → MP4 animation (Blender Cycles) |
| `render-worker/scripts/blender_render.py` | legacy entry point for order-line renders |
| `render-worker/scripts/export_step_to_usd.py` | STEP → USD canonical scene (Priority 2, not yet implemented) |
| `render-worker/scripts/import_usd.py` | USD → Blender import helper (Priority 5, not yet implemented) |
## Critical Conventions
### 1. Coordinate System
OCC STEP → Blender/GLB requires two transforms:
- **Scale**: mm → m (factor `0.001`)
- **Axis**: OCC Z-up → Blender/glTF Y-up
```python
# OCC (X, Y, Z) mm → Blender (X, -Z, Y) m
blender_x = occ_x * 0.001
blender_y = -occ_z * 0.001
blender_z = occ_y * 0.001
```
Applied in `export_step_to_gltf.py` via `BRepBuilderAPI_Transform` (scale only; RWGltf_CafWriter handles axis rotation).
Applied in `_apply_sharp_edges_from_occ()` in `export_gltf.py` for KD-tree matching.
### 2. GPU Activation Order (critical — order matters)
```python
_early_gpu_type = _activate_gpu() # 1. Before open_mainfile
bpy.ops.wm.open_mainfile(filepath=blend) # 2. Resets compute_device_type to NONE
# ... scene setup ...
gpu_type = _activate_gpu() or _early_gpu # 3. Re-activate after file open
scene.render.engine = 'CYCLES' # 4. Set engine AFTER GPU prefs
scene.cycles.device = 'GPU' # 5. Set device AFTER engine
_activate_gpu() # 6. Re-ensure after engine reset
```
**Never** set `render.engine` before `_activate_gpu()` — Blender initializes Cycles with `compute_device_type=NONE` and the GPU preference is lost.
### 3. Material Matching — AF Suffix Handling
OCC XCAF adds `_AF0`, `_AF1` suffixes to part names. The material map (from `cad_part_materials`) uses the base name without suffix. In `export_gltf.py`:
```python
import re
# Strip _AF\d+ suffix from both mat_map keys and Blender object names before matching
def _strip_af(name: str) -> str:
return re.sub(r'_AF\d+$', '', name)
mat_map_lower = {_strip_af(k).lower(): v for k, v in mat_map.items()}
obj_key = _strip_af(obj.name).lower()
material_name = mat_map_lower.get(obj_key)
```
### 4. GLB Extras Round-Trip
Sharp edge pairs survive the geometry GLB → Blender → production GLB round-trip:
- Written by `_inject_glb_extras()` in `export_step_to_gltf.py` into `scenes[0].extras`
- Read by Blender's glTF importer as `bpy.context.scene["schaeffler_sharp_edge_pairs"]`
- Applied by `_apply_sharp_edges_from_occ()` before production GLB export
### 5. OCC Sharp Edge Extraction
`BRep_Tool.Polygon3D_s()` returns `None` in XCAF compound context. Always use `GCPnts_UniformAbscissa`:
```python
from OCP.GCPnts import GCPnts_UniformAbscissa
from OCP.BRepAdaptor import BRepAdaptor_Curve
SAMPLE_STEP_MM = 0.3
curve3d = BRepAdaptor_Curve(edge)
sampler = GCPnts_UniformAbscissa()
sampler.Initialize(curve3d, SAMPLE_STEP_MM, 1e-6)
if sampler.IsDone() and sampler.NbPoints() >= 2:
for j in range(1, sampler.NbPoints() + 1):
t = sampler.Parameter(j)
p = curve3d.Value(t)
pts.append([round(p.X(), 4), round(p.Y(), 4), round(p.Z(), 4)])
```
### 6. Blender shade_smooth_by_angle
In Blender 5.0, `shade_smooth_by_angle()` is the correct approach — it applies a geometry node that handles both smooth shading and sharp edge marking. Do not use `bpy.ops.mesh.edges_select_sharp()` + `mark_sharp()` loop — it was 210s on a 175-part assembly.
```python
# Applied per mesh object after import:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(smooth_angle_deg))
```
## Parameter Propagation Rule
When adding a new parameter to the pipeline, it must flow through **every link**:
```
admin.py (system_settings)
→ export_glb.py (read setting, build CLI args)
→ export_step_to_gltf.py / export_gltf.py / still_render.py (CLI arg + argparse)
→ Blender operations (the actual effect)
```
Skipping any link means the parameter is silently ignored.
## Testing Scripts Directly
```bash
# Test geometry GLB export (OCC)
docker compose exec render-worker python3 /render-scripts/export_step_to_gltf.py \
--step_path /app/uploads/[cad_file_id]/[file].stp \
--output_path /tmp/test_geom.glb \
--linear_deflection 0.03 \
--angular_deflection 0.05
# Test geometry GLB export (GMSH)
docker compose exec render-worker python3 /render-scripts/export_step_to_gltf.py \
--step_path /app/uploads/[cad_file_id]/[file].stp \
--output_path /tmp/test_geom_gmsh.glb \
--tessellation_engine gmsh \
--linear_deflection 0.03 \
--angular_deflection 0.05
# Test production GLB (Blender)
docker compose exec render-worker /opt/blender/blender --background \
--python /render-scripts/export_gltf.py -- \
--glb_path /tmp/test_geom.glb \
--output_path /tmp/test_prod.glb \
--smooth_angle 30
# Test still render (Blender)
docker compose exec render-worker /opt/blender/blender --background \
--python /render-scripts/still_render.py -- \
--glb_path /tmp/test_prod.glb \
--output_path /tmp/test_thumb.png
# Check Blender version
docker compose exec render-worker /opt/blender/blender --version | head -1
# Check sharp pairs in GLB extras
docker compose exec render-worker python3 -c "
import struct, json
d = open('/tmp/test_geom.glb', 'rb').read()
jl = struct.unpack_from('<I', d, 12)[0]
j = json.loads(d[20:20+jl])
pairs = j.get('scenes', [{}])[0].get('extras', {}).get('schaeffler_sharp_edge_pairs', [])
print(f'{len(pairs)} sharp edge pairs in GLB extras')
if pairs: print('First pair:', pairs[0])
"
```
## Common Problems
| Symptom | Cause | Fix |
|---|---|---|
| No sharp edges in Blender after import | `Polygon3D_s()` returned None | Use `GCPnts_UniformAbscissa` (already in export_step_to_gltf.py) |
| GLB extras not read by Blender | `scenes[0].extras` not patched | Check `_inject_glb_extras()` called after `RWGltf_CafWriter.Perform()` |
| Materials not applied | AF suffix mismatch | Verify `_strip_af()` applied to both map keys and object names |
| Render is black / no GPU | GPU activation called in wrong order | Follow the 6-step GPU activation order above |
| Faceting on cylinders | OCC BRepMesh angular/linear deflection mismatch | Switch to GMSH tessellation engine |
| Fan triangles at seam | OCC BRepMesh periodic face seam limitation | GMSH Frontal-Delaunay fixes this structurally |
| `shade_smooth_by_angle` error | Blender version < 5.0 | Verify `BLENDER_VERSION=5.0.1` in render-worker |
| GMSH hangs | `gmsh.finalize()` not called | Wrap entire GMSH block in try/finally with `gmsh.finalize()` |
## Failure Protocol
If a script fails mid-pipeline:
1. Note the exact script name and error message
2. Add `[BLOCKED]` to the failing task in `plan.md`
3. Invoke `/plan` to refine — include: script name, CLI args used, and full traceback
## Completion
After changes to render scripts: "Render pipeline updated. Test with the commands above, then verify with `/check` and `/review`."