From 22920691ccfe9a5f4216326ed4000e8881d1f3a8 Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Fri, 9 May 2025 18:27:50 -0700 Subject: [PATCH 1/4] WIP --- .../Model/VimAssistant+Handler.swift | 85 +++++++++++++------ .../VimAssistant/Model/VimPrediction.swift | 34 +++++++- .../Views/VimPredictionView.swift | 4 +- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/Sources/VimAssistant/Model/VimAssistant+Handler.swift b/Sources/VimAssistant/Model/VimAssistant+Handler.swift index 0d8f506..7cce592 100644 --- a/Sources/VimAssistant/Model/VimAssistant+Handler.swift +++ b/Sources/VimAssistant/Model/VimAssistant+Handler.swift @@ -23,48 +23,79 @@ public extension VimAssistant { let action = bestPrediction.action - print("❤️", bestPrediction) + guard let db = vim.db else { return } + let modelContext = ModelContext(db.modelContainer) - for entity in prediction.entities { - if entity.label == "CON-BIM-CATG" { - print("🚀", entity.value) - perform(vim: vim, action: action, category: entity.value) - } else if entity.label == "CON-BIM-FAML" { + var ids: Set = .init() - } else if entity.label == "CON-BIM-TYPE" { + // TODO: This needs reworked but predicate disjunction is way effin complicated. + let predicates = buildPredicates(prediction: prediction) + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.index)]) + guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return } - } + for predicate in predicates { + guard let filtered = try? results.filter(predicate) else { continue } + ids.formUnion(filtered.compactMap{ Int($0.index) }) } - } - - private func perform(vim: Vim, action: VimPrediction.Action, category: String) { - guard let db = vim.db else { return } - let modelContext = ModelContext(db.modelContainer) + guard ids.isNotEmpty else { return } - let orderedSame = ComparisonResult.orderedSame - let predicate = #Predicate{ - if let element = $0.element, let cat = element.category { - return cat.name.caseInsensitiveCompare(category) == orderedSame - } else { - return false - } - } - - let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.index)]) - guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return } - let ids = results.compactMap{ Int($0.index) } Task { switch action { case .hide: - await vim.hide(ids: ids) + await vim.hide(ids: ids.sorted()) case .isolate: - await vim.isolate(ids: ids) + await vim.isolate(ids: ids.sorted()) case .quantify: break } } } + /// Builds a node predicate that matches on category name. + /// - Parameter name: the category name to match + /// - Returns: a predicate that matches case insensitive category name + private func buildCategoryPredicate(_ name: String) -> Predicate { + let predicate = #Predicate { node in + if let element = node.element, let category = element.category { + return category.name.localizedStandardContains(name) + } else { + return false + } + } + return predicate + } + + /// Builds a node predicate that matches on family name. + /// - Parameter name: the family name to match + /// - Returns: a predicate that matches case insensitive family name + private func buildFamilyPredicate(_ name: String) -> Predicate { + let predicate = #Predicate { node in + if let element = node.element, let familyName = element.familyName { + return familyName.localizedStandardContains(name) + } else { + return false + } + } + return predicate + } + + /// Builds a list of predicates to query on based on the given prediction. + /// - Parameter prediction: the prediction to use + /// - Returns: a list of node predicates + private func buildPredicates(prediction: VimPrediction) -> [Predicate] { + let categories = prediction.entities.filter{ $0.label == .bimCategory }.map { $0.value } + let families = prediction.entities.filter{ $0.label == .bimFamily }.map { $0.value } + let categoryPredicates = categories.map { buildCategoryPredicate($0) } + let familyPredicates = families.map { buildFamilyPredicate($0) } + let predicates = categoryPredicates + familyPredicates + return predicates + } + } +} + +extension Array where Element == String { + func containsIgnoringCase(_ element: Element) -> Bool { + contains { $0.caseInsensitiveCompare(element) == .orderedSame } } } diff --git a/Sources/VimAssistant/Model/VimPrediction.swift b/Sources/VimAssistant/Model/VimPrediction.swift index 3c3a180..23be00d 100644 --- a/Sources/VimAssistant/Model/VimPrediction.swift +++ b/Sources/VimAssistant/Model/VimPrediction.swift @@ -16,6 +16,33 @@ public struct VimPrediction: Decodable, Equatable { case tokens = "tokens" } + enum NerLabel: String, Identifiable { + + case person = "PERSON" + case organization = "ORGANIZATION" + case location = "LOCATION" + case date = "DATE" + case time = "TIME" + case event = "EVENT" + case workOfArt = "WORK_OF_ART" + case fac = "FAC" + case gpe = "GPE" + case language = "LANGUAGE" + case law = "LAW" + case norp = "NORP" + case cardinal = "CARDINAL" + case bimCategory = "CON-BIM-CATG" + case bimFamily = "CON-BIM-FAML" + case bimType = "CON-BIM-TYPE" + case bimInstance = "CON-BIM-INST" + case bimLevel = "CON-BIM-LEVL" + case bimView = "CON-BIM-VIEW" + + public var id: String { + rawValue + } + } + enum Action: String, Codable, Identifiable { case isolate = "ISOLATE" case hide = "HIDE" @@ -76,17 +103,18 @@ public struct VimPrediction: Decodable, Equatable { case end = "end" } - var label: String + var label: NerLabel var value: String = .empty var range: Range public var id: String { - label + "_\(range)" + label.rawValue + "_\(range)" } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - label = try values.decode(String.self, forKey: .label) + let labelString = try values.decode(String.self, forKey: .label) + label = .init(rawValue: labelString)! let start = try values.decode(Int.self, forKey: .start) let end = try values.decode(Int.self, forKey: .end) range = start.. Date: Fri, 9 May 2025 18:30:32 -0700 Subject: [PATCH 2/4] Comments --- Sources/VimAssistant/Model/VimAssistant+Handler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/VimAssistant/Model/VimAssistant+Handler.swift b/Sources/VimAssistant/Model/VimAssistant+Handler.swift index 7cce592..1fd818f 100644 --- a/Sources/VimAssistant/Model/VimAssistant+Handler.swift +++ b/Sources/VimAssistant/Model/VimAssistant+Handler.swift @@ -28,7 +28,7 @@ public extension VimAssistant { var ids: Set = .init() - // TODO: This needs reworked but predicate disjunction is way effin complicated. + // TODO: This is highly inefficient and needs reworked but predicate disjunction is way effin complicated and frankly stupid. let predicates = buildPredicates(prediction: prediction) let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.index)]) guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return } From 3205ccd286cb106c56c8cd3837121a79ee306ee6 Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Sat, 10 May 2025 13:28:19 -0700 Subject: [PATCH 3/4] WIP --- README.md | 38 +++++----- .../Model/VimAssistant+Handler.swift | 76 +++++++++++++------ .../VimAssistant/Model/VimPrediction.swift | 23 ++++-- .../Views/VimPredictionView.swift | 13 ++-- 4 files changed, 99 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ec3bb77..e568b92 100644 --- a/README.md +++ b/README.md @@ -24,32 +24,36 @@ Some examples of categorized actions include (but not limited to): ### Label Scheme The base model was created with the OntoNotes 5.0 NER annotations which includes: -* **PERSON**: Individual names (e.g., Barack Obama). -* **ORGANIZATION**: Company or institution names (e.g., Apple). -* **LOCATION**: Geographical places (e.g., Tokyo). +* **CARDINAL**: Cardinal numbers (e.g., 1, 2, 3). * **DATE**: Dates (e.g., May 8, 2025). -* **TIME**: Times (e.g., 10:00 AM). * **EVENT**: Names of events (e.g., World Series). -* **WORK\_OF\_ART**: Names of works of art (e.g., "Hamlet"). * **FAC**: Buildings or facilities (e.g., White House). * **GPE**: Geo-political entities (e.g., United States). * **LANGUAGE**: Names of languages (e.g., English). * **LAW**: Legal names (e.g., The Constitution). -* **NORP**: National/religious/political group (e.g., Democrats). -* **CARDINAL**: Cardinal numbers (e.g., 1, 2, 3). +* **LOC**: Represents locations (e.g., "New York City"). +* **MONEY**: Indicates monetary values (e.g., "100 dollars"). +* **NORP**: Represents national or political or religious groups (e.g., "Democrats", "the Catholic Church"). +* **ORDINAL**: Denotes ordinal numbers (e.g., "first", "second", "10th"). +* **ORG**: Represents organizations (e.g., "Google", "Microsoft"). +* **PERCENT**: Denotes percentages (e.g., "10%", "20%"). +* **PERSON**: Individual names (e.g., Barack Obama). +* **PRODUCT**: Represents products (e.g., "iPhone", "MacBook"). +* **QUANTITY**: Indicates measurements or quantities (e.g., "10 kilograms"). +* **TIME**: Times (e.g., 10:00 AM). +* **WORK\_OF\_ART**: Names of works of art (e.g., "Hamlet"). The trained model provides Construction NER annotations: -* **CON-BIM-CATG**: BIM Category - a high-level classification for families and elements, grouping them based on their functional type. -* **CON-BIM-FAML**: BIM Family - a collection of elements that share common properties, behaviors, and physical characteristics. -* **CON-BIM-TYPE**: BIM Type - a specific instantiation of a family that defines a unique set of parameters, essentially a variation within a family. Think of it as a specific size, material, or configuration of a particular family, such as a 3' x 6' door within a door family. -* **CON-BIM-INST**: BIM Instance - a single, unique occurrence of a family type placed within a model. -* **CON-BIM-LEVL**: BIM Level - a horizontal plane used to define the vertical position of elements like walls, floors, and ceilings. -* **CON-BIM-VIEW**: BIM View - represents a specific way of looking at the model, whether it's a 2D plan, elevation, section, or 3D view. - +* **CON\_BIM\_CATG**: BIM Category - a high-level classification for families and elements, grouping them based on their functional type. +* **CON\_BIM\_FAML**: BIM Family - a collection of elements that share common properties, behaviors, and physical characteristics. +* **CON\_BIM\_TYPE**: BIM Type - a specific instantiation of a family that defines a unique set of parameters, essentially a variation within a family. Think of it as a specific size, material, or configuration of a particular family, such as a 3' x 6' door within a door family. +* **CON\_BIM\_INST**: BIM Instance - a single, unique occurrence of a family type placed within a model. +* **CON\_BIM\_LEVL**: BIM Level - a horizontal plane used to define the vertical position of elements like walls, floors, and ceilings. +* **CON\_BIM\_VIEW**: BIM View - represents a specific way of looking at the model, whether it's a 2D plan, elevation, section, or 3D view. -| Component | Labels | +| Component | Labels | | -------- | ------- | -| named entities | CARDINAL, DATE, EVENT, FAC, GPE, LANGUAGE, LAW, LOC, MONEY, NORP, ORDINAL, ORG, PERCENT, PERSON, PRODUCT, QUANTITY, TIME, WORK_OF_ART, CON-BIM-CATG, CON-BIM-FAML, CON-BIM-TYPE, CON-BIM-INST, CON-BIM-LEVL, CON-BIM-VIEW | -| categories | ISOLATE, HIDE, QUANTIFY | +| named entities | CARDINAL, DATE, EVENT, FAC, GPE, LANGUAGE, LAW, LOC, MONEY, NORP, ORDINAL, ORG, PERCENT, PERSON, PRODUCT, QUANTITY, TIME, WORK\_OF\_ART, CON\_BIM\_CATG, CON\_BIM\_FAML, CON\_BIM\_TYPE, CON\_BIM\_INST, CON\_BIM\_LEVL, CON\_BIM\_VIEW | +| categories | ISOLATE, HIDE, QUANTIFY, ZOOM\_IN, ZOOM\_OUT, PAN\_LEFT, PAN\_RIGHT, PAN\_UP, PAN\_DOWN, LOOK\_LEFT, LOOK\_RIGHT, LOOK\_UP, LOOK\_DOWN | diff --git a/Sources/VimAssistant/Model/VimAssistant+Handler.swift b/Sources/VimAssistant/Model/VimAssistant+Handler.swift index 1fd818f..8434f0b 100644 --- a/Sources/VimAssistant/Model/VimAssistant+Handler.swift +++ b/Sources/VimAssistant/Model/VimAssistant+Handler.swift @@ -19,37 +19,69 @@ public extension VimAssistant { /// - prediction: the prediction func handle(vim: Vim, prediction: VimPrediction?) { guard let prediction, let bestPrediction = prediction.bestPrediction, bestPrediction.confidence >= 0.85 else { return } - guard prediction.entities.isNotEmpty else { return } - let action = bestPrediction.action + let ids = collect(vim: vim, prediction: prediction) + Task { @MainActor in + switch action { + case .hide: + guard ids.isNotEmpty else { return } + await vim.hide(ids: ids) + case .isolate: + guard ids.isNotEmpty else { return } + await vim.isolate(ids: ids) + case .quantify: + // TODO: Probably just emit an event that shows the quantities view + break + case .zoomIn: + await vim.zoom() + case .zoomOut: + await vim.zoom(out: true) + case .lookLeft: + await vim.look(.left) + case .lookRight: + await vim.look(.right) + case .lookUp: + await vim.look(.up) + case .lookDown: + await vim.look(.down) + case .panLeft: + await vim.pan(.left) + case .panRight: + await vim.pan(.right) + case .panUp: + await vim.pan(.up) + case .panDown: + await vim.pan(.down) + } + } + } - guard let db = vim.db else { return } - let modelContext = ModelContext(db.modelContainer) + private func collect(vim: Vim, prediction: VimPrediction) -> [Int] { - var ids: Set = .init() + guard let bestPrediction = prediction.bestPrediction, prediction.entities.isNotEmpty else { return []} + let action = bestPrediction.action - // TODO: This is highly inefficient and needs reworked but predicate disjunction is way effin complicated and frankly stupid. - let predicates = buildPredicates(prediction: prediction) - let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.index)]) - guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return } + switch action { + case .hide, .isolate: + guard let db = vim.db else { return [] } + let modelContext = ModelContext(db.modelContainer) - for predicate in predicates { - guard let filtered = try? results.filter(predicate) else { continue } - ids.formUnion(filtered.compactMap{ Int($0.index) }) - } + var ids: Set = .init() - guard ids.isNotEmpty else { return } + // TODO: This is highly inefficient and needs reworked but predicate disjunction is way effin complicated and frankly stupid. + let predicates = buildPredicates(prediction: prediction) + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.index)]) + guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return [] } - Task { - switch action { - case .hide: - await vim.hide(ids: ids.sorted()) - case .isolate: - await vim.isolate(ids: ids.sorted()) - case .quantify: - break + for predicate in predicates { + guard let filtered = try? results.filter(predicate) else { continue } + ids.formUnion(filtered.compactMap{ Int($0.index) }) } + return ids.sorted() + case .quantify, .zoomIn, .zoomOut, .lookLeft, .lookRight, .lookUp, .lookDown, .panLeft, .panRight, .panUp, .panDown: + return [] } + } /// Builds a node predicate that matches on category name. diff --git a/Sources/VimAssistant/Model/VimPrediction.swift b/Sources/VimAssistant/Model/VimPrediction.swift index 23be00d..1f38452 100644 --- a/Sources/VimAssistant/Model/VimPrediction.swift +++ b/Sources/VimAssistant/Model/VimPrediction.swift @@ -30,13 +30,14 @@ public struct VimPrediction: Decodable, Equatable { case language = "LANGUAGE" case law = "LAW" case norp = "NORP" + case product = "PRODUCT" case cardinal = "CARDINAL" - case bimCategory = "CON-BIM-CATG" - case bimFamily = "CON-BIM-FAML" - case bimType = "CON-BIM-TYPE" - case bimInstance = "CON-BIM-INST" - case bimLevel = "CON-BIM-LEVL" - case bimView = "CON-BIM-VIEW" + case bimCategory = "CON_BIM_CATG" + case bimFamily = "CON_BIM_FAML" + case bimType = "CON_BIM_TYPE" + case bimInstance = "CON_BIM_INST" + case bimLevel = "CON_BIM_LEVL" + case bimView = "CON_BIM_VIEW" public var id: String { rawValue @@ -47,6 +48,16 @@ public struct VimPrediction: Decodable, Equatable { case isolate = "ISOLATE" case hide = "HIDE" case quantify = "QUANTIFY" + case zoomIn = "ZOOM_IN" + case zoomOut = "ZOOM_OUT" + case lookLeft = "LOOK_LEFT" + case lookRight = "LOOK_RIGHT" + case lookUp = "LOOK_UP" + case lookDown = "LOOK_DOWN" + case panLeft = "PAN_LEFT" + case panRight = "PAN_RIGHT" + case panUp = "PAN_UP" + case panDown = "PAN_DOWN" public var id: String { rawValue diff --git a/Sources/VimAssistant/Views/VimPredictionView.swift b/Sources/VimAssistant/Views/VimPredictionView.swift index b8f0303..c2cedae 100644 --- a/Sources/VimAssistant/Views/VimPredictionView.swift +++ b/Sources/VimAssistant/Views/VimPredictionView.swift @@ -71,7 +71,7 @@ struct VimPredictionView: View { for entity in prediction.entities { let entityText = text[entity.range] var attributedEntityString = AttributedString(entityText) - attributedEntityString.foregroundColor = .orange + attributedEntityString.foregroundColor = .cyan attributedEntityString.underlineStyle = .single attributedEntityString.link = URL(string: "/\(entity.label)/\(entity.value)")! result.replaceSubrange(bounds: entity.range, with: attributedEntityString) @@ -102,9 +102,10 @@ struct VimPredictionView: View { Text(text[entity.range]) .bold() Text(entity.label.rawValue) - .padding(2) - .background(Color.orange) - .cornerRadius(4) + .padding(1) + .background(Color.cyan) + .foregroundStyle(Color.black) + .cornerRadius(2) } } if let bestPrediction = prediction.bestPrediction { @@ -116,7 +117,7 @@ struct VimPredictionView: View { HStack { Text(bestPrediction.action.rawValue.lowercased()) .bold() - Text(bestPrediction.confidence.formatted(.percent)) + Text(bestPrediction.confidence.formatted(.percent.precision(.fractionLength(2)))) .foregroundStyle(predictionConfidenceColor) } } @@ -128,7 +129,7 @@ struct VimPredictionView: View { #Preview { - let json = "{\"text\":\"Hide all walls and air terminals \",\"ents\":[{\"start\":9,\"end\":14,\"label\":\"CON-BIM-CATG\"},{\"start\":19,\"end\":32,\"label\":\"CON-BIM-CATG\"}],\"cats\":{\"ISOLATE\":0.0122569752857089,\"HIDE\":0.978784739971161,\"QUANTIFY\":0.00895828753709793}}" + let json = "{\"text\":\"Hide all walls and air terminals \",\"ents\":[{\"start\":9,\"end\":14,\"label\":\"CON_BIM_CATG\"},{\"start\":19,\"end\":32,\"label\":\"CON_BIM_CATG\"}],\"cats\":{\"ISOLATE\":0.0122569752857089,\"HIDE\":0.978784739971161,\"QUANTIFY\":0.00895828753709793}}" let prediction = try! JSONDecoder().decode(VimPrediction.self, from: json.data(using: .utf8)!) VimPredictionView(prediction: prediction, explain: .constant(true)) } From 1d1e9d77bd0a7218492febcecc95af71e16e4fbd Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Mon, 12 May 2025 13:57:21 -0700 Subject: [PATCH 4/4] WIP --- Package.swift | 2 +- .../Model/VimAssistant+Handler.swift | 91 ++++++++----------- 2 files changed, 37 insertions(+), 56 deletions(-) diff --git a/Package.swift b/Package.swift index 26261ea..1ed2725 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 7)) + .package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 8)) ], targets: [ .target( diff --git a/Sources/VimAssistant/Model/VimAssistant+Handler.swift b/Sources/VimAssistant/Model/VimAssistant+Handler.swift index 8434f0b..2d11a5d 100644 --- a/Sources/VimAssistant/Model/VimAssistant+Handler.swift +++ b/Sources/VimAssistant/Model/VimAssistant+Handler.swift @@ -33,25 +33,25 @@ public extension VimAssistant { // TODO: Probably just emit an event that shows the quantities view break case .zoomIn: - await vim.zoom() + vim.zoom() case .zoomOut: - await vim.zoom(out: true) + vim.zoom(out: true) case .lookLeft: - await vim.look(.left) + vim.look(.left) case .lookRight: - await vim.look(.right) + vim.look(.right) case .lookUp: - await vim.look(.up) + vim.look(.up) case .lookDown: - await vim.look(.down) + vim.look(.down) case .panLeft: - await vim.pan(.left) + vim.pan(.left) case .panRight: - await vim.pan(.right) + vim.pan(.right) case .panUp: - await vim.pan(.up) + vim.pan(.up) case .panDown: - await vim.pan(.down) + vim.pan(.down) } } } @@ -63,65 +63,46 @@ public extension VimAssistant { switch action { case .hide, .isolate: - guard let db = vim.db else { return [] } + guard let db = vim.db, db.nodes.isNotEmpty else { return [] } let modelContext = ModelContext(db.modelContainer) var ids: Set = .init() - // TODO: This is highly inefficient and needs reworked but predicate disjunction is way effin complicated and frankly stupid. - let predicates = buildPredicates(prediction: prediction) - let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.index)]) + // Fetch all geometry nodes + let nodes = db.nodes + let predicate = Database.Node.predicate(nodes: nodes) + let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.index)]) guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return [] } - for predicate in predicates { - guard let filtered = try? results.filter(predicate) else { continue } - ids.formUnion(filtered.compactMap{ Int($0.index) }) + let categoryNames = prediction.entities.filter{ $0.label == .bimCategory }.map { $0.value } + let familyNames = prediction.entities.filter{ $0.label == .bimFamily }.map { $0.value } + + // Tuple of category names and ids + let categories = results.compactMap{ $0.element?.category?.name }.uniqued().sorted{ $0 < $1 }.map { name in + (name: name, ids: results.filter{ $0.element?.category?.name == name}.compactMap{ Int($0.index) }) } - return ids.sorted() - case .quantify, .zoomIn, .zoomOut, .lookLeft, .lookRight, .lookUp, .lookDown, .panLeft, .panRight, .panUp, .panDown: - return [] - } - } + // Tuple of family names and ids + let familes = results.compactMap{ $0.element?.familyName }.uniqued().sorted{ $0 < $1 }.map { name in + (name: name, ids: results.filter{ $0.element?.familyName == name}.compactMap{ Int($0.index) }) + } - /// Builds a node predicate that matches on category name. - /// - Parameter name: the category name to match - /// - Returns: a predicate that matches case insensitive category name - private func buildCategoryPredicate(_ name: String) -> Predicate { - let predicate = #Predicate { node in - if let element = node.element, let category = element.category { - return category.name.localizedStandardContains(name) - } else { - return false + // Collect the ids of the matching categories + for name in categoryNames { + let found = categories.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +) + ids.formUnion(found) } - } - return predicate - } - /// Builds a node predicate that matches on family name. - /// - Parameter name: the family name to match - /// - Returns: a predicate that matches case insensitive family name - private func buildFamilyPredicate(_ name: String) -> Predicate { - let predicate = #Predicate { node in - if let element = node.element, let familyName = element.familyName { - return familyName.localizedStandardContains(name) - } else { - return false + // Collect the ids of the matching families + for name in familyNames { + let found = familes.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +) + ids.formUnion(found) } + return ids.sorted() + case .quantify, .zoomIn, .zoomOut, .lookLeft, .lookRight, .lookUp, .lookDown, .panLeft, .panRight, .panUp, .panDown: + return [] } - return predicate - } - /// Builds a list of predicates to query on based on the given prediction. - /// - Parameter prediction: the prediction to use - /// - Returns: a list of node predicates - private func buildPredicates(prediction: VimPrediction) -> [Predicate] { - let categories = prediction.entities.filter{ $0.label == .bimCategory }.map { $0.value } - let families = prediction.entities.filter{ $0.label == .bimFamily }.map { $0.value } - let categoryPredicates = categories.map { buildCategoryPredicate($0) } - let familyPredicates = families.map { buildFamilyPredicate($0) } - let predicates = categoryPredicates + familyPredicates - return predicates } } }