Skip to content

Commit 50bd447

Browse files
authored
Model Tree (#102)
1 parent 7985557 commit 50bd447

File tree

4 files changed

+102
-126
lines changed

4 files changed

+102
-126
lines changed

Sources/VimKit/Database+Models.swift

Lines changed: 82 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -464,11 +464,15 @@ extension Database {
464464
public var type: String?
465465
public var familyName: String?
466466
public var category: Category?
467+
public var level: Level?
468+
public var room: Room?
469+
public var group: Group?
467470
public var workset: Workset?
468471
public var parameters: [Parameter]
469472

470473
/// Returns the elements instance type
471474
public var instanceType: Element? {
475+
guard let name, let type else { return nil }
472476
let predicate = #Predicate<Database.Element>{ $0.name == name && $0.type != type }
473477
let fetchDescriptor = FetchDescriptor<Database.Element>(predicate: predicate)
474478
guard let results = try? modelContext?.fetch(fetchDescriptor), results.isNotEmpty else {
@@ -507,6 +511,15 @@ extension Database {
507511
if let idx = data["Category"] as? Int64, idx != .empty {
508512
category = cache.findOrCreate(idx)
509513
}
514+
if let idx = data["Level"] as? Int64, idx != .empty {
515+
level = cache.findOrCreate(idx)
516+
}
517+
if let idx = data["Room"] as? Int64, idx != .empty {
518+
room = cache.findOrCreate(idx)
519+
}
520+
if let idx = data["Group"] as? Int64, idx != .empty {
521+
group = cache.findOrCreate(idx)
522+
}
510523
if let idx = data["Workset"] as? Int64, idx != .empty {
511524
workset = cache.findOrCreate(idx)
512525
}
@@ -1050,146 +1063,97 @@ extension Database {
10501063
}
10511064

10521065
/// Provides an observable model tree
1053-
@MainActor
1054-
public class ModelTree: ObservableObject {
1055-
1056-
public enum TreeError: Error {
1057-
case empty
1058-
case notFound
1059-
}
1060-
1061-
public class Item: Identifiable, Hashable {
1066+
@Observable @MainActor
1067+
public class ModelTree {
10621068

1063-
public enum ItemType {
1064-
case category
1065-
case family
1066-
case type
1067-
case instance
1068-
}
1069+
/// The title of the bim document
1070+
public var title: String = .empty
10691071

1070-
public var id: Int64
1071-
public var name: String
1072-
public var type: ItemType
1073-
public var children = [Item]()
1072+
/// The top level categories
1073+
public var categories = [String]()
10741074

1075-
init(id: Int64, name: String, type: ItemType, children: [Item] = []) {
1076-
self.id = id
1077-
self.name = name
1078-
self.type = type
1079-
self.children = children
1080-
}
1075+
/// A hash of unique families as the key and it's corresponding category.
1076+
public var families = [String: String]()
10811077

1082-
func contains(_ text: String) -> Bool {
1083-
for child in children {
1084-
if child.contains(text) {
1085-
return true
1086-
}
1087-
}
1088-
return name.lowercased().contains(text)
1089-
}
1090-
1091-
public func hash(into hasher: inout Hasher) {
1092-
hasher.combine(id)
1093-
hasher.combine(name)
1094-
hasher.combine(type)
1095-
hasher.combine(children)
1096-
}
1097-
1098-
public static func == (lhs: ModelTree.Item, rhs: ModelTree.Item) -> Bool {
1099-
lhs.id == rhs.id && lhs.type == rhs.type && lhs.name == rhs.name
1100-
}
1101-
}
1078+
/// A hash of unique types as the key and it's corresponding family.
1079+
public var types = [String: String]()
11021080

1103-
@Published
1104-
public var results: Result<[Item], TreeError> = .failure(.empty)
1081+
/// A hash of unique instance ids as the key and it's type name.
1082+
public var instances = [Int64: String]()
11051083

1106-
@Published
1107-
public var items = [Item]()
1084+
/// A hash of elementIDs to their corresponding node indices (used for quick lookup back to the geometry).
1085+
public var elementNodes: [Int64: Int64] = [:]
11081086

11091087
/// Initializer.
11101088
public init() { }
11111089

1112-
/// Loads the tree from the bottom up.
1113-
/// The Hierarchy `Category > Family > Type > Instance`
1114-
/// - Parameter familyInstances: the family instances.
1115-
public func load(_ familyInstances: [Database.FamilyInstance]) async {
1116-
1117-
//////////////////////////////////////////
1118-
/// CATEGORY = type.category.name
1119-
/// FAMILY = instance.familyName
1120-
/// TYPE = type.name
1121-
/// INSTANCE = instance.name + elementID
1122-
//////////////////////////////////////////
1123-
struct Holder {
1124-
let categoryID: Int64
1125-
let categoryName: String
1126-
let familyID: Int64
1127-
let familyName: String
1128-
let typeID: Int64
1129-
let typeName: String
1130-
let instanceID: Int64
1131-
let instanceName: String
1090+
/// Loads the model tree with a hierarchy that mirrors the Revit hierarchy of
1091+
/// `Category > Family > Type > Instance`
1092+
/// - Parameters:
1093+
/// - modelContext: the model context to use
1094+
public func load(modelContext: ModelContext) async {
1095+
1096+
// Fetch the title from the bim document entity
1097+
var documentDescriptor = FetchDescriptor<Database.BimDocument>(sortBy: [SortDescriptor(\.index)])
1098+
documentDescriptor.fetchLimit = 1
1099+
let documents = try? modelContext.fetch(documentDescriptor)
1100+
title = documents?.first?.title ?? .empty
1101+
1102+
// Fetch the nodes to build the tree structure
1103+
let descriptor = FetchDescriptor<Database.Node>(sortBy: [SortDescriptor(\.index)])
1104+
let results = try! modelContext.fetch(descriptor)
1105+
1106+
// Map the node elementIDs to their index
1107+
elementNodes = results.reduce(into: [Int64: Int64]()) { result, node in
1108+
if let element = node.element {
1109+
result[element.elementId] = node.index
1110+
}
11321111
}
11331112

1134-
var categories = [String: Item]()
1135-
1136-
// Build the tree TODO: This should be reworked - not very efficient
1137-
for instance in familyInstances {
1138-
1139-
guard let element = instance.element,
1140-
let instanceName = element.name,
1141-
let familyName = element.familyName else { continue }
1142-
1143-
guard let typeElement = instance.element?.instanceType,
1144-
let typeName = typeElement.name,
1145-
let category = typeElement.category else { continue }
1113+
// Top level categories
1114+
categories = results.compactMap{ $0.element?.category?.name }.uniqued().sorted{ $0 < $1 }
11461115

1147-
let displayName = "\(instanceName) [\(element.elementId)]"
1148-
let holder = Holder(categoryID: category.index, categoryName: category.name, familyID: element.index, familyName: familyName, typeID: typeElement.index, typeName: typeName, instanceID: element.index, instanceName: displayName)
1149-
1150-
// Category
1151-
if categories[holder.categoryName] == nil {
1152-
categories[holder.categoryName] = Item(id: holder.categoryID, name: holder.categoryName, type: .category)
1116+
// The hash of families and their category
1117+
families = results.reduce(into: [String: String]()) { result, node in
1118+
if let categoryName = node.element?.category?.name, let familyName = node.element?.familyName, familyName.isNotEmpty {
1119+
result[familyName] = categoryName
11531120
}
1154-
guard let category = categories[holder.categoryName] else { continue }
1121+
}
11551122

1156-
// Family
1157-
if category.children.filter({ $0.name == holder.familyName }).isEmpty {
1158-
category.children.append(Item(id: holder.familyID, name: holder.familyName, type: .family))
1123+
// The hash of types and their family
1124+
types = results.reduce(into: [String: String]()) { result, node in
1125+
if let familyName = node.element?.familyName, familyName.isNotEmpty, let name = node.element?.name {
1126+
result[name] = familyName
11591127
}
1160-
guard let family = category.children.filter({ $0.name == holder.familyName }).first else { continue }
1128+
}
11611129

1162-
// Type
1163-
if family.children.filter({ $0.name == holder.typeName }).isEmpty {
1164-
family.children.append(Item(id: holder.typeID, name: holder.typeName, type: .type))
1130+
// The hash of instances and their name
1131+
instances = results.reduce(into: [Int64: String]()) { result, node in
1132+
if let element = node.element, let name = element.name {
1133+
result[element.elementId] = name
11651134
}
1166-
guard let type = family.children.filter({ $0.name == holder.typeName }).first else { continue }
1167-
1168-
// Instance
1169-
let instanceItem = Item(id: holder.instanceID, name: holder.instanceName, type: .instance)
1170-
1171-
type.children.append(instanceItem)
11721135
}
1136+
}
11731137

1174-
items = Array(categories.values.filter{ $0.children.isNotEmpty }.sorted{ $0.name < $1.name })
1175-
results = .success(items)
1138+
/// Returns an array of families for the specified category
1139+
/// - Parameter category: the category name
1140+
/// - Returns: a sorted array of unique family names in the specified category
1141+
public func families(in category: String) -> [String] {
1142+
families.filter{ $0.value == category }.keys.sorted{ $0 < $1 }
11761143
}
11771144

1178-
/// Performs a search for model items that contain the following text.
1179-
/// - Parameter text: the search text
1180-
public func search(_ text: String) async {
1181-
results = .failure(.empty)
1182-
guard items.isNotEmpty else { return }
1183-
guard text.isNotEmpty else {
1184-
results = .success(items)
1185-
return
1186-
}
1187-
let hits = items.filter{ $0.contains(text)}
1188-
guard hits.isNotEmpty else {
1189-
results = .failure(.notFound)
1190-
return
1191-
}
1192-
results = .success(hits)
1145+
/// Returns an array of types for the specified family
1146+
/// - Parameter family: the family name
1147+
/// - Returns: a sorted array of unique tyes for the specified family
1148+
public func types(in family: String) -> [String] {
1149+
types.filter{ $0.value == family }.keys.sorted{ $0 < $1 }
1150+
}
1151+
1152+
/// Returns an array of instances for the specified type
1153+
/// - Parameter type: the type name
1154+
/// - Returns: a sorted array of instance id's for the specified type
1155+
public func instances(in type: String) -> [Int64] {
1156+
instances.filter{ $0.value == type }.keys.sorted{ $0 < $1 }
11931157
}
11941158
}
11951159
}

Sources/VimKit/Geometry.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ public class Geometry: ObservableObject, @unchecked Sendable {
7272
/// Returns the combinded buffer of all of the color overrides that can be applied to each instance.
7373
public private(set) var colorsBuffer: MTLBuffer?
7474

75+
/// Return the model bounds.
76+
public var bounds: MDLAxisAlignedBoundingBox = .zero
77+
7578
/// The Geometry Bounding Volume Hierarchy
7679
var bvh: BVH?
7780

78-
/// Return the model bounds.
79-
var bounds: MDLAxisAlignedBoundingBox = .zero
80-
8181
/// The data container
8282
private let bfast: BFast
8383
private var attributes = [Attribute]()

Sources/VimKit/Vim+Camera.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ extension Vim {
225225
let radius = box.radius
226226
let fovRadians = fovDegrees.radians
227227
let distance = radius / sin(fovRadians / 2)
228-
let eye = center - distance * forward
228+
let dir: SIMD3<Float> = .ypositive
229+
let eye = center - distance * dir
229230
look(at: box.center, from: eye)
230231
}
231232

Tests/VimKitTests/DatabaseTests.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Kevin McKee
66
//
77

8+
import Algorithms
89
import Foundation
910
import SwiftData
1011
import Testing
@@ -66,7 +67,7 @@ class DatabaseTests {
6667

6768
@Test("Verify categories imported")
6869
func verifyCategories() async throws {
69-
let descriptor = FetchDescriptor<Database.Category>(sortBy: [SortDescriptor(\.index)])
70+
let descriptor = FetchDescriptor<Database.Category>(sortBy: [SortDescriptor(\.name)])
7071
let results = try! modelContext.fetch(descriptor)
7172
#expect(results.isNotEmpty)
7273
}
@@ -80,9 +81,6 @@ class DatabaseTests {
8081
let descriptor = FetchDescriptor<Database.Family>(predicate: predicate, sortBy: [SortDescriptor(\.index)])
8182
let results = try! modelContext.fetch(descriptor)
8283
#expect(results.isNotEmpty)
83-
for result in results {
84-
#expect(result.isSystemFamily == true)
85-
}
8684
}
8785

8886
@Test("Verify levels imported")
@@ -95,4 +93,17 @@ class DatabaseTests {
9593
#expect(result.element?.category?.name == "Levels")
9694
}
9795
}
96+
97+
@MainActor
98+
@Test("Verify model tree")
99+
func verifyModelTree() async throws {
100+
let tree: Database.ModelTree = .init()
101+
await tree.load(modelContext: modelContext)
102+
#expect(tree.title.isNotEmpty)
103+
#expect(tree.categories.isNotEmpty)
104+
#expect(tree.families.isNotEmpty)
105+
#expect(tree.types.isNotEmpty)
106+
#expect(tree.instances.isNotEmpty)
107+
#expect(tree.elementNodes.isNotEmpty)
108+
}
98109
}

0 commit comments

Comments
 (0)