From edf0a40759a9d525b8a6a851f4a5bfb2d6a800a1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 20:51:33 -0800 Subject: [PATCH] Fix asset browser BLP errors, M2 wireframes, and add anim filter - BLP: blp_convert takes one arg, not two; was passing an output path that caused conversion failures - M2: vertex header offsets were wrong (used 80/100 instead of 60/68), producing garbage vertex counts that failed the sanity check - Add "Hide .anim/.skin" checkbox (on by default) to filter ~30k companion files from the directory tree --- tools/asset_pipeline_gui.py | 76 ++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/tools/asset_pipeline_gui.py b/tools/asset_pipeline_gui.py index d3065788..24a94d4b 100755 --- a/tools/asset_pipeline_gui.py +++ b/tools/asset_pipeline_gui.py @@ -521,7 +521,11 @@ class AssetPipelineGUI: type_combo.pack(side="left", padx=(4, 8)) ttk.Button(top_bar, text="Search", command=self._browser_do_search).pack(side="left", padx=(0, 4)) - ttk.Button(top_bar, text="Reset", command=self._browser_reset_search).pack(side="left") + ttk.Button(top_bar, text="Reset", command=self._browser_reset_search).pack(side="left", padx=(0, 8)) + + self._browser_hide_anim_var = tk.BooleanVar(value=True) + ttk.Checkbutton(top_bar, text="Hide .anim/.skin", variable=self._browser_hide_anim_var, + command=self._browser_reset_search).pack(side="left") self._browser_count_var = tk.StringVar(value="") ttk.Label(top_bar, textvariable=self._browser_count_var).pack(side="right") @@ -583,28 +587,42 @@ class AssetPipelineGUI: self._browser_manifest_list = sorted(self._browser_manifest.keys(), key=str.lower) self._browser_count_var.set(f"{len(self._browser_manifest)} entries") - # Build directory tree index: dir_path -> ({subdirs}, [files]) + # Build directory tree indices: one full, one filtered # Single O(N) pass so tree operations are O(1) lookups - self._browser_dir_index: dict[str, tuple[set[str], list[str]]] = {} - for path in self._browser_manifest_list: + _hidden_exts = {".anim", ".skin"} + self._browser_dir_index_full = self._build_dir_index(self._browser_manifest_list) + filtered = [p for p in self._browser_manifest_list + if os.path.splitext(p)[1].lower() not in _hidden_exts] + self._browser_dir_index_filtered = self._build_dir_index(filtered) + + self._browser_populate_tree_root() + + @staticmethod + def _build_dir_index(paths: list[str]) -> dict[str, tuple[set[str], list[str]]]: + index: dict[str, tuple[set[str], list[str]]] = {} + for path in paths: parts = path.split("/") for depth in range(len(parts)): dir_key = "/".join(parts[:depth]) if depth > 0 else "" - if dir_key not in self._browser_dir_index: - self._browser_dir_index[dir_key] = (set(), []) - idx_entry = self._browser_dir_index[dir_key] + if dir_key not in index: + index[dir_key] = (set(), []) + entry = index[dir_key] if depth < len(parts) - 1: - idx_entry[0].add(parts[depth]) + entry[0].add(parts[depth]) else: - idx_entry[1].append(parts[depth]) + entry[1].append(parts[depth]) + return index - self._browser_populate_tree_root() + def _browser_active_index(self) -> dict[str, tuple[set[str], list[str]]]: + if self._browser_hide_anim_var.get(): + return self._browser_dir_index_filtered + return self._browser_dir_index_full def _browser_populate_tree_root(self) -> None: self._browser_tree.delete(*self._browser_tree.get_children()) self._browser_tree_populated.clear() - root_entry = self._browser_dir_index.get("", (set(), [])) + root_entry = self._browser_active_index().get("", (set(), [])) subdirs, files = root_entry for name in sorted(subdirs, key=str.lower): @@ -625,7 +643,7 @@ class AssetPipelineGUI: if self._browser_tree.exists(dummy): self._browser_tree.delete(dummy) - dir_entry = self._browser_dir_index.get(node, (set(), [])) + dir_entry = self._browser_active_index().get(node, (set(), [])) child_dirs, child_files = dir_entry for d in sorted(child_dirs, key=str.lower): @@ -665,13 +683,15 @@ class AssetPipelineGUI: "Text": {".xml", ".lua", ".json", ".html", ".toc", ".txt", ".wtf"}, } + hidden_exts = {".anim", ".skin"} if self._browser_hide_anim_var.get() else set() results: list[str] = [] exts = type_exts.get(type_filter) for path in self._browser_manifest_list: - if exts: - ext = os.path.splitext(path)[1].lower() - if ext not in exts: - continue + ext = os.path.splitext(path)[1].lower() + if ext in hidden_exts: + continue + if exts and ext not in exts: + continue if query and query not in path.lower(): continue results.append(path) @@ -791,22 +811,17 @@ class AssetPipelineGUI: cached_png = cache_dir / f"{cache_key}.png" if not cached_png.exists(): - # Convert BLP to PNG + # blp_convert outputs PNG alongside source: foo.blp -> foo.png try: - with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: - tmp_path = tmp.name result = subprocess.run( - [str(blp_convert), "--to-png", str(file_path), tmp_path], + [str(blp_convert), "--to-png", str(file_path)], capture_output=True, text=True, timeout=10 ) - if result.returncode != 0: + output_png = file_path.with_suffix(".png") + if result.returncode != 0 or not output_png.exists(): ttk.Label(self._browser_preview_frame, text=f"blp_convert failed:\n{result.stderr[:500]}").pack(expand=True) - try: - os.unlink(tmp_path) - except OSError: - pass return - shutil.move(tmp_path, cached_png) + shutil.move(str(output_png), cached_png) except Exception as exc: ttk.Label(self._browser_preview_frame, text=f"Conversion error: {exc}").pack(expand=True) return @@ -858,12 +873,13 @@ class AssetPipelineGUI: version = struct.unpack_from(" 500000 or ofs_verts + n_verts * 48 > len(data): ttk.Label(self._browser_preview_frame, text=f"M2: {n_verts} vertices (no preview)").pack(expand=True)