From 6db4010d32f8f1a228a610c80c16d27c24fa7e04 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Tue, 6 Oct 2026 22:46:05 -0700 Subject: [PATCH 1/2] [Patch] Hide cook-source folders in asset browser, reveal via context menu Source folders sharing a stem with a runtime asset (e.g. bl/ for bl.untoldpack) cluttered the listing; they're now hidden and reachable through a "Reveal Source Folder" menu item. Co-Authored-By: Claude Sonnet 5 --- .../AssetBrowserView+Contents.swift | 32 ++++++++++++++++++- .../AssetBrowserView+Loading.swift | 23 +++++++++++-- .../Editor/AssetBrowser/AssetModel.swift | 4 +++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Contents.swift b/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Contents.swift index 4b1570b..fd05f22 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Contents.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Contents.swift @@ -156,6 +156,17 @@ extension AssetBrowserView { } } } + if let sourceFolder = asset.sourceFolder { + Button { + if selectedDirURL != nil { + selectedDirURL = sourceFolder + } else if selectedCategory != AssetCategory.scripts.rawValue { + folderPathStack.append(sourceFolder) + } + } label: { + Label("Reveal Source Folder", systemImage: "folder") + } + } Button(role: .destructive) { pendingDeleteAsset = asset showDeleteConfirmation = true @@ -183,11 +194,25 @@ extension AssetBrowserView { @ViewBuilder func folderContentsView(for folder: URL, selectionManager _: SelectionManager) -> some View { if let contents = try? FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { + // A folder that shares its stem with a sibling runtime asset is that + // asset's cook source (see `runtimeExportLocation`): it's not listed on + // its own, only reachable via that asset's "Reveal Source Folder" menu. + let sourceFolderByStem: [String: URL] = contents.reduce(into: [:]) { result, item in + guard allRuntimeAssetExtensions.contains(item.pathExtension.lowercased()) else { return } + let stem = item.deletingPathExtension().lastPathComponent + let candidateFolder = folder.appendingPathComponent(stem, isDirectory: true) + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: candidateFolder.path, isDirectory: &isDir), isDir.boolValue { + result[stem] = candidateFolder + } + } + let items = contents.compactMap { item -> Asset? in var isDir: ObjCBool = false if FileManager.default.fileExists(atPath: item.path, isDirectory: &isDir) { let itemCategory = selectedCategory ?? inferCategory(for: item)?.rawValue ?? "" if isDir.boolValue { + guard sourceFolderByStem[item.lastPathComponent] == nil else { return nil } return Asset(name: item.lastPathComponent, category: itemCategory, path: item, isFolder: true) } else { // Imported sources (USD, .blend) are listed too: they stay in the @@ -200,9 +225,14 @@ extension AssetBrowserView { .union(sourceAssetExtensions) guard allowedExtensions.contains(itemExtension) else { return nil } + let sourceFolder = allRuntimeAssetExtensions.contains(itemExtension) + ? sourceFolderByStem[item.deletingPathExtension().lastPathComponent] + : nil + return Asset(name: item.lastPathComponent, category: itemCategory, - path: item) + path: item, + sourceFolder: sourceFolder) } } return nil diff --git a/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Loading.swift b/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Loading.swift index 24b7e32..61a7a95 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Loading.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowser/AssetBrowserView+Loading.swift @@ -50,10 +50,24 @@ extension AssetBrowserView { includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] ) { + // A folder that shares its stem with a sibling runtime asset is that + // asset's cook source (see `runtimeExportLocation`): it's not listed on + // its own, only reachable via that asset's "Reveal Source Folder" menu. + let sourceFolderByStem: [String: URL] = contents.reduce(into: [:]) { result, item in + guard allRuntimeAssetExtensions.contains(item.pathExtension.lowercased()) else { return } + let stem = item.deletingPathExtension().lastPathComponent + let candidateFolder = categoryPath.appendingPathComponent(stem, isDirectory: true) + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: candidateFolder.path, isDirectory: &isDir), isDir.boolValue { + result[stem] = candidateFolder + } + } + for item in contents { var isDir: ObjCBool = false if FileManager.default.fileExists(atPath: item.path, isDirectory: &isDir) { if isDir.boolValue { + guard sourceFolderByStem[item.lastPathComponent] == nil else { continue } // It’s a folder — valid for all categories categoryAssets.append(Asset(name: item.lastPathComponent, category: category.rawValue, @@ -81,11 +95,16 @@ extension AssetBrowserView { path: item, isFolder: false)) } else if category == .models || category == .animations { - if runtimeAssetExtensions(for: category).contains(item.pathExtension.lowercased()) || sourceAssetExtensions.contains(item.pathExtension.lowercased()) { + let itemExtension = item.pathExtension.lowercased() + if runtimeAssetExtensions(for: category).contains(itemExtension) || sourceAssetExtensions.contains(itemExtension) { + let sourceFolder = allRuntimeAssetExtensions.contains(itemExtension) + ? sourceFolderByStem[item.deletingPathExtension().lastPathComponent] + : nil categoryAssets.append(Asset(name: item.lastPathComponent, category: category.rawValue, path: item, - isFolder: false)) + isFolder: false, + sourceFolder: sourceFolder)) } } else if category == .scripts { // Not used anymore due to flat listing, but keep for safety (won’t execute due to continue above) diff --git a/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift b/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift index e8f43a3..8e69d12 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift @@ -17,6 +17,10 @@ public struct Asset: Identifiable { public let category: String public let path: URL var isFolder: Bool = false + /// The source/working folder this runtime asset was cooked from (same stem, + /// e.g. `bl.untoldpack` <- `bl/`), if one exists. Hidden from the listing in + /// its own right; reachable via the asset's "Reveal Source Folder" context menu. + var sourceFolder: URL? = nil } enum AssetCategory: String, CaseIterable { From f5845d5447f947e1852e63c243594c7263ad5e18 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Tue, 6 Oct 2026 23:30:31 -0700 Subject: [PATCH 2/2] [Chores] Formatted files --- Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift b/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift index 8e69d12..da65f83 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift @@ -20,7 +20,7 @@ public struct Asset: Identifiable { /// The source/working folder this runtime asset was cooked from (same stem, /// e.g. `bl.untoldpack` <- `bl/`), if one exists. Hidden from the listing in /// its own right; reachable via the asset's "Reveal Source Folder" context menu. - var sourceFolder: URL? = nil + var sourceFolder: URL? } enum AssetCategory: String, CaseIterable {