Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Sources/UntoldEditor/Editor/AssetBrowser/AssetModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}

enum AssetCategory: String, CaseIterable {
Expand Down
Loading