# -*- 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)