chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31774 additions and 2753 deletions
+144 -18
View File
@@ -20,6 +20,9 @@ import re
# ── Part key generation ───────────────────────────────────────────────────────
_AF_RE = re.compile(r'_AF\d+$', re.IGNORECASE)
_AF_VARIANT_RE = re.compile(r"_AF\d+(_ASM)?_?$", re.IGNORECASE)
_LEGACY_MATERIAL_PREFIX = "SCHAEFFLER_"
_CURRENT_MATERIAL_PREFIX = "HARTOMAT_"
def generate_part_key(
@@ -53,6 +56,95 @@ def generate_part_key(
return key
def normalize_material_name(material_name: str | None) -> str | None:
"""Normalize persisted legacy material names to the current HartOMat prefix."""
if not isinstance(material_name, str):
return None
value = material_name.strip()
if not value:
return None
if value.upper().startswith(_LEGACY_MATERIAL_PREFIX):
return f"{_CURRENT_MATERIAL_PREFIX}{value[len(_LEGACY_MATERIAL_PREFIX):]}"
return value
def _normalize_semantic_source_name(raw_name: str) -> str:
"""Collapse exporter-only suffixes back to their semantic OCC source name."""
name = (raw_name or "").strip()
name = re.sub(r"\.\d{3}$", "", name)
previous = None
while previous != name:
previous = name
name = _AF_VARIANT_RE.sub("", name)
return name
def _slugify_semantic_source_name(raw_name: str) -> str:
base = _normalize_semantic_source_name(raw_name)
base = re.sub(r"([a-z])([A-Z])", r"\1_\2", base)
return re.sub(r"[^a-z0-9]+", "_", base.lower()).strip("_")[:50]
def _derive_semantic_alias_key(part_key: str, source_name: str) -> str | None:
"""Return the semantic alias for deduplicated instance keys, if any."""
alias_key = _slugify_semantic_source_name(source_name)
if not alias_key or alias_key == part_key:
return None
if re.fullmatch(
rf"{re.escape(alias_key)}(?:_[2-9]\d*|_af\d+(?:_asm)?)",
part_key,
flags=re.IGNORECASE,
) is None:
return None
return alias_key
def _alias_priority(part_key: str, source_name: str) -> tuple[int, int, int]:
match = re.fullmatch(r".+_(\d+)$", part_key)
suffix_number = int(match.group(1)) if match else 1_000_000
return (suffix_number, len(source_name or ""), len(part_key))
def _iter_lookup_keys(part_key: str, fallback_part_keys: tuple[str, ...] = ()) -> tuple[str, ...]:
ordered_keys: list[str] = []
for key in (part_key, *fallback_part_keys):
if key and key not in ordered_keys:
ordered_keys.append(key)
return tuple(ordered_keys)
def _build_part_entry(
*,
part_key: str,
source_name: str,
prim_path: str | None,
manual: dict,
resolved: dict,
source: dict,
fallback_part_keys: tuple[str, ...] = (),
) -> dict:
effective_material, provenance = _resolve_material(
part_key,
source_name,
manual,
resolved,
source,
fallback_part_keys=fallback_part_keys,
)
is_unassigned = effective_material is None
return {
"part_key": part_key,
"source_name": source_name,
"prim_path": prim_path,
"effective_material": effective_material,
"assignment_provenance": provenance,
"is_unassigned": is_unassigned,
}
# ── Scene manifest building ───────────────────────────────────────────────────
def build_scene_manifest(cad_file, usd_asset=None) -> dict:
@@ -65,7 +157,8 @@ def build_scene_manifest(cad_file, usd_asset=None) -> dict:
Material assignment priority per part:
1. `manual_material_overrides[part_key]` — provenance "manual"
2. `resolved_material_assignments[part_key]["material"]` — provenance "auto"
2. `resolved_material_assignments[part_key]["canonical_material"]` (or legacy
`["material"]`) — provenance "auto"
3. substring match in `source_material_assignments` against source_name — provenance "source"
4. None, is_unassigned=True — provenance "default"
"""
@@ -80,25 +173,51 @@ def build_scene_manifest(cad_file, usd_asset=None) -> dict:
if resolved:
# Build from resolved assignments (USD pipeline has run)
alias_candidates: dict[str, tuple[tuple[int, int, int], dict]] = {}
for part_key, meta in resolved.items():
source_name = meta.get("source_name", "") if isinstance(meta, dict) else ""
prim_path = meta.get("prim_path") if isinstance(meta, dict) else None
effective_material, provenance = _resolve_material(
part_key, source_name, manual, resolved, source
part_entry = _build_part_entry(
part_key=part_key,
source_name=source_name,
prim_path=prim_path,
manual=manual,
resolved=resolved,
source=source,
)
is_unassigned = effective_material is None
parts.append(part_entry)
if part_entry["is_unassigned"]:
unassigned_parts.append(part_key)
parts.append({
"part_key": part_key,
alias_key = _derive_semantic_alias_key(part_key, source_name)
if alias_key is None or alias_key in resolved:
continue
candidate = {
"part_key": alias_key,
"source_name": source_name,
"prim_path": prim_path,
"effective_material": effective_material,
"assignment_provenance": provenance,
"is_unassigned": is_unassigned,
})
if is_unassigned:
unassigned_parts.append(part_key)
"fallback_part_keys": (part_key,),
}
candidate_priority = _alias_priority(part_key, source_name)
current = alias_candidates.get(alias_key)
if current is None or candidate_priority < current[0]:
alias_candidates[alias_key] = (candidate_priority, candidate)
for alias_key, (_, candidate) in alias_candidates.items():
alias_entry = _build_part_entry(
part_key=candidate["part_key"],
source_name=candidate["source_name"],
prim_path=candidate["prim_path"],
manual=manual,
resolved=resolved,
source=source,
fallback_part_keys=candidate["fallback_part_keys"],
)
parts.append(alias_entry)
if alias_entry["is_unassigned"]:
unassigned_parts.append(alias_key)
elif cad_file.parsed_objects:
# Fall back to parsed_objects from STEP extraction
@@ -149,23 +268,30 @@ def _resolve_material(
manual: dict,
resolved: dict,
source: dict,
fallback_part_keys: tuple[str, ...] = (),
) -> tuple[str | None, str]:
"""Return (material_name, provenance) for one part using priority order."""
lookup_keys = _iter_lookup_keys(part_key, fallback_part_keys)
# 1. Manual override
if part_key in manual and manual[part_key]:
return str(manual[part_key]), "manual"
for lookup_key in lookup_keys:
if lookup_key in manual and manual[lookup_key]:
return normalize_material_name(str(manual[lookup_key])), "manual"
# 2. Auto-resolved from USD pipeline
meta = resolved.get(part_key)
if isinstance(meta, dict) and meta.get("material"):
return str(meta["material"]), "auto"
for lookup_key in lookup_keys:
meta = resolved.get(lookup_key)
if isinstance(meta, dict):
canonical = normalize_material_name(meta.get("canonical_material") or meta.get("material"))
if canonical:
return canonical, "auto"
# 3. Substring match in source_material_assignments against source_name
sn_lower = source_name.lower()
for src_key, src_mat in source.items():
if src_key.lower() in sn_lower or sn_lower in src_key.lower():
if src_mat:
return str(src_mat), "source"
return normalize_material_name(str(src_mat)), "source"
# 4. Unassigned
return None, "default"