chore: snapshot workflow migration progress
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user