Files
step2urdf/app/services/step_text.py
T
sunxianghui 93773f3887 Initial commit: step2urdf tool with handtuned JSON import.
Includes FastAPI backend, vendored step2urdf frontend, and A7 handtuned arm JSON for URDF generation.
2026-08-26 15:32:47 +08:00

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",
},
}