From a00ab811125ac7c957a59b01f8ffa686d96ab9ce Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Tue, 23 Jun 2026 18:45:01 -0700 Subject: [PATCH 1/5] Start adding helper functions in SetupView to enable selection of attributes for each feature class --- IOSAccessAssessment/View/SetupView.swift | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/IOSAccessAssessment/View/SetupView.swift b/IOSAccessAssessment/View/SetupView.swift index cee2e43..cb8dd62 100644 --- a/IOSAccessAssessment/View/SetupView.swift +++ b/IOSAccessAssessment/View/SetupView.swift @@ -218,6 +218,7 @@ class CurrentDatasetStatusViewModel: ObservableObject { struct SetupView: View { @State private var selectedClasses = Set() + @State private var selectedAttributesByClass = [AccessibilityFeatureClass: Set]() private var isSelectionEmpty: Bool { return (self.selectedClasses.count == 0) } @@ -454,6 +455,52 @@ struct SetupView: View { } } + private func toggleClass(_ accessibilityFeatureClass: AccessibilityFeatureClass) { + if self.selectedClasses.contains(accessibilityFeatureClass) { + self.selectedClasses.remove(accessibilityFeatureClass) + + /// Clear the selected attributes for the class when it is deselected + self.selectedAttributesByClass[accessibilityFeatureClass] = nil + } else { + self.selectedClasses.insert(accessibilityFeatureClass) + + /// Add all the attributes for the class when it is selected + self.selectedAttributesByClass[accessibilityFeatureClass] = Set(accessibilityFeatureClass.kind.attributes) + } + } + + private func toggleAttribute( + _ attribute: AccessibilityFeatureAttribute, for accessibilityFeatureClass: AccessibilityFeatureClass + ) { + var selectedAttributes = selectedAttributesByClass[accessibilityFeatureClass, default: []] + + if selectedAttributes.contains(attribute) { + selectedAttributes.remove(attribute) + } else { + selectedAttributes.insert(attribute) + } + + selectedAttributesByClass[accessibilityFeatureClass] = selectedAttributes + } + + private func attributeSelectionSummary( + for accessibilityFeatureClass: AccessibilityFeatureClass + ) -> String { + let attributes = accessibilityFeatureClass.kind.attributes + guard !attributes.isEmpty else { + return "" + } + let selectedCount = selectedAttributesByClass[accessibilityFeatureClass, default: []].count + + if selectedCount == attributes.count { + return "All" + } else if selectedCount == 0 { + return "None" + } else { + return "\(selectedCount)/\(attributes.count)" + } + } + private func openChangeset() { Task { do { From 2ce8903d81a7466b09e8ac6352f207ecf352a983 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Tue, 23 Jun 2026 19:16:44 -0700 Subject: [PATCH 2/5] Create initial menu-based attribute selection --- IOSAccessAssessment/View/SetupView.swift | 112 ++++++++++++++++------- 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/IOSAccessAssessment/View/SetupView.swift b/IOSAccessAssessment/View/SetupView.swift index cb8dd62..0d545f7 100644 --- a/IOSAccessAssessment/View/SetupView.swift +++ b/IOSAccessAssessment/View/SetupView.swift @@ -71,9 +71,16 @@ enum SetupViewConstants { static let profileIcon = "person.crop.circle" static let logoutIcon = "rectangle.portrait.and.arrow.right" static let uploadIcon = "arrow.up" + + /// Class Selection static let classSelectionColorHintIcon = "circle.fill" static let classSelectionColorHintBorderIcon = "circle" + /// Attribute Selection + static let attributeSelectedStatusIcon = "checkmark.circle.fill" + static let attributeUnselectedStatusIcon = "circle" + static let attributeSelectionDropdownIcon = "chevron.down.circle" + /// InfoTip static let infoIcon = "info.circle" } @@ -319,37 +326,7 @@ struct SetupView: View { List { ForEach(SharedAppConstants.SelectedAccessibilityFeatureConfig.classes, id: \.self) { accessibilityFeatureClass in - Button(action: { - if self.selectedClasses.contains(accessibilityFeatureClass) { - self.selectedClasses.remove(accessibilityFeatureClass) - } else { - self.selectedClasses.insert(accessibilityFeatureClass) - } - }) { - HStack { - Text(accessibilityFeatureClass.name) - .foregroundStyle( - self.selectedClasses.contains(accessibilityFeatureClass) - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - Spacer() - Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) - .overlay( - Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle( - self.selectedClasses.contains(accessibilityFeatureClass) - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - ) - } - } + listElementView(for: accessibilityFeatureClass) } } } @@ -446,6 +423,72 @@ struct SetupView: View { .environmentObject(self.segmentationPipeline) } + @ViewBuilder + private func listElementView(for accessibilityFeatureClass: AccessibilityFeatureClass) -> some View { + HStack { + let isClassSelected = self.selectedClasses.contains(accessibilityFeatureClass) + let attributes = Array(accessibilityFeatureClass.kind.attributes) + + HStack { + Button(action: { + toggleClass(accessibilityFeatureClass) + }) { + HStack { + Text(accessibilityFeatureClass.name) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + Spacer() + + Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) + .overlay( + Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + ) + } + } + .buttonStyle(.plain) + + if !attributes.isEmpty { + Menu { + ForEach(attributes) { attribute in + Button(action: { + toggleAttribute(attribute, for: accessibilityFeatureClass) + }) { + Label( + attribute.name, + systemImage: isAttributeSelected(attribute, for: accessibilityFeatureClass) + ? SetupViewConstants.Images.attributeSelectedStatusIcon + : SetupViewConstants.Images.attributeUnselectedStatusIcon + ) + } + } + } label: { + HStack(spacing: 4) { + Text(attributeSelectionSummary(for: accessibilityFeatureClass)) + .font(.caption) + .foregroundStyle(.secondary) + + Image(systemName: SetupViewConstants.Images.attributeSelectionDropdownIcon) + } + } + .disabled(!isClassSelected) + } + } + } + } + @ViewBuilder private var mappingDestination: some View { if userStateViewModel.appMode == .standard { @@ -483,6 +526,13 @@ struct SetupView: View { selectedAttributesByClass[accessibilityFeatureClass] = selectedAttributes } + private func isAttributeSelected( + _ attribute: AccessibilityFeatureAttribute, + for accessibilityFeatureClass: AccessibilityFeatureClass + ) -> Bool { + selectedAttributesByClass[accessibilityFeatureClass, default: []].contains(attribute) + } + private func attributeSelectionSummary( for accessibilityFeatureClass: AccessibilityFeatureClass ) -> String { From 473141db2108152c5f5c4cc78ea3fc2dd68a05a2 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Tue, 23 Jun 2026 19:48:30 -0700 Subject: [PATCH 3/5] Complete second version of attribute selection based on custom accordian-like component --- IOSAccessAssessment/View/SetupView.swift | 138 ++++++++++++++--------- 1 file changed, 87 insertions(+), 51 deletions(-) diff --git a/IOSAccessAssessment/View/SetupView.swift b/IOSAccessAssessment/View/SetupView.swift index 0d545f7..f230ae0 100644 --- a/IOSAccessAssessment/View/SetupView.swift +++ b/IOSAccessAssessment/View/SetupView.swift @@ -79,7 +79,8 @@ enum SetupViewConstants { /// Attribute Selection static let attributeSelectedStatusIcon = "checkmark.circle.fill" static let attributeUnselectedStatusIcon = "circle" - static let attributeSelectionDropdownIcon = "chevron.down.circle" + static let attributeSectionExpandedIcon = "chevron.up.circle" + static let attributeSectionCollapsedIcon = "chevron.down.circle" /// InfoTip static let infoIcon = "info.circle" @@ -229,6 +230,7 @@ struct SetupView: View { private var isSelectionEmpty: Bool { return (self.selectedClasses.count == 0) } + @State private var expandedAttributeSections: Set = [] @EnvironmentObject var workspaceViewModel: WorkspaceViewModel @EnvironmentObject var userStateViewModel: UserStateViewModel @@ -427,65 +429,82 @@ struct SetupView: View { private func listElementView(for accessibilityFeatureClass: AccessibilityFeatureClass) -> some View { HStack { let isClassSelected = self.selectedClasses.contains(accessibilityFeatureClass) - let attributes = Array(accessibilityFeatureClass.kind.attributes) + let attributes = Array(accessibilityFeatureClass.kind.attributes).sorted(by: { $0.name < $1.name }) + let hasAttributes = !attributes.isEmpty + let isExpanded = isAttributeSectionExpanded(for: accessibilityFeatureClass) - HStack { - Button(action: { - toggleClass(accessibilityFeatureClass) - }) { - HStack { - Text(accessibilityFeatureClass.name) - .foregroundStyle( - isClassSelected - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - Spacer() - - Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) - .overlay( - Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle( - isClassSelected - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - ) + VStack(alignment: .leading, spacing: 8) { + HStack { + Button(action: { + toggleClass(accessibilityFeatureClass) + }) { + HStack { + Text(accessibilityFeatureClass.name) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + Spacer() + + Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) + .overlay( + Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + ) + } + } + .buttonStyle(.plain) + + if hasAttributes && isClassSelected { + Button(action: { + toggleAttributeSectionExpansion(for: accessibilityFeatureClass) + }) { + HStack(spacing: 4) { + Text(attributeSelectionSummary(for: accessibilityFeatureClass)) + .font(.caption) + .foregroundStyle(.secondary) + + Image(systemName: isExpanded ? SetupViewConstants.Images.attributeSectionExpandedIcon : SetupViewConstants.Images.attributeSectionCollapsedIcon) + .imageScale(.medium) + } + } } } - .buttonStyle(.plain) - if !attributes.isEmpty { - Menu { - ForEach(attributes) { attribute in + if hasAttributes && isExpanded && isClassSelected { + VStack(alignment: .leading, spacing: 4) { + ForEach(attributes, id: \.self) { attribute in Button(action: { toggleAttribute(attribute, for: accessibilityFeatureClass) }) { - Label( - attribute.name, - systemImage: isAttributeSelected(attribute, for: accessibilityFeatureClass) - ? SetupViewConstants.Images.attributeSelectedStatusIcon - : SetupViewConstants.Images.attributeUnselectedStatusIcon - ) + HStack { + Text(attribute.name) + .font(.subheadline) + Spacer() + Image(systemName: isAttributeSelected(attribute, for: accessibilityFeatureClass) ? SetupViewConstants.Images.attributeSelectedStatusIcon : SetupViewConstants.Images.attributeUnselectedStatusIcon + ) + } + .contentShape(Rectangle()) } - } - } label: { - HStack(spacing: 4) { - Text(attributeSelectionSummary(for: accessibilityFeatureClass)) - .font(.caption) - .foregroundStyle(.secondary) - - Image(systemName: SetupViewConstants.Images.attributeSelectionDropdownIcon) + .buttonStyle(.plain) } } - .disabled(!isClassSelected) + .padding(.leading, 12) + .padding(.top, 4) +// .transition(.opacity.combined(with: .move(edge: .top))) } } + .padding(.vertical, 4) } } @@ -503,12 +522,15 @@ struct SetupView: View { self.selectedClasses.remove(accessibilityFeatureClass) /// Clear the selected attributes for the class when it is deselected - self.selectedAttributesByClass[accessibilityFeatureClass] = nil +// self.selectedAttributesByClass[accessibilityFeatureClass] = nil + self.expandedAttributeSections.remove(accessibilityFeatureClass) } else { self.selectedClasses.insert(accessibilityFeatureClass) - /// Add all the attributes for the class when it is selected - self.selectedAttributesByClass[accessibilityFeatureClass] = Set(accessibilityFeatureClass.kind.attributes) + if !self.selectedAttributesByClass.contains(where: { $0.key == accessibilityFeatureClass }) { + /// Add all the attributes for the class when it is selected + self.selectedAttributesByClass[accessibilityFeatureClass] = Set(accessibilityFeatureClass.kind.attributes) + } } } @@ -533,6 +555,20 @@ struct SetupView: View { selectedAttributesByClass[accessibilityFeatureClass, default: []].contains(attribute) } + private func toggleAttributeSectionExpansion(for accessibilityFeatureClass: AccessibilityFeatureClass) { +// withAnimation { + if expandedAttributeSections.contains(accessibilityFeatureClass) { + expandedAttributeSections.remove(accessibilityFeatureClass) + } else { + expandedAttributeSections.insert(accessibilityFeatureClass) + } +// } + } + + private func isAttributeSectionExpanded(for accessibilityFeatureClass: AccessibilityFeatureClass) -> Bool { + expandedAttributeSections.contains(accessibilityFeatureClass) + } + private func attributeSelectionSummary( for accessibilityFeatureClass: AccessibilityFeatureClass ) -> String { From c2fb59df7f542733ce30a430063a7709f9593387 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Thu, 25 Jun 2026 23:25:49 -0700 Subject: [PATCH 4/5] Accomodation of the selectedAttributesByClass dictionary and addition of fine-grained caching --- IOSAccessAssessment/View/ARCameraView.swift | 4 +++- IOSAccessAssessment/View/AnnotationView.swift | 1 + IOSAccessAssessment/View/SetupView.swift | 10 ++++++++-- .../View/TestMode/TestCameraView.swift | 4 +++- .../View/TestMode/TestListView.swift | 16 +++++++++++++--- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index ea592ad..af5ed3a 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -142,6 +142,7 @@ class MappingDataStatusViewModel: ObservableObject { struct ARCameraView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @EnvironmentObject var sharedAppData: SharedAppData @EnvironmentObject var sharedAppContext: SharedAppContext @@ -298,7 +299,8 @@ struct ARCameraView: View { .fullScreenCover(isPresented: $showAnnotationView) { if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( - selectedClasses: selectedClasses, captureLocation: captureLocation, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController ) } else { diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index 46e4db3..d3a7335 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -225,6 +225,7 @@ class APIChangesetUploadStatusViewModel: ObservableObject { struct AnnotationView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] let captureLocation: CLLocationCoordinate2D let apiChangesetUploadController: APIChangesetUploadController diff --git a/IOSAccessAssessment/View/SetupView.swift b/IOSAccessAssessment/View/SetupView.swift index f230ae0..31ff02d 100644 --- a/IOSAccessAssessment/View/SetupView.swift +++ b/IOSAccessAssessment/View/SetupView.swift @@ -511,9 +511,15 @@ struct SetupView: View { @ViewBuilder private var mappingDestination: some View { if userStateViewModel.appMode == .standard { - ARCameraView(selectedClasses: Array(self.selectedClasses).sorted()) + ARCameraView( + selectedClasses: Array(self.selectedClasses).sorted(), + selectedAttributesByClass: self.selectedAttributesByClass + ) } else { - TestEnvironmentListView(selectedClasses: Array(self.selectedClasses).sorted()) + TestEnvironmentListView( + selectedClasses: Array(self.selectedClasses).sorted(), + selectedAttributesByClass: self.selectedAttributesByClass + ) } } diff --git a/IOSAccessAssessment/View/TestMode/TestCameraView.swift b/IOSAccessAssessment/View/TestMode/TestCameraView.swift index 1e94565..1f12748 100644 --- a/IOSAccessAssessment/View/TestMode/TestCameraView.swift +++ b/IOSAccessAssessment/View/TestMode/TestCameraView.swift @@ -103,6 +103,7 @@ class LocationManagerPlaceholder: NSObject, ObservableObject { */ struct TestCameraView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] let selectedEnvironment: APIEnvironment let workspaceId: String let changesetId: String @@ -268,7 +269,8 @@ struct TestCameraView: View { .fullScreenCover(isPresented: $showAnnotationView) { if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( - selectedClasses: selectedClasses, captureLocation: captureLocation, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController ) } else { diff --git a/IOSAccessAssessment/View/TestMode/TestListView.swift b/IOSAccessAssessment/View/TestMode/TestListView.swift index a439310..d380ac8 100644 --- a/IOSAccessAssessment/View/TestMode/TestListView.swift +++ b/IOSAccessAssessment/View/TestMode/TestListView.swift @@ -25,6 +25,7 @@ enum TestListViewError: Error, LocalizedError { struct TestEnvironmentListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @Environment(\.dismiss) var dismiss @@ -65,7 +66,10 @@ struct TestEnvironmentListView: View { } } .navigationDestination(item: $selectedEnvironment) { environmentDir in - TestWorkspaceListView(selectedClasses: selectedClasses, datasetLister: datasetLister) + TestWorkspaceListView( + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + datasetLister: datasetLister + ) } } @@ -84,6 +88,7 @@ struct TestEnvironmentListView: View { */ struct TestWorkspaceListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @ObservedObject var datasetLister: DatasetLister @Environment(\.dismiss) var dismiss @@ -118,7 +123,10 @@ struct TestWorkspaceListView: View { } .navigationBarTitle("Test: Workspace Selection", displayMode: .inline) .navigationDestination(item: $selectedWorkspace) { workspaceDir in - TestChangesetListView(selectedClasses: selectedClasses, datasetLister: datasetLister) + TestChangesetListView( + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + datasetLister: datasetLister + ) } } @@ -137,6 +145,7 @@ struct TestWorkspaceListView: View { */ struct TestChangesetListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @ObservedObject var datasetLister: DatasetLister @Environment(\.dismiss) var dismiss @@ -184,7 +193,8 @@ struct TestChangesetListView: View { if let selectedEnvironment = datasetLister.selectedEnvironment, let selectedWorkspace = datasetLister.selectedWorkspace { TestCameraView( - selectedClasses: selectedClasses, selectedEnvironment: selectedEnvironment.apiEnvironment, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + selectedEnvironment: selectedEnvironment.apiEnvironment, workspaceId: selectedWorkspace.workspaceId, changesetId: changesetDir.changesetId ) } else { From 93acb0e9f52492738e9ee2a0614cc24e4d6d785e Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sat, 27 Jun 2026 19:10:54 -0700 Subject: [PATCH 5/5] Accomodate the caching updates in Attribute estimation pipeline --- IOSAccessAssessment/View/AnnotationView.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index d3a7335..abb8e5b 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -522,7 +522,7 @@ struct AnnotationView: View { var lastEstimationError: Error? = nil accessibilityFeatures.forEach { accessibilityFeature in do { - try attributeEstimationPipeline.setPrerequisites(accessibilityFeature: accessibilityFeature) +// try attributeEstimationPipeline.setPrerequisites(accessibilityFeature: accessibilityFeature) try attributeEstimationPipeline.processLocationRequest( deviceLocation: captureLocation, accessibilityFeature: accessibilityFeature @@ -532,7 +532,8 @@ struct AnnotationView: View { mappingData: sharedAppData.currentMappingData, accessibilityFeature: accessibilityFeature ) try attributeEstimationPipeline.processAttributeRequest( - accessibilityFeature: accessibilityFeature + accessibilityFeature: accessibilityFeature, + attributes: selectedAttributesByClass[currentClass] ?? [] ) attributeEstimationPipeline.clearPrerequisites() } catch { @@ -576,11 +577,13 @@ struct AnnotationView: View { featureSelectedStatus[oldFeature.id] = false /// Selected, but not highlighted } /// MARK: Temporary code for visualization. Incurs significant performance overhead. + /// TODO: Check what happens when we use the cache-based methods instead if currentClass.kind.attributes.contains(where: { $0 == .width || $0 == .runningSlope || $0 == .crossSlope || $0 == .surfaceIntegrity }) { + let worldPoints = try attributeEstimationPipeline.getWorldPoints(accessibilityFeature: currentFeature) let plane = try attributeEstimationPipeline.calculateAlignedPlane( - accessibilityFeature: currentFeature, worldPoints: nil + accessibilityFeature: currentFeature, worldPoints: worldPoints ) let projectedPlane = try attributeEstimationPipeline.calculateProjectedPlane( accessibilityFeature: currentFeature, plane: plane