feat: performance optimizations + part-materials validation
- @timed_step decorator with wall-clock + RSS tracking (pipeline_logger) - Blender timing laps for sharp edges and material assignment - MeshRegistry pattern: eliminate 13 scene.traverse() calls across viewers - Lazy material cloning (clone-on-first-write in both viewers) - _pipeline_session context manager: 7 create_engine() → 2 in render_thumbnail - KD-tree spatial pre-filter for sharp edge marking (bbox-based pruning) - Batch material library append: N bpy.ops.wm.append → single bpy.data.libraries.load - GMSH single-session batching: compound all solids into one tessellation call - Validate part-materials save endpoints against parsed_objects (prevents bogus keys) - ROADMAP updated with completion status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -390,6 +390,28 @@ async def get_part_materials(
|
||||
)
|
||||
|
||||
|
||||
def _normalize_part_name(name: str) -> str:
|
||||
"""Strip OCC _AF\\d+ suffixes and lowercase for comparison."""
|
||||
import re
|
||||
n = name.strip().lower()
|
||||
prev = ""
|
||||
while prev != n:
|
||||
prev = n
|
||||
n = re.sub(r"_af\d+(_asm)?$", "", n)
|
||||
return n
|
||||
|
||||
|
||||
def _valid_part_names(cad) -> set[str] | None:
|
||||
"""Return normalized part names from parsed_objects, or None if unavailable."""
|
||||
po = cad.parsed_objects
|
||||
if not po or not isinstance(po, dict):
|
||||
return None
|
||||
objects = po.get("objects")
|
||||
if not objects or not isinstance(objects, list):
|
||||
return None
|
||||
return {_normalize_part_name(n) for n in objects if isinstance(n, str)}
|
||||
|
||||
|
||||
@router.put("/{id}/part-materials", response_model=PartMaterialsResponse)
|
||||
async def save_part_materials(
|
||||
id: uuid.UUID,
|
||||
@@ -401,10 +423,26 @@ async def save_part_materials(
|
||||
|
||||
Accepts a full dict of part-name -> {type, value} and overwrites the existing
|
||||
assignment. Pass an empty dict to clear all assignments.
|
||||
|
||||
Keys are validated against parsed_objects — unknown part names are rejected.
|
||||
"""
|
||||
if not is_privileged(user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
cad = await _get_cad_file(id, db)
|
||||
|
||||
# Validate keys against known part names from STEP extraction
|
||||
valid_names = _valid_part_names(cad)
|
||||
if valid_names is not None and body:
|
||||
invalid_keys = [
|
||||
k for k in body
|
||||
if _normalize_part_name(k) not in valid_names
|
||||
]
|
||||
if invalid_keys:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Unknown part names (not in parsed_objects): {invalid_keys[:10]}",
|
||||
)
|
||||
|
||||
# Serialise Pydantic models to plain dicts for JSONB storage
|
||||
cad.part_materials = {name: entry.model_dump() for name, entry in body.items()}
|
||||
cad.updated_at = datetime.utcnow()
|
||||
@@ -514,6 +552,20 @@ async def save_manual_material_overrides(
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
cad = await _get_cad_file(id, db)
|
||||
|
||||
# Validate keys against known part names (slugified form)
|
||||
valid_names = _valid_part_names(cad)
|
||||
if valid_names is not None and body.overrides:
|
||||
invalid_keys = [
|
||||
k for k in body.overrides
|
||||
if _normalize_part_name(k) not in valid_names
|
||||
]
|
||||
if invalid_keys:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Unknown part keys (not in parsed_objects): {invalid_keys[:10]}",
|
||||
)
|
||||
|
||||
cad.manual_material_overrides = body.overrides
|
||||
cad.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user