140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
||
|
|
扫描 agv/*.glb 中的网格节点,生成最小化 JSON 配置骨架。
|
||
|
|
|
||
|
|
- 组件显示名为:组件1、组件2、组件3 …(不沿用 Blender 原始节点名)
|
||
|
|
- 每个组件仅生成默认骨架(static / 无 motion);不识别灯光、轮子等类型
|
||
|
|
- 灯光/轮子等专用字段请在查看器中改类型后保存,或由你手写 JSON
|
||
|
|
- 不生成 actions;基准在 baseline,动作在查看器按组件配置
|
||
|
|
|
||
|
|
用法:
|
||
|
|
python analyze_agv.py
|
||
|
|
python analyze_agv.py 英招
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from pygltflib import GLTF2
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parent
|
||
|
|
AGV_DIR = ROOT / "agv"
|
||
|
|
|
||
|
|
def component_label(index_1based: int) -> str:
|
||
|
|
return f"组件{index_1based}"
|
||
|
|
|
||
|
|
|
||
|
|
def _detect_glb_root(glb_path: Path) -> str:
|
||
|
|
try:
|
||
|
|
g = GLTF2().load(str(glb_path))
|
||
|
|
if g.scenes and g.scenes[0].nodes:
|
||
|
|
idx = g.scenes[0].nodes[0]
|
||
|
|
if 0 <= idx < len(g.nodes) and g.nodes[idx].name:
|
||
|
|
return g.nodes[idx].name
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return "SceneRoot"
|
||
|
|
|
||
|
|
|
||
|
|
def update_manifest() -> Path:
|
||
|
|
AGV_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
models = []
|
||
|
|
for path in sorted(AGV_DIR.glob("*.glb")):
|
||
|
|
cfg = path.with_suffix(".json")
|
||
|
|
models.append(
|
||
|
|
{
|
||
|
|
"id": path.stem,
|
||
|
|
"name": path.stem.replace("_", " "),
|
||
|
|
"file": path.name,
|
||
|
|
"root": _detect_glb_root(path),
|
||
|
|
"config": cfg.name if cfg.exists() else None,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
manifest = AGV_DIR / "manifest.json"
|
||
|
|
manifest.write_text(json.dumps({"models": models}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
|
return manifest
|
||
|
|
|
||
|
|
|
||
|
|
def list_mesh_nodes(glb_path: Path, root_name: str) -> list[dict[str, Any]]:
|
||
|
|
"""列出除根节点外的所有命名节点(含无 mesh 的空节点,便于完整对应)。"""
|
||
|
|
g = GLTF2().load(str(glb_path))
|
||
|
|
out = []
|
||
|
|
for node in g.nodes:
|
||
|
|
if not node.name or node.name == root_name:
|
||
|
|
continue
|
||
|
|
out.append({"name": node.name, "hasMesh": node.mesh is not None})
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def analyze_glb(glb_path: Path, model_id: str | None = None) -> dict[str, Any]:
|
||
|
|
model_id = model_id or glb_path.stem
|
||
|
|
root = _detect_glb_root(glb_path)
|
||
|
|
# 仅含实际网格的节点,避免把空集合/根分组也算成组件
|
||
|
|
nodes = [n for n in list_mesh_nodes(glb_path, root) if n["hasMesh"]]
|
||
|
|
|
||
|
|
components: dict[str, Any] = {}
|
||
|
|
for i, n in enumerate(nodes, start=1):
|
||
|
|
label = component_label(i)
|
||
|
|
components[label] = {
|
||
|
|
"glbNode": (n["name"] or "").replace("\u00a0", " ").strip(),
|
||
|
|
"type": "static",
|
||
|
|
"motion": None,
|
||
|
|
"hasMesh": n["hasMesh"],
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"version": 2,
|
||
|
|
"description": "analyze 仅生成组件骨架;类型与动作请在查看器按组件配置后导出。",
|
||
|
|
"model": {"id": model_id, "file": glb_path.name, "root": root},
|
||
|
|
"baseline": components,
|
||
|
|
"groups": {},
|
||
|
|
"actions": {},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def write_config(glb_path: Path, model_id: str | None = None) -> Path:
|
||
|
|
cfg = analyze_glb(glb_path, model_id)
|
||
|
|
out = Path(AGV_DIR) / f"{cfg['model']['id']}.json"
|
||
|
|
out.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def analyze_all() -> list[Path]:
|
||
|
|
written = []
|
||
|
|
for glb in sorted(Path(AGV_DIR).glob("*.glb")):
|
||
|
|
written.append(write_config(glb))
|
||
|
|
update_manifest()
|
||
|
|
return written
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str]) -> None:
|
||
|
|
if len(argv) > 1:
|
||
|
|
arg = argv[1]
|
||
|
|
path = Path(arg)
|
||
|
|
if not path.is_absolute():
|
||
|
|
path = Path(AGV_DIR) / (arg if arg.endswith(".glb") else f"{arg}.glb")
|
||
|
|
if not path.exists():
|
||
|
|
print(f"文件不存在: {path}")
|
||
|
|
sys.exit(1)
|
||
|
|
out = write_config(path)
|
||
|
|
update_manifest()
|
||
|
|
print(f"已生成: {out}")
|
||
|
|
data = json.loads(out.read_text(encoding="utf-8"))
|
||
|
|
print(f" 共 {len(data.get('baseline', data.get('components', {})))} 个基准组件(组件1 …)")
|
||
|
|
print(" actions: 空(动作在查看器按组件配置)")
|
||
|
|
return
|
||
|
|
|
||
|
|
paths = analyze_all()
|
||
|
|
print(f"已分析 {len(paths)} 个模型:")
|
||
|
|
for p in paths:
|
||
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||
|
|
bl = data.get("baseline", data.get("components", {}))
|
||
|
|
print(f" {p.name}: {len(bl)} 个基准组件, actions={len(data.get('actions', data.get('states', {})))} 个")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main(sys.argv)
|