93773f3887
Includes FastAPI backend, vendored step2urdf frontend, and A7 handtuned arm JSON for URDF generation.
150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
from app.models import PartNode
|
|
|
|
|
|
_X2_RE = re.compile(r"\\X2\\([0-9A-Fa-f]+)\\X0\\")
|
|
_PRODUCT_RE = re.compile(
|
|
r"#(\d+)\s*=\s*PRODUCT\s*\(\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'",
|
|
re.IGNORECASE,
|
|
)
|
|
# PRODUCT_DEFINITION_FORMATION(... , #product, ...)
|
|
_PDF_RE = re.compile(
|
|
r"#(\d+)\s*=\s*PRODUCT_DEFINITION_FORMATION\s*\([^;]*?#(\d+)\s*\)\s*;",
|
|
re.IGNORECASE | re.DOTALL,
|
|
)
|
|
# PRODUCT_DEFINITION(..., #pdf, ...)
|
|
_PD_RE = re.compile(
|
|
r"#(\d+)\s*=\s*PRODUCT_DEFINITION\s*\([^;]*?#(\d+)\s*\)\s*;",
|
|
re.IGNORECASE | re.DOTALL,
|
|
)
|
|
# NEXT_ASSEMBLY_USAGE_OCCURRENCE('name', ..., #parent_pd, #child_pd, ...)
|
|
_NAUO_RE = re.compile(
|
|
r"#(\d+)\s*=\s*NEXT_ASSEMBLY_USAGE_OCCURRENCE\s*\(\s*'([^']*)'\s*,"
|
|
r"[^;]*?#(\d+)\s*,\s*#(\d+)\s*",
|
|
re.IGNORECASE | re.DOTALL,
|
|
)
|
|
|
|
|
|
def decode_step_string(s: str) -> str:
|
|
def repl(m: re.Match[str]) -> str:
|
|
hexpart = m.group(1)
|
|
chars: list[str] = []
|
|
for i in range(0, len(hexpart), 4):
|
|
chars.append(chr(int(hexpart[i : i + 4], 16)))
|
|
return "".join(chars)
|
|
|
|
return _X2_RE.sub(repl, s)
|
|
|
|
|
|
def parse_step_products(step_path: Path) -> dict:
|
|
text = step_path.read_text(encoding="utf-8", errors="replace")
|
|
|
|
products: dict[str, str] = {}
|
|
for m in _PRODUCT_RE.finditer(text):
|
|
eid, name, *_ = m.groups()
|
|
products[eid] = decode_step_string(name)
|
|
|
|
# pdf_id -> product_id
|
|
pdf_to_product: dict[str, str] = {}
|
|
for m in _PDF_RE.finditer(text):
|
|
pdf_id, product_id = m.groups()
|
|
pdf_to_product[pdf_id] = product_id
|
|
|
|
# pd_id -> pdf_id
|
|
pd_to_pdf: dict[str, str] = {}
|
|
for m in _PD_RE.finditer(text):
|
|
pd_id, pdf_id = m.groups()
|
|
pd_to_pdf[pd_id] = pdf_id
|
|
|
|
def pd_name(pd_id: str) -> str:
|
|
pdf = pd_to_pdf.get(pd_id)
|
|
if not pdf:
|
|
return f"pd_{pd_id}"
|
|
prod = pdf_to_product.get(pdf)
|
|
if not prod:
|
|
return f"pdf_{pdf}"
|
|
return products.get(prod, f"product_{prod}")
|
|
|
|
children: dict[str, list[str]] = defaultdict(list)
|
|
parents: dict[str, str] = {}
|
|
edge_names: dict[tuple[str, str], str] = {}
|
|
|
|
for m in _NAUO_RE.finditer(text):
|
|
_eid, edge_name, parent_pd, child_pd = m.groups()
|
|
children[parent_pd].append(child_pd)
|
|
parents[child_pd] = parent_pd
|
|
edge_names[(parent_pd, child_pd)] = decode_step_string(edge_name)
|
|
|
|
# Roots = PDs that appear as parents or products but have no parent
|
|
all_pd = set(pd_to_pdf) | set(children) | set(parents)
|
|
roots = [pd for pd in all_pd if pd not in parents]
|
|
if not roots and products:
|
|
# fallback: flat product list
|
|
nodes = [
|
|
PartNode(id=pid, name=name, parent_id=None)
|
|
for pid, name in products.items()
|
|
]
|
|
return {
|
|
"root_name": nodes[0].name if nodes else "assembly",
|
|
"parts": nodes,
|
|
"product_names": sorted({p.name for p in nodes}),
|
|
"stats": {
|
|
"products": len(products),
|
|
"nauo": 0,
|
|
"axis2_placement": text.upper().count("AXIS2_PLACEMENT_3D"),
|
|
"circles": len(re.findall(r"\bCIRCLE\b", text, flags=re.I)),
|
|
"cylinders": text.upper().count("CYLINDRICAL_SURFACE"),
|
|
"mode": "flat_products",
|
|
},
|
|
}
|
|
|
|
# Prefer the largest tree root as assembly root
|
|
def subtree_size(pd: str, seen: set[str] | None = None) -> int:
|
|
seen = seen or set()
|
|
if pd in seen:
|
|
return 0
|
|
seen.add(pd)
|
|
return 1 + sum(subtree_size(c, seen) for c in children.get(pd, []))
|
|
|
|
roots_sorted = sorted(roots, key=subtree_size, reverse=True)
|
|
root_pd = roots_sorted[0] if roots_sorted else None
|
|
|
|
nodes: list[PartNode] = []
|
|
for pd in sorted(all_pd, key=lambda x: int(x) if x.isdigit() else 0):
|
|
nodes.append(
|
|
PartNode(
|
|
id=pd,
|
|
name=pd_name(pd),
|
|
parent_id=parents.get(pd),
|
|
children=list(children.get(pd, [])),
|
|
)
|
|
)
|
|
|
|
product_names = sorted({decode_step_string(n) for n in products.values()})
|
|
root_name = pd_name(root_pd) if root_pd else ""
|
|
if not root_name or root_name.startswith(("pd_", "pdf_", "product_")):
|
|
preferred = next((n for n in product_names if "装配体" in n or "assembly" in n.lower()), None)
|
|
root_name = preferred or (product_names[0] if product_names else "assembly")
|
|
|
|
return {
|
|
"root_name": root_name,
|
|
"parts": nodes,
|
|
"product_names": product_names,
|
|
"stats": {
|
|
"products": len(products),
|
|
"product_definitions": len(pd_to_pdf),
|
|
"nauo": len(edge_names),
|
|
"roots": len(roots),
|
|
"axis2_placement": text.upper().count("AXIS2_PLACEMENT_3D"),
|
|
"circles": len(re.findall(r"\bCIRCLE\b", text, flags=re.I)),
|
|
"cylinders": text.upper().count("CYLINDRICAL_SURFACE"),
|
|
"mode": "assembly_tree",
|
|
"geometry_backend": "text_only",
|
|
},
|
|
}
|