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>
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
# USD Export Agent
|
||||
|
||||
You are a specialist for the USD pipeline in the Schaeffler Automat project. You implement and debug everything related to `export_step_to_usd.py`, `import_usd.py`, and the `pxr` authoring API.
|
||||
|
||||
## Architecture Decisions (all locked)
|
||||
|
||||
| Decision | Value |
|
||||
|---|---|
|
||||
| USD library | `usd-core` pip → `from pxr import Usd, UsdGeom, Sdf, Vt, Gf, UsdShade, UsdUtils` |
|
||||
| Seam/sharp encoding | Index-space primvars on mesh prim (not world-space coordinates) |
|
||||
| Preview GLB | Co-authored from same tessellation pass (not USD→GLB round-trip) |
|
||||
| Layer strategy | Canonical geometry layer + material override layer, flattened via `UsdUtils.FlattenLayerStack()` for delivery |
|
||||
| Instancing | None in Phase 1 (co-author `FlattenLayerStack` is instancing-safe for later) |
|
||||
|
||||
Full implementation checklist: `docs/plans/0001-step-to-usd-implementation.md`
|
||||
|
||||
## Key pxr API Reference
|
||||
|
||||
### Stage and Layer Setup
|
||||
|
||||
```python
|
||||
from pxr import Usd, UsdGeom, UsdShade, Sdf, Vt, Gf, UsdUtils
|
||||
|
||||
# Create a new stage (writes to disk)
|
||||
stage = Usd.Stage.CreateNew("/path/to/output.usd")
|
||||
|
||||
# Set up coordinate system and units
|
||||
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y) # glTF/Blender convention
|
||||
UsdGeom.SetStageMetersPerUnit(stage, 0.001) # STEP is in mm
|
||||
|
||||
# Define root prims
|
||||
root = UsdGeom.Xform.Define(stage, "/Root")
|
||||
assembly = UsdGeom.Xform.Define(stage, "/Root/Assembly")
|
||||
|
||||
stage.Save()
|
||||
```
|
||||
|
||||
### Part Hierarchy + Custom Metadata
|
||||
|
||||
```python
|
||||
# Define an Xform for a part
|
||||
part_xform = UsdGeom.Xform.Define(stage, f"/Root/Assembly/{node_name}/{part_key}")
|
||||
|
||||
# Author custom metadata (schaeffler: namespace)
|
||||
part_prim = part_xform.GetPrim()
|
||||
part_prim.SetCustomDataByKey("schaeffler:partKey", part_key)
|
||||
part_prim.SetCustomDataByKey("schaeffler:sourceName", source_name)
|
||||
part_prim.SetCustomDataByKey("schaeffler:sourceColor", hex_color)
|
||||
part_prim.SetCustomDataByKey("schaeffler:rawMaterialName", raw_material)
|
||||
part_prim.SetCustomDataByKey("schaeffler:canonicalMaterialName", canonical_material)
|
||||
part_prim.SetCustomDataByKey("schaeffler:cadFileId", str(cad_file_id))
|
||||
part_prim.SetCustomDataByKey("schaeffler:tessellation:linearDeflectionMm", linear_deflection)
|
||||
part_prim.SetCustomDataByKey("schaeffler:tessellation:angularDeflectionRad", angular_deflection)
|
||||
```
|
||||
|
||||
### Mesh Geometry
|
||||
|
||||
```python
|
||||
from pxr import Vt, Gf
|
||||
|
||||
mesh = UsdGeom.Mesh.Define(stage, f"{part_path}/Mesh")
|
||||
|
||||
# Points (vertices) — Gf.Vec3f array
|
||||
points = Vt.Vec3fArray([Gf.Vec3f(x, y, z) for x, y, z in vertex_list])
|
||||
mesh.CreatePointsAttr(points)
|
||||
|
||||
# Face topology
|
||||
face_vertex_counts = Vt.IntArray([3] * len(triangles)) # all triangles
|
||||
mesh.CreateFaceVertexCountsAttr(face_vertex_counts)
|
||||
|
||||
face_vertex_indices = Vt.IntArray([idx for tri in triangles for idx in tri])
|
||||
mesh.CreateFaceVertexIndicesAttr(face_vertex_indices)
|
||||
|
||||
# Normals (per-vertex or per-face)
|
||||
normals = Vt.Vec3fArray([Gf.Vec3f(nx, ny, nz) for nx, ny, nz in normal_list])
|
||||
mesh.CreateNormalsAttr(normals)
|
||||
mesh.SetNormalsInterpolation(UsdGeom.Tokens.vertex)
|
||||
|
||||
# Display color from OCC
|
||||
color = Vt.Vec3fArray([Gf.Vec3f(r, g, b)])
|
||||
mesh.CreateDisplayColorAttr(color)
|
||||
```
|
||||
|
||||
### Seam and Sharp Edge Primvars (index-space)
|
||||
|
||||
```python
|
||||
from pxr import UsdGeom, Vt, Sdf
|
||||
|
||||
primvars_api = UsdGeom.PrimvarsAPI(mesh)
|
||||
|
||||
# Sharp edges: list of (vertex_index_0, vertex_index_1) pairs
|
||||
sharp_pairs = [(vi0, vi1), (vi2, vi3), ...] # local mesh vertex indices
|
||||
sharp_array = Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in sharp_pairs])
|
||||
pv_sharp = primvars_api.CreatePrimvar(
|
||||
"schaeffler:sharpEdgeVertexPairs",
|
||||
Sdf.ValueTypeNames.Int2Array,
|
||||
UsdGeom.Tokens.constant,
|
||||
)
|
||||
pv_sharp.Set(sharp_array)
|
||||
|
||||
# Seam edges: same structure
|
||||
seam_pairs = [(vi0, vi1), ...]
|
||||
seam_array = Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in seam_pairs])
|
||||
pv_seam = primvars_api.CreatePrimvar(
|
||||
"schaeffler:seamEdgeVertexPairs",
|
||||
Sdf.ValueTypeNames.Int2Array,
|
||||
UsdGeom.Tokens.constant,
|
||||
)
|
||||
pv_seam.Set(seam_array)
|
||||
```
|
||||
|
||||
### Material Binding
|
||||
|
||||
```python
|
||||
from pxr import UsdShade
|
||||
|
||||
# Define material prim
|
||||
mat_path = f"/Root/Looks/{canonical_material_name}"
|
||||
material = UsdShade.Material.Define(stage, mat_path)
|
||||
|
||||
# Bind material to part mesh
|
||||
binding_api = UsdShade.MaterialBindingAPI(mesh.GetPrim())
|
||||
binding_api.Bind(material)
|
||||
```
|
||||
|
||||
### Override Layer (for material replacements)
|
||||
|
||||
```python
|
||||
# Create canonical stage
|
||||
canonical_stage = Usd.Stage.CreateNew("/path/to/canonical.usd")
|
||||
# ... author geometry ...
|
||||
canonical_stage.Save()
|
||||
|
||||
# Create override layer for material assignments
|
||||
override_layer = Sdf.Layer.CreateNew("/path/to/overrides.usd")
|
||||
override_stage = Usd.Stage.Open("/path/to/canonical.usd")
|
||||
override_stage.GetRootLayer().subLayerPaths.append("/path/to/overrides.usd")
|
||||
|
||||
# Author override opinions
|
||||
with Usd.EditContext(override_stage, override_stage.GetRootLayer()):
|
||||
prim = override_stage.GetPrimAtPath("/Root/Assembly/Node/ring_outer")
|
||||
prim.SetCustomDataByKey("schaeffler:canonicalMaterialName", "SCHAEFFLER_010102_Steel-Polished")
|
||||
|
||||
override_stage.Save()
|
||||
|
||||
# Flatten for delivery (preserves instanceable prims)
|
||||
flat_stage = Usd.Stage.CreateNew("/path/to/delivery.usd")
|
||||
UsdUtils.FlattenLayerStack(override_stage, flat_stage.GetRootLayer())
|
||||
flat_stage.Save()
|
||||
```
|
||||
|
||||
### Reading Primvars in Blender (import_usd.py)
|
||||
|
||||
```python
|
||||
import bpy
|
||||
|
||||
# After USD import:
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
# Blender maps USD primvars to custom attributes
|
||||
seam_attr = obj.data.attributes.get("schaeffler:seamEdgeVertexPairs")
|
||||
sharp_attr = obj.data.attributes.get("schaeffler:sharpEdgeVertexPairs")
|
||||
if seam_attr:
|
||||
_mark_seams_from_index_pairs(obj, seam_attr.data)
|
||||
if sharp_attr:
|
||||
_mark_sharp_from_index_pairs(obj, sharp_attr.data)
|
||||
```
|
||||
|
||||
## XCAF Traversal Pattern for Part Keys
|
||||
|
||||
```python
|
||||
from OCP.XCAFDoc import XCAFDoc_ShapeTool, XCAFDoc_DocumentTool
|
||||
from OCP.TDF import TDF_LabelSequence
|
||||
from OCP.TDataStd import TDataStd_Name
|
||||
|
||||
def traverse_xcaf(label, shape_tool, depth=0, path=""):
|
||||
"""Yield (label, part_key, source_name, xcaf_path) for each leaf shape."""
|
||||
name_attr = TDataStd_Name()
|
||||
source_name = ""
|
||||
if label.FindAttribute(TDataStd_Name.GetID_s(), name_attr):
|
||||
source_name = name_attr.Get().ToExtString()
|
||||
|
||||
components = TDF_LabelSequence()
|
||||
XCAFDoc_ShapeTool.GetComponents_s(label, components)
|
||||
|
||||
xcaf_path = f"{path}/{source_name}" if source_name else f"{path}/unnamed_{depth}"
|
||||
|
||||
if components.Length() == 0:
|
||||
# Leaf node
|
||||
part_key = generate_part_key(xcaf_path, source_name)
|
||||
yield label, part_key, source_name, xcaf_path
|
||||
else:
|
||||
for i in range(1, components.Length() + 1):
|
||||
yield from traverse_xcaf(components.Value(i), shape_tool, depth+1, xcaf_path)
|
||||
```
|
||||
|
||||
## Part Key Generation
|
||||
|
||||
```python
|
||||
import re, hashlib
|
||||
|
||||
def generate_part_key(xcaf_path: str, source_name: str, seen: set) -> str:
|
||||
"""Deterministic slug, max 64 chars, unique within assembly."""
|
||||
base = source_name or xcaf_path.split("/")[-1] or "part"
|
||||
# Normalize: lowercase, replace non-alphanumeric with underscore
|
||||
slug = re.sub(r'[^a-z0-9]+', '_', base.lower()).strip('_')
|
||||
slug = slug[:50] or f"part_{hashlib.sha256(xcaf_path.encode()).hexdigest()[:8]}"
|
||||
# Deduplicate
|
||||
key = slug
|
||||
n = 2
|
||||
while key in seen:
|
||||
key = f"{slug}_{n}"
|
||||
n += 1
|
||||
seen.add(key)
|
||||
return key
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
OCC STEP files are in **mm, Z-up**. USD stage is set to **mm, Y-up** (matching Blender and glTF convention).
|
||||
|
||||
Transform when writing points:
|
||||
```python
|
||||
# OCC (X, Y, Z) mm Z-up → USD (X, -Z, Y) mm Y-up
|
||||
usd_x = occ_x
|
||||
usd_y = -occ_z
|
||||
usd_z = occ_y
|
||||
```
|
||||
|
||||
The `BRepBuilderAPI_Transform` mm→m scaling done in `export_step_to_gltf.py` is **not needed** here — USD stage metadata carries `metersPerUnit=0.001`, so Blender handles the scale on import.
|
||||
|
||||
## CLI Interface for `export_step_to_usd.py`
|
||||
|
||||
```bash
|
||||
python3 export_step_to_usd.py \
|
||||
--step_path /path/to/file.stp \
|
||||
--output_path /path/to/output.usd \
|
||||
[--linear_deflection 0.03] \
|
||||
[--angular_deflection 0.05] \
|
||||
[--tessellation_engine occ|gmsh] \
|
||||
[--color_map '{"Ring": "#4C9BE8"}'] \
|
||||
[--cad_file_id uuid]
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
```bash
|
||||
# Verify usd-core is installed
|
||||
docker compose exec render-worker python3 -c "from pxr import Usd; print(Usd.GetVersion())"
|
||||
|
||||
# Run USD exporter
|
||||
docker compose exec render-worker python3 /render-scripts/export_step_to_usd.py \
|
||||
--step_path /app/uploads/[cad_file_id]/[filename].stp \
|
||||
--output_path /tmp/test.usd
|
||||
|
||||
# Inspect output
|
||||
docker compose exec render-worker python3 -c "
|
||||
from pxr import Usd
|
||||
stage = Usd.Stage.Open('/tmp/test.usd')
|
||||
for prim in stage.Traverse():
|
||||
if prim.GetTypeName() == 'Mesh':
|
||||
print(prim.GetPath(), '| partKey:', prim.GetCustomDataByKey('schaeffler:partKey'))
|
||||
"
|
||||
|
||||
# Count parts with partKey
|
||||
docker compose exec render-worker python3 -c "
|
||||
from pxr import Usd
|
||||
stage = Usd.Stage.Open('/tmp/test.usd')
|
||||
parts = [p for p in stage.Traverse() if p.GetCustomDataByKey('schaeffler:partKey')]
|
||||
print(f'{len(parts)} parts with partKey')
|
||||
"
|
||||
```
|
||||
|
||||
## Common Errors and Fixes
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `ModuleNotFoundError: pxr` | usd-core not installed | `docker compose build render-worker` after adding to Dockerfile |
|
||||
| `ValueError: Invalid SdfPath` | Part name contains special chars | Use `generate_part_key()` — strips non-alphanumeric |
|
||||
| `RuntimeError: stage is invalid` | File path doesn't exist | Ensure output directory exists before `Stage.CreateNew()` |
|
||||
| Prim hierarchy too deep | Assembly has > 10 levels | Use relative paths from Assembly root, skip empty intermediate nodes |
|
||||
| Blender doesn't read primvars | Attribute name mismatch | Check USD attribute name matches what Blender's USD importer maps |
|
||||
| Seam pairs reference wrong vertices | Index computed before tessellation | Always compute index-space pairs AFTER BRepMesh, on the final triangulation |
|
||||
|
||||
## Failure Protocol
|
||||
|
||||
If a task fails, add `[BLOCKED]` to the task in `plan.md` with:
|
||||
- exact pxr error message
|
||||
- which XCAF label or mesh caused it
|
||||
- what was attempted
|
||||
|
||||
Then invoke `/plan` for a refined task specification.
|
||||
|
||||
## Completion
|
||||
|
||||
After implementation: "USD export complete. Test with: `python3 export_step_to_usd.py --step_path [file]`. Please verify with `/review`."
|
||||
Reference in New Issue
Block a user