From 837c6cd42ea5eca99121e3ba39bd38ef0ea4ed22 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Fri, 1 May 2026 17:48:10 -0700 Subject: [PATCH 1/4] Add two additional sub-attributes for surface integrity --- .../AccessibilityFeatureAttribute.swift | 28 +++++++++++++++++++ .../Config/AccessibilityFeatureKind.swift | 1 + 2 files changed, 29 insertions(+) diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift index f531d0c..ef44329 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift @@ -17,6 +17,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C case runningSlope case crossSlope case surfaceIntegrity + case surfaceDisruption + case heightFromGround /** - NOTE: Experimental attributes @@ -38,6 +40,7 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C public enum ValueType: Sendable, Codable, Equatable { case length case angle + case number case flag case categorical(typeID: String) } @@ -45,6 +48,7 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C public enum Value: Sendable, Codable, Equatable { case length(Measurement) case angle(Measurement) + case number(Double) case flag(Bool) case categorical(AnyCategoricalValue) @@ -54,6 +58,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C return l1 == l2 case (.angle(let a1), .angle(let a2)): return a1 == a2 + case (.number(let n1), .number(let n2)): + return n1 == n2 case (.flag(let f1), .flag(let f2)): return f1 == f2 case (.categorical(let c1), .categorical(let c2)): @@ -93,6 +99,16 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C id: 40, name: "Surface Integrity", unit: nil, valueType: .categorical(typeID: SurfaceIntegrityStatus.typeID), ) + case .surfaceDisruption: + return Metadata( + id: 45, name: "Surface Disruption", unit: nil, + valueType: .number + ) + case .heightFromGround: + return Metadata( + id: 48, name: "Height from Ground", unit: UnitLength.meters, + valueType: .length, + ) case .lidarDepth: return Metadata( id: 50, name: "LiDAR Depth", unit: UnitLength.meters, @@ -175,6 +191,7 @@ public extension AccessibilityFeatureAttribute.Value { switch self { case .length: return .length case .angle: return .angle + case .number: return .number case .flag: return .flag case .categorical(let categoricalValue): return .categorical(typeID: categoricalValue.typeID) } @@ -192,6 +209,7 @@ public extension AccessibilityFeatureAttribute { switch (self.valueType, value) { case (.length, .length), (.angle, .angle), + (.number, .number), (.flag, .flag): return true case (.categorical(let expectedID), .categorical(let cat)): @@ -212,6 +230,8 @@ public extension AccessibilityFeatureAttribute.Value { return measurement.converted(to: .meters).value case .angle(let measurement): return measurement.converted(to: .degrees).value + case .number(let value): + return value case .flag: return nil case .categorical: @@ -234,6 +254,8 @@ public extension AccessibilityFeatureAttribute.Value { return String(format: "%.2f", measurement.converted(to: .meters).value) case .angle(let measurement): return String(format: "%.2f", measurement.converted(to: .degrees).value) + case .number(let value): + return String(format: "%.2f", value) case .flag(let value): return value ? "yes" : "no" case .categorical(let value): @@ -249,6 +271,8 @@ public extension AccessibilityFeatureAttribute { return .length(Measurement(value: double, unit: .meters)) case .angle: return .angle(Measurement(value: double, unit: .degrees)) + case .number: + return .number(double) case .flag: return nil // Flags cannot be represented as doubles case .categorical: @@ -327,6 +351,10 @@ public extension AccessibilityFeatureAttribute { return String(format: "%.2f", measurement.converted(to: .degrees).value) case (.surfaceIntegrity, .categorical(let categoricalValue)): return categoricalValue.rawValue + case (.surfaceDisruption, .number(let value)): + return String(format: "%.2f", value) + case (.heightFromGround, .length(let measurement)): + return String(format: "%.2f", measurement.converted(to: .meters).value) case (.lidarDepth, .length(let measurement)): return String(format: "%.2f", measurement.converted(to: .meters).value) case (.latitudeDelta, .length(let measurement)): diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift index a5c9173..f2ce58a 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift @@ -36,6 +36,7 @@ public enum AccessibilityFeatureKind: String, Identifiable, Codable, CaseIterabl switch self { case .sidewalk: return [ .width, .runningSlope, .crossSlope, .surfaceIntegrity, + .surfaceDisruption, .heightFromGround, .widthLegacy, .runningSlopeLegacy, .crossSlopeLegacy, .widthFromImage, .runningSlopeFromImage, .crossSlopeFromImage ] From 71a29f0805702fb50bf39075e4eddc657e93ecd7 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Fri, 1 May 2026 18:01:34 -0700 Subject: [PATCH 2/4] Add placeholder code for the additional sub-attributes in Attribute Estimation pipeline and Annotation view feature detail view --- .../AttributeEstimationPipeline.swift | 18 +++++++ .../SurfaceIntegrityExtension.swift | 53 +++++++++++++++++++ .../AnnotationFeatureDetailViewBase.swift | 15 ++++++ 3 files changed, 86 insertions(+) diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift index c8c052f..262ecf6 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift @@ -55,6 +55,10 @@ public enum AttributeEstimationPipelineConstants { /** An attribute estimation pipeline that processes editable accessibility features to estimate their attributes. + + - MARK: + The individual attribute calculation functions have a lot of redundant code to get the relevant properties from the prerequisite cache. + Find a way to streamline this. */ public class AttributeEstimationPipeline: ObservableObject { public struct PrerequisiteCache: Sendable { @@ -261,6 +265,20 @@ public class AttributeEstimationPipeline: ObservableObject { try accessibilityFeature.setAttributeValue( surfaceIntegrityAttributeValue, for: .surfaceIntegrity, isCalculated: true ) + case .surfaceDisruption: + let surfaceDisruptionAttributeValue = try self.calculateSurfaceDisruption( + accessibilityFeature: accessibilityFeature + ) + try accessibilityFeature.setAttributeValue( + surfaceDisruptionAttributeValue, for: .surfaceDisruption, isCalculated: true + ) + case .heightFromGround: + let heightFromGroundAttributeValue = try self.calculateHeightFromGround( + accessibilityFeature: accessibilityFeature + ) + try accessibilityFeature.setAttributeValue( + heightFromGroundAttributeValue, for: .heightFromGround, isCalculated: true + ) default: continue } diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift index 0b75af5..7457d84 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift @@ -145,3 +145,56 @@ public extension AttributeEstimationPipeline { return worstStatus } } + +/** + Additional sub-attributes of surface-integrity + */ +/// Sub-attribute: Surface disruption +public extension AttributeEstimationPipeline { + func calculateSurfaceDisruption( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + let isMeshEnabled: Bool = self.captureMeshData != nil + if isMeshEnabled { + return try calculateSurfaceDisruptionFromMesh(accessibilityFeature: accessibilityFeature) + } + return try calculateSurfaceDisruptionFromImage(accessibilityFeature: accessibilityFeature) + } + + func calculateSurfaceDisruptionFromImage( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + return nil + } + + func calculateSurfaceDisruptionFromMesh( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + return nil + } +} + +/// Sub-attribute: Height from ground +extension AttributeEstimationPipeline { + func calculateHeightFromGround( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + let isMeshEnabled: Bool = self.captureMeshData != nil + if isMeshEnabled { + return try calculateHeightFromGroundFromMesh(accessibilityFeature: accessibilityFeature) + } + return try calculateHeightFromGroundFromImage(accessibilityFeature: accessibilityFeature) + } + + func calculateHeightFromGroundFromImage( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + return nil + } + + func calculateHeightFromGroundFromMesh( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + return nil + } +} diff --git a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift index 5131452..292a0ed 100644 --- a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift +++ b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift @@ -168,6 +168,21 @@ public struct AnnotationFeatureDetailViewBase< } } + if (accessibilityFeature.accessibilityFeatureClass.kind.attributes.contains(.surfaceDisruption)) { + Section(header: Text(AccessibilityFeatureAttribute.surfaceDisruption.displayName)) { + pickerView(attribute: .surfaceDisruption) + .focused($focusedField, equals: .surfaceDisruption) + .id(refreshTrigger) // Refresh the Picker view when refreshTrigger changes + } + } + + if (accessibilityFeature.accessibilityFeatureClass.kind.attributes.contains(.heightFromGround)) { + Section(header: Text(AccessibilityFeatureAttribute.heightFromGround.displayName)) { + numberTextFieldView(attribute: .heightFromGround) + .focused($focusedField, equals: .heightFromGround) + } + } + /// Experimental Attributes Section if (accessibilityFeature.accessibilityFeatureClass.kind.experimentalAttributes.contains(.lidarDepth)) { Section(header: Text(AccessibilityFeatureAttribute.lidarDepth.displayName)) { From d5b47d6252444cc03508bed7418711bf9cad7058 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Fri, 1 May 2026 18:36:56 -0700 Subject: [PATCH 3/4] Add first version of surface integrity sub-attribute implementations --- .../AttributeEstimationPipeline.swift | 4 + .../SurfaceIntegrityExtension.swift | 107 +++++++++++++++++- .../SurfaceIntegrityFromImageExtension.swift | 23 +++- .../SurfaceIntegrityFromMeshExtension.swift | 18 ++- 4 files changed, 141 insertions(+), 11 deletions(-) diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift index 262ecf6..6b775f7 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift @@ -71,6 +71,10 @@ public class AttributeEstimationPipeline: ObservableObject { public var meshTriangles: [MeshTriangle]? = nil public var meshAlignedPlane: Plane? = nil public var meshProjectedPlane: ProjectedPlane? = nil + + /// Additional lazy properties for caching can be added here as needed. + public var surfaceNormalsGrid: SurfaceNormalsForPointsGrid? = nil + } public var captureImageData: (any CaptureImageDataProtocol)? diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift index 7457d84..9d04773 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreLocation import PointNMapShaderTypes +import simd public extension AttributeEstimationPipeline { func calculateSurfaceIntegrity( @@ -43,9 +44,10 @@ public extension AttributeEstimationPipeline { let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane( accessibilityFeature: accessibilityFeature, plane: alignedPlane ) - let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( + let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane ) + self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid let surfaceIntegrityResults = try surfaceIntegrityProcessor.getIntegrityResultsFromImage( worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid, damageDetectionResults: damageDetectionResults, captureData: captureImageData @@ -164,13 +166,70 @@ public extension AttributeEstimationPipeline { func calculateSurfaceDisruptionFromImage( accessibilityFeature: any EditableAccessibilityFeatureProtocol ) throws -> AccessibilityFeatureAttribute.Value { - return nil + guard let captureImageData = self.captureImageData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + guard let surfaceNormalsProcessor = self.surfaceNormalsProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature) + let worldPointsGrid = try self.prerequisiteCache.worldPointsGrid ?? self.getWorldPointsGrid(accessibilityFeature: accessibilityFeature) + let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints( + accessibilityFeature: accessibilityFeature + ) + let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, worldPoints: worldPoints + ) + let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane( + accessibilityFeature: accessibilityFeature, plane: alignedPlane + ) + let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( + worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane + ) + self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid + let (deviantPoints, validPoints) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromImage( + worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid, + damageDetectionResults: damageDetectionResults, captureData: captureImageData + ) + let deviantPointProportion = validPoints > 0 ? deviantPoints / validPoints : 0 + + guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPointProportion) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return surfaceDisruptionAttributeValue } func calculateSurfaceDisruptionFromMesh( accessibilityFeature: any EditableAccessibilityFeatureProtocol ) throws -> AccessibilityFeatureAttribute.Value { - return nil + guard let captureMeshData = self.captureMeshData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature) + let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents( + accessibilityFeature: accessibilityFeature + ) + let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons + let meshTriangles: [MeshTriangle] = self.prerequisiteCache.meshTriangles ?? meshContents.triangles + let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons + ) + let (totalDeviantPolygons, totalValidPolygons) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromMesh( + meshTriangles: meshTriangles, plane: alignedPlane, + damageDetectionResults: damageDetectionResults, captureData: captureMeshData + ) + let deviantPolygonProportion = totalValidPolygons > 0 ? totalDeviantPolygons / totalValidPolygons : 0 + + guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPolygonProportion) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return surfaceDisruptionAttributeValue } } @@ -189,12 +248,50 @@ extension AttributeEstimationPipeline { func calculateHeightFromGroundFromImage( accessibilityFeature: any EditableAccessibilityFeatureProtocol ) throws -> AccessibilityFeatureAttribute.Value { - return nil + guard let captureImageData = self.captureImageData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints( + accessibilityFeature: accessibilityFeature + ) + let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, worldPoints: worldPoints + ) + let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureImageData.cameraTransform) + + guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return heightFromGroundAttributeValue } func calculateHeightFromGroundFromMesh( accessibilityFeature: any EditableAccessibilityFeatureProtocol ) throws -> AccessibilityFeatureAttribute.Value { - return nil + guard let captureMeshData = self.captureMeshData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents( + accessibilityFeature: accessibilityFeature + ) + let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons + let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons + ) + let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureMeshData.cameraTransform) + + guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return heightFromGroundAttributeValue + } + + private func getHeightFromGround(plane: Plane, cameraTransform: simd_float4x4) -> Double { + let planeNormal = plane.normalVector + let planeOrigin = plane.origin + let userPosition4 = cameraTransform.columns.3 + let userPosition = SIMD3(userPosition4.x, userPosition4.y, userPosition4.z) + let vectorFromPlaneToUser = userPosition - planeOrigin + return Double(simd_dot(vectorFromPlaneToUser, planeNormal)) } } diff --git a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift index 33a4d07..2f1181c 100644 --- a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift +++ b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift @@ -15,7 +15,7 @@ public extension SurfaceIntegrityProcessor { /** This function assesses the integrity of the surface based on the angular deviation of surface normals from the plane normal using GPU acceleration. It calculates the proportion of points that deviate beyond a specified angular threshold and determines the integrity status based on whether this proportion exceeds a defined threshold. */ - func getSurfaceNormalIntegrityResultFromImage( + func getSurfaceNormalIntegrityValueFromImage( worldPointsGrid: WorldPointsGrid, plane: Plane, surfaceNormalsForPointsGrid: SurfaceNormalsForPointsGrid, @@ -23,7 +23,7 @@ public extension SurfaceIntegrityProcessor { captureData: (any CaptureImageDataProtocol), angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imagePlaneAngularDeviationThreshold, deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imageDeviantPointProportionThreshold - ) throws -> IntegrityStatusDetails { + ) throws -> (totalDeviantPoints: Double, totalValidPoints: Double) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { throw SurfaceIntegrityProcessorError.metalPipelineCreationError } @@ -90,10 +90,25 @@ public extension SurfaceIntegrityProcessor { let totalValidPoints = totalValidBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee let totalDeviantPoints = totalDeviantBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee - let deviantPointProportion = totalValidPoints > 0 ? Float(totalDeviantPoints) / Float(totalValidPoints) : 0 + return (Double(totalDeviantPoints), Double(totalValidPoints)) + } + + func getSurfaceNormalIntegrityResultFromImage( + worldPointsGrid: WorldPointsGrid, + plane: Plane, + surfaceNormalsForPointsGrid: SurfaceNormalsForPointsGrid, + damageDetectionResults: [DamageDetectionResult], + captureData: (any CaptureImageDataProtocol), + angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imagePlaneAngularDeviationThreshold, + deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imageDeviantPointProportionThreshold + ) throws -> IntegrityStatusDetails { + let (deviantPoints, validPoints) = try getSurfaceNormalIntegrityValueFromImage( + worldPointsGrid: worldPointsGrid, plane: plane, surfaceNormalsForPointsGrid: surfaceNormalsForPointsGrid, damageDetectionResults: damageDetectionResults, captureData: captureData, angularDeviationThreshold: angularDeviationThreshold, deviantPointProportionThreshold: deviantPointProportionThreshold + ) + let deviantPointProportion = validPoints > 0 ? Float(deviantPoints) / Float(validPoints) : 0 let statusDetails: IntegrityStatusDetails = IntegrityStatusDetails( status: deviantPointProportion > deviantPointProportionThreshold ? .slight : .intact, - details: "Deviant Point Proportion: \(deviantPointProportion * 100)%, Total Deviant Points: \(totalDeviantPoints), Total Points: \(totalValidPoints)" + details: "Deviant Point Proportion: \(deviantPointProportion * 100)%, Total Deviant Points: \(deviantPoints), Total Points: \(validPoints)" ) return statusDetails } diff --git a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift index b889745..8eabd8f 100644 --- a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift +++ b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift @@ -12,14 +12,14 @@ import simd import PointNMapShaderTypes public extension SurfaceIntegrityProcessor { - func getSurfaceNormalIntegrityResultFromMesh( + func getSurfaceNormalIntegrityValueFromMesh( meshTriangles: [MeshTriangle], plane: Plane, damageDetectionResults: [DamageDetectionResult], captureData: (any CaptureMeshDataProtocol), angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshPlaneAngularDeviationThreshold, deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshDeviantPolygonProportionThreshold - ) throws -> IntegrityStatusDetails { + ) throws -> (totalDeviantPoints: Double, totalValidPoints: Double) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { throw SurfaceIntegrityProcessorError.metalPipelineCreationError } @@ -79,6 +79,20 @@ public extension SurfaceIntegrityProcessor { let totalValidPolygons = totalValidBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee let totalDeviantPolygons = totalDeviantBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee + return (Double(totalDeviantPolygons), Double(totalValidPolygons)) + } + + func getSurfaceNormalIntegrityResultFromMesh( + meshTriangles: [MeshTriangle], + plane: Plane, + damageDetectionResults: [DamageDetectionResult], + captureData: (any CaptureMeshDataProtocol), + angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshPlaneAngularDeviationThreshold, + deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshDeviantPolygonProportionThreshold + ) throws -> IntegrityStatusDetails { + let (totalDeviantPolygons, totalValidPolygons) = try getSurfaceNormalIntegrityValueFromMesh( + meshTriangles: meshTriangles, plane: plane, damageDetectionResults: damageDetectionResults, captureData: captureData, angularDeviationThreshold: angularDeviationThreshold, deviantPointProportionThreshold: deviantPointProportionThreshold) + let deviantPolygonProportion = totalValidPolygons > 0 ? Float(totalDeviantPolygons) / Float(totalValidPolygons) : 0 let statusDetails: IntegrityStatusDetails = IntegrityStatusDetails( status: deviantPolygonProportion > deviantPointProportionThreshold ? .slight : .intact, From b9adde91fae54ed888d47e2637f40d4bdb835acd Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Fri, 1 May 2026 18:41:30 -0700 Subject: [PATCH 4/4] Fix detail view for surface disruption value --- .../View/SubView/AnnotationFeatureDetailViewBase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift index 292a0ed..feaabe6 100644 --- a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift +++ b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift @@ -170,7 +170,7 @@ public struct AnnotationFeatureDetailViewBase< if (accessibilityFeature.accessibilityFeatureClass.kind.attributes.contains(.surfaceDisruption)) { Section(header: Text(AccessibilityFeatureAttribute.surfaceDisruption.displayName)) { - pickerView(attribute: .surfaceDisruption) + numberTextFieldView(attribute: .surfaceDisruption) .focused($focusedField, equals: .surfaceDisruption) .id(refreshTrigger) // Refresh the Picker view when refreshTrigger changes }