diff --git a/RoomPlanSimple/ImprovedFloorPlanGenerator.swift b/RoomPlanSimple/ImprovedFloorPlanGenerator.swift new file mode 100644 index 0000000..585c2de --- /dev/null +++ b/RoomPlanSimple/ImprovedFloorPlanGenerator.swift @@ -0,0 +1,807 @@ +import UIKit +import RoomPlan +import simd +import CoreGraphics + +// MARK: - Improved Floor Plan Generator +/// Advanced floor plan generator with RF propagation visualization +class ImprovedFloorPlanGenerator { + + // MARK: - Enums + enum RenderStyle { + case blueprint + case architectural + case modern + case minimal + case detailed + } + + enum LayerType { + case walls + case doors + case windows + case furniture + case dimensions + case labels + case grid + case rfPropagation + case networkDevices + } + + // MARK: - Properties + private var capturedRoom: CapturedRoom? + private var rooms: [RoomAnalyzer.IdentifiedRoom] = [] + private var renderStyle: RenderStyle = .modern + private var enabledLayers: Set = [.walls, .doors, .windows, .labels, .rfPropagation] + private var scale: CGFloat = 50.0 // pixels per meter + private var propagationModel: RFPropagationModel? + private var heatmapGenerator: RFHeatmapGenerator? + + // Style configuration + private var wallThickness: CGFloat = 4.0 + private var doorWidth: CGFloat = 0.9 // meters + private var windowWidth: CGFloat = 1.2 // meters + + // Colors for different styles + private var styleColors: [RenderStyle: StyleColorPalette] = [ + .blueprint: StyleColorPalette( + background: UIColor(red: 0.0, green: 0.1, blue: 0.3, alpha: 1.0), + walls: UIColor.white, + doors: UIColor(red: 0.8, green: 0.8, blue: 1.0, alpha: 1.0), + windows: UIColor(red: 0.6, green: 0.8, blue: 1.0, alpha: 1.0), + furniture: UIColor(red: 0.7, green: 0.7, blue: 0.9, alpha: 1.0), + text: UIColor.white, + grid: UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 0.3) + ), + .architectural: StyleColorPalette( + background: UIColor.white, + walls: UIColor.black, + doors: UIColor.darkGray, + windows: UIColor.gray, + furniture: UIColor.lightGray, + text: UIColor.black, + grid: UIColor(white: 0.9, alpha: 1.0) + ), + .modern: StyleColorPalette( + background: UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1.0), + walls: UIColor(red: 0.2, green: 0.2, blue: 0.25, alpha: 1.0), + doors: UIColor(red: 0.6, green: 0.4, blue: 0.2, alpha: 1.0), + windows: UIColor(red: 0.5, green: 0.7, blue: 0.9, alpha: 1.0), + furniture: UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1.0), + text: UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1.0), + grid: UIColor(white: 0.85, alpha: 0.5) + ), + .minimal: StyleColorPalette( + background: UIColor.white, + walls: UIColor(white: 0.2, alpha: 1.0), + doors: UIColor(white: 0.5, alpha: 1.0), + windows: UIColor(white: 0.6, alpha: 1.0), + furniture: UIColor(white: 0.8, alpha: 1.0), + text: UIColor(white: 0.3, alpha: 1.0), + grid: UIColor.clear + ), + .detailed: StyleColorPalette( + background: UIColor(red: 0.95, green: 0.95, blue: 0.92, alpha: 1.0), + walls: UIColor(red: 0.3, green: 0.25, blue: 0.2, alpha: 1.0), + doors: UIColor(red: 0.55, green: 0.35, blue: 0.15, alpha: 1.0), + windows: UIColor(red: 0.4, green: 0.6, blue: 0.8, alpha: 0.8), + furniture: UIColor(red: 0.6, green: 0.5, blue: 0.4, alpha: 1.0), + text: UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0), + grid: UIColor(red: 0.8, green: 0.8, blue: 0.75, alpha: 0.3) + ) + ] + + struct StyleColorPalette { + let background: UIColor + let walls: UIColor + let doors: UIColor + let windows: UIColor + let furniture: UIColor + let text: UIColor + let grid: UIColor + } + + // MARK: - Initialization + init() { + print("🏗 Improved Floor Plan Generator initialized") + } + + // MARK: - Configuration + func configure(with capturedRoom: CapturedRoom?, rooms: [RoomAnalyzer.IdentifiedRoom]) { + self.capturedRoom = capturedRoom + self.rooms = rooms + print("📐 Configured with \(rooms.count) rooms") + } + + func setRenderStyle(_ style: RenderStyle) { + self.renderStyle = style + } + + func setEnabledLayers(_ layers: Set) { + self.enabledLayers = layers + } + + func setScale(_ scale: CGFloat) { + self.scale = max(10, min(200, scale)) + } + + func setPropagationModel(_ model: RFPropagationModel) { + self.propagationModel = model + self.heatmapGenerator = RFHeatmapGenerator(propagationModel: model) + } + + // MARK: - Floor Plan Generation + + /// Generate complete floor plan image + func generateFloorPlan(size: CGSize) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, true, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return nil + } + + // Get style colors + let colors = styleColors[renderStyle] ?? styleColors[.modern]! + + // Draw background + drawBackground(context: context, size: size, color: colors.background) + + // Draw grid if enabled + if enabledLayers.contains(.grid) { + drawGrid(context: context, size: size, color: colors.grid) + } + + // Calculate transform for centering and scaling + let transform = calculateTransform(for: size) + context.concatenate(transform) + + // Draw RF propagation heatmap if enabled (underneath floor plan) + if enabledLayers.contains(.rfPropagation), let heatmapGen = heatmapGenerator { + drawRFPropagation(context: context, generator: heatmapGen, transform: transform, size: size) + } + + // Draw floor plan layers + if enabledLayers.contains(.walls) { + drawWalls(context: context, color: colors.walls) + } + + if enabledLayers.contains(.doors) { + drawDoors(context: context, color: colors.doors) + } + + if enabledLayers.contains(.windows) { + drawWindows(context: context, color: colors.windows) + } + + if enabledLayers.contains(.furniture) { + drawFurniture(context: context, color: colors.furniture) + } + + if enabledLayers.contains(.networkDevices) { + drawNetworkDevices(context: context) + } + + // Reset transform for UI elements + context.concatenate(transform.inverted()) + + if enabledLayers.contains(.labels) { + drawRoomLabels(context: context, transform: transform, color: colors.text) + } + + if enabledLayers.contains(.dimensions) { + drawDimensions(context: context, transform: transform, color: colors.text) + } + + // Draw legend + drawLegend(context: context, size: size, colors: colors) + + // Draw scale + drawScale(context: context, size: size, color: colors.text) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image + } + + // MARK: - Drawing Methods + + private func drawBackground(context: CGContext, size: CGSize, color: UIColor) { + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + } + + private func drawGrid(context: CGContext, size: CGSize, color: UIColor) { + context.setStrokeColor(color.cgColor) + context.setLineWidth(0.5) + + let gridSpacing: CGFloat = scale // 1 meter grid + + // Vertical lines + var x: CGFloat = 0 + while x < size.width { + context.move(to: CGPoint(x: x, y: 0)) + context.addLine(to: CGPoint(x: x, y: size.height)) + x += gridSpacing + } + + // Horizontal lines + var y: CGFloat = 0 + while y < size.height { + context.move(to: CGPoint(x: 0, y: y)) + context.addLine(to: CGPoint(x: size.width, y: y)) + y += gridSpacing + } + + context.strokePath() + } + + private func drawWalls(context: CGContext, color: UIColor) { + context.setStrokeColor(color.cgColor) + context.setLineWidth(wallThickness) + context.setLineCap(.round) + context.setLineJoin(.round) + + // Draw walls from captured room data if available + if let room = capturedRoom { + for surface in room.walls { + drawSurface(context: context, surface: surface) + } + } + + // Draw walls from analyzed rooms + for room in rooms { + drawRoomWalls(context: context, room: room) + } + + context.strokePath() + } + + private func drawRoomWalls(context: CGContext, room: RoomAnalyzer.IdentifiedRoom) { + let points = room.wallPoints + guard points.count >= 3 else { return } + + context.beginPath() + + // Convert 3D points to 2D and scale + let scaledPoints = points.map { point in + CGPoint(x: CGFloat(point.x) * scale, y: CGFloat(point.z) * scale) + } + + // Draw walls + context.move(to: scaledPoints[0]) + for i in 1.. 0 { + let areaLabel = String(format: "%.1f m²", room.floorArea) + let areaAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 11), + .foregroundColor: color.withAlphaComponent(0.7) + ] + + let areaSize = areaLabel.size(withAttributes: areaAttributes) + let areaRect = CGRect( + x: transformedCenter.x - areaSize.width / 2, + y: transformedCenter.y + size.height / 2 + 2, + width: areaSize.width, + height: areaSize.height + ) + + areaLabel.draw(in: areaRect, withAttributes: areaAttributes) + } + } + } + + private func drawDimensions(context: CGContext, transform: CGAffineTransform, color: UIColor) { + context.setStrokeColor(color.cgColor) + context.setLineWidth(0.5) + + // Draw dimension lines for rooms + for room in rooms { + drawRoomDimensions(context: context, room: room, transform: transform, color: color) + } + } + + private func drawRoomDimensions(context: CGContext, room: RoomAnalyzer.IdentifiedRoom, transform: CGAffineTransform, color: UIColor) { + let points = room.wallPoints + guard points.count >= 2 else { return } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 10), + .foregroundColor: color + ] + + // Draw dimensions for each wall + for i in 0.. CGAffineTransform { + guard !rooms.isEmpty else { + return CGAffineTransform.identity + } + + // Calculate bounds + let allPoints = rooms.flatMap { $0.wallPoints } + guard !allPoints.isEmpty else { + return CGAffineTransform.identity + } + + let xValues = allPoints.map { CGFloat($0.x) * scale } + let zValues = allPoints.map { CGFloat($0.z) * scale } + + let minX = xValues.min() ?? 0 + let maxX = xValues.max() ?? 0 + let minZ = zValues.min() ?? 0 + let maxZ = zValues.max() ?? 0 + + let floorPlanWidth = maxX - minX + let floorPlanHeight = maxZ - minZ + + // Calculate scale to fit + let scaleX = (size.width - 100) / max(floorPlanWidth, 1) + let scaleY = (size.height - 100) / max(floorPlanHeight, 1) + let fitScale = min(scaleX, scaleY, 1.0) + + // Calculate translation to center + let centerX = size.width / 2 + let centerY = size.height / 2 + let floorPlanCenterX = (minX + maxX) / 2 + let floorPlanCenterY = (minZ + maxZ) / 2 + + // Create transform + var transform = CGAffineTransform.identity + transform = transform.translatedBy(x: centerX, y: centerY) + transform = transform.scaledBy(x: fitScale, y: fitScale) + transform = transform.translatedBy(x: -floorPlanCenterX, y: -floorPlanCenterY) + + return transform + } + + private func calculateRoomCenter(room: RoomAnalyzer.IdentifiedRoom) -> CGPoint { + let points = room.wallPoints + guard !points.isEmpty else { + return .zero + } + + let sumX = points.reduce(0) { $0 + $1.x } + let sumZ = points.reduce(0) { $0 + $1.z } + + return CGPoint( + x: CGFloat(sumX / Float(points.count)) * scale, + y: CGFloat(sumZ / Float(points.count)) * scale + ) + } + + private func drawSurface(context: CGContext, surface: CapturedRoom.Surface) { + // Convert surface to path + let transform = surface.transform + let dimensions = surface.dimensions + + let position = transform.position + let scaledPosition = CGPoint(x: CGFloat(position.x) * scale, y: CGFloat(position.z) * scale) + + // Draw surface outline + let rect = CGRect( + x: scaledPosition.x - CGFloat(dimensions.x) * scale / 2, + y: scaledPosition.y - CGFloat(dimensions.y) * scale / 2, + width: CGFloat(dimensions.x) * scale, + height: CGFloat(dimensions.y) * scale + ) + + context.addRect(rect) + } + + // MARK: - Export Methods + + /// Export floor plan as PDF + func exportAsPDF(size: CGSize) -> Data? { + let pdfData = NSMutableData() + + guard let consumer = CGDataConsumer(data: pdfData as CFMutableData), + let pdfContext = CGContext(consumer: consumer, mediaBox: nil, nil) else { + return nil + } + + pdfContext.beginPDFPage(nil) + + // Draw floor plan to PDF context + if let floorPlanImage = generateFloorPlan(size: size), + let cgImage = floorPlanImage.cgImage { + pdfContext.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + + pdfContext.endPDFPage() + pdfContext.closePDF() + + return pdfData as Data + } + + /// Export floor plan as SVG + func exportAsSVG(size: CGSize) -> String { + var svg = """ + + + """ + + // Add background + let colors = styleColors[renderStyle] ?? styleColors[.modern]! + svg += """ + + """ + + // Add walls + for room in rooms { + svg += createSVGPath(for: room, color: colors.walls) + } + + svg += "" + + return svg + } + + private func createSVGPath(for room: RoomAnalyzer.IdentifiedRoom, color: UIColor) -> String { + let points = room.wallPoints + guard !points.isEmpty else { return "" } + + var path = "" + + return path + } +} + +// MARK: - UIColor Extension +extension UIColor { + var hexString: String { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + getRed(&r, green: &g, blue: &b, alpha: &a) + + return String(format: "#%02X%02X%02X", + Int(r * 255), + Int(g * 255), + Int(b * 255)) + } +} \ No newline at end of file diff --git a/RoomPlanSimple/RFHeatmapGenerator.swift b/RoomPlanSimple/RFHeatmapGenerator.swift new file mode 100644 index 0000000..f406b2d --- /dev/null +++ b/RoomPlanSimple/RFHeatmapGenerator.swift @@ -0,0 +1,710 @@ +import Foundation +import UIKit +import simd +import CoreGraphics +import Accelerate + +// MARK: - RF Heatmap Generator +/// Advanced heatmap generator with multiple interpolation algorithms +class RFHeatmapGenerator { + + // MARK: - Enums + enum InterpolationMethod { + case nearestNeighbor + case linear + case bilinear + case bicubic + case idw // Inverse Distance Weighting + case kriging + case spline + } + + enum ColorScheme { + case traditional // Red to Green + case thermal // Black to White through colors + case spectrum // Full spectrum + case grayscale + case custom(colors: [UIColor]) + + func colors() -> [UIColor] { + switch self { + case .traditional: + return [ + UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0), // Red (poor) + UIColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0), // Orange + UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0), // Yellow + UIColor(red: 0.5, green: 1.0, blue: 0.0, alpha: 1.0), // Yellow-green + UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green (excellent) + ] + case .thermal: + return [ + UIColor.black, + UIColor.blue, + UIColor.cyan, + UIColor.green, + UIColor.yellow, + UIColor.orange, + UIColor.red, + UIColor.white + ] + case .spectrum: + return [ + UIColor(red: 0.5, green: 0.0, blue: 1.0, alpha: 1.0), // Purple + UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0), // Blue + UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0), // Cyan + UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0), // Green + UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0), // Yellow + UIColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0), // Orange + UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) // Red + ] + case .grayscale: + return [ + UIColor(white: 0.0, alpha: 1.0), + UIColor(white: 0.25, alpha: 1.0), + UIColor(white: 0.5, alpha: 1.0), + UIColor(white: 0.75, alpha: 1.0), + UIColor(white: 1.0, alpha: 1.0) + ] + case .custom(let colors): + return colors + } + } + } + + // MARK: - Properties + private let propagationModel: RFPropagationModel + private var colorScheme: ColorScheme = .traditional + private var interpolationMethod: InterpolationMethod = .idw + private var resolution: Float = 0.5 // meters per pixel + private var smoothingFactor: Float = 2.0 + + // Cache for performance + private var cachedHeatmapImage: UIImage? + private var cachedPropagationData: [RFPropagationModel.PropagationPoint]? + + // MARK: - Initialization + init(propagationModel: RFPropagationModel) { + self.propagationModel = propagationModel + print("🎨 RF Heatmap Generator initialized") + } + + // MARK: - Configuration + func setColorScheme(_ scheme: ColorScheme) { + self.colorScheme = scheme + invalidateCache() + } + + func setInterpolationMethod(_ method: InterpolationMethod) { + self.interpolationMethod = method + invalidateCache() + } + + func setResolution(_ resolution: Float) { + self.resolution = max(0.1, min(5.0, resolution)) + invalidateCache() + } + + private func invalidateCache() { + cachedHeatmapImage = nil + cachedPropagationData = nil + } + + // MARK: - Heatmap Generation + + /// Generate 2D heatmap image + func generateHeatmapImage(size: CGSize, floorHeight: Float = 1.0) -> UIImage? { + // Check cache + if let cached = cachedHeatmapImage { + return cached + } + + // Generate propagation data if needed + if cachedPropagationData == nil { + cachedPropagationData = propagationModel.generatePropagationMap(resolution: resolution) + } + + guard let propagationData = cachedPropagationData, !propagationData.isEmpty else { + print("⚠️ No propagation data available") + return nil + } + + // Create heatmap grid + let gridSize = calculateGridSize(for: size) + var heatmapGrid = createHeatmapGrid( + propagationData: propagationData, + gridSize: gridSize, + floorHeight: floorHeight + ) + + // Apply interpolation + heatmapGrid = applyInterpolation( + grid: heatmapGrid, + method: interpolationMethod, + size: gridSize + ) + + // Apply smoothing + if smoothingFactor > 1.0 { + heatmapGrid = applyGaussianSmoothing( + grid: heatmapGrid, + size: gridSize, + sigma: smoothingFactor + ) + } + + // Convert to image + let image = createImage(from: heatmapGrid, size: size) + cachedHeatmapImage = image + + return image + } + + /// Generate 3D heatmap volume data + func generate3DHeatmapVolume(heightLevels: Int = 5) -> [Float] { + let volumeData = propagationModel.generate3DPropagationVolume( + resolution: resolution, + heightLevels: heightLevels + ) + + // Convert to float array for visualization + return volumeData.map { $0.signalStrength } + } + + // MARK: - Grid Creation + + private func calculateGridSize(for imageSize: CGSize) -> (width: Int, height: Int) { + let width = Int(imageSize.width) + let height = Int(imageSize.height) + return (width, height) + } + + private func createHeatmapGrid( + propagationData: [RFPropagationModel.PropagationPoint], + gridSize: (width: Int, height: Int), + floorHeight: Float + ) -> [[Float]] { + var grid = Array(repeating: Array(repeating: Float(-100.0), count: gridSize.width), count: gridSize.height) + + // Find bounds + let xValues = propagationData.map { $0.position.x } + let zValues = propagationData.map { $0.position.z } + + guard let minX = xValues.min(), let maxX = xValues.max(), + let minZ = zValues.min(), let maxZ = zValues.max() else { + return grid + } + + let xRange = maxX - minX + let zRange = maxZ - minZ + + // Filter points at the specified height + let heightTolerance: Float = 0.5 + let relevantPoints = propagationData.filter { + abs($0.position.y - floorHeight) < heightTolerance + } + + // Map propagation points to grid + for point in relevantPoints { + let x = Int(((point.position.x - minX) / xRange) * Float(gridSize.width - 1)) + let z = Int(((point.position.z - minZ) / zRange) * Float(gridSize.height - 1)) + + if x >= 0 && x < gridSize.width && z >= 0 && z < gridSize.height { + grid[z][x] = point.signalStrength + } + } + + return grid + } + + // MARK: - Interpolation Methods + + private func applyInterpolation( + grid: [[Float]], + method: InterpolationMethod, + size: (width: Int, height: Int) + ) -> [[Float]] { + switch method { + case .nearestNeighbor: + return applyNearestNeighborInterpolation(grid: grid, size: size) + case .linear, .bilinear: + return applyBilinearInterpolation(grid: grid, size: size) + case .bicubic: + return applyBicubicInterpolation(grid: grid, size: size) + case .idw: + return applyIDWInterpolation(grid: grid, size: size) + case .kriging: + return applyKrigingInterpolation(grid: grid, size: size) + case .spline: + return applySplineInterpolation(grid: grid, size: size) + } + } + + private func applyNearestNeighborInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + var interpolated = grid + + for y in 0..= 0 && ny < size.height && nx >= 0 && nx < size.width { + if grid[ny][nx] != -100.0 { + let distance = sqrt(Float(dx * dx + dy * dy)) + if distance < minDistance { + minDistance = distance + nearestValue = grid[ny][nx] + } + } + } + } + } + + interpolated[y][x] = nearestValue + } + } + } + + return interpolated + } + + private func applyBilinearInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + var interpolated = grid + + // Collect known points + var knownPoints: [(x: Int, y: Int, value: Float)] = [] + for y in 0..= 4 { + interpolated[y][x] = bilinearInterpolate( + target: (x, y), + points: nearestPoints + ) + } else if !nearestPoints.isEmpty { + // Fall back to weighted average + interpolated[y][x] = weightedAverage( + target: (x, y), + points: nearestPoints + ) + } + } + } + } + + return interpolated + } + + private func applyBicubicInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + // Simplified bicubic interpolation + var interpolated = applyBilinearInterpolation(grid: grid, size: size) + + // Apply cubic smoothing + for _ in 0..<2 { + interpolated = applyGaussianSmoothing( + grid: interpolated, + size: size, + sigma: 1.5 + ) + } + + return interpolated + } + + private func applyIDWInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + var interpolated = grid + let power: Float = 2.0 // IDW power parameter + + // Collect known points + var knownPoints: [(x: Int, y: Int, value: Float)] = [] + for y in 0.. 0 { + let weight = 1.0 / pow(distance, power) + weightSum += weight + valueSum += weight * point.value + } else { + // Point coincides with known point + interpolated[y][x] = point.value + weightSum = 1 + valueSum = point.value + break + } + } + + if weightSum > 0 { + interpolated[y][x] = valueSum / weightSum + } + } + } + } + + return interpolated + } + + private func applyKrigingInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + // Simplified Kriging - using Gaussian process regression concepts + // For full implementation, would need variogram modeling + + // For now, use IDW with adaptive radius + return applyIDWInterpolation(grid: grid, size: size) + } + + private func applySplineInterpolation( + grid: [[Float]], + size: (width: Int, height: Int) + ) -> [[Float]] { + // Simplified spline interpolation + // Full implementation would use B-splines or thin-plate splines + + var interpolated = applyBilinearInterpolation(grid: grid, size: size) + + // Apply multiple smoothing passes for spline-like effect + for _ in 0..<3 { + interpolated = applyGaussianSmoothing( + grid: interpolated, + size: size, + sigma: 2.0 + ) + } + + return interpolated + } + + // MARK: - Smoothing + + private func applyGaussianSmoothing( + grid: [[Float]], + size: (width: Int, height: Int), + sigma: Float + ) -> [[Float]] { + var smoothed = grid + + // Create Gaussian kernel + let kernelSize = Int(ceil(sigma * 3)) * 2 + 1 + let kernel = createGaussianKernel(size: kernelSize, sigma: sigma) + + // Apply convolution + let halfKernel = kernelSize / 2 + + for y in halfKernel..<(size.height - halfKernel) { + for x in halfKernel..<(size.width - halfKernel) { + var sum: Float = 0 + var weightSum: Float = 0 + + for ky in 0.. 0 { + smoothed[y][x] = sum / weightSum + } + } + } + + return smoothed + } + + private func createGaussianKernel(size: Int, sigma: Float) -> [[Float]] { + var kernel = Array(repeating: Array(repeating: Float(0), count: size), count: size) + let center = size / 2 + var sum: Float = 0 + + for y in 0.. UIImage? { + let width = Int(size.width) + let height = Int(size.height) + + // Create bitmap context + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) + + guard let context = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: bitmapInfo.rawValue + ) else { + return nil + } + + // Draw heatmap + for y in 0.. UIColor { + // Normalize signal strength to 0-1 range + let minSignal: Float = -100.0 + let maxSignal: Float = -30.0 + let normalized = max(0, min(1, (strength - minSignal) / (maxSignal - minSignal))) + + // Get color scheme colors + let colors = colorScheme.colors() + + // Interpolate between colors + let colorIndex = normalized * Float(colors.count - 1) + let lowerIndex = Int(floor(colorIndex)) + let upperIndex = min(lowerIndex + 1, colors.count - 1) + let fraction = colorIndex - Float(lowerIndex) + + return interpolateColor( + from: colors[lowerIndex], + to: colors[upperIndex], + fraction: CGFloat(fraction) + ) + } + + private func interpolateColor(from: UIColor, to: UIColor, fraction: CGFloat) -> UIColor { + var fromR: CGFloat = 0, fromG: CGFloat = 0, fromB: CGFloat = 0, fromA: CGFloat = 0 + var toR: CGFloat = 0, toG: CGFloat = 0, toB: CGFloat = 0, toA: CGFloat = 0 + + from.getRed(&fromR, green: &fromG, blue: &fromB, alpha: &fromA) + to.getRed(&toR, green: &toG, blue: &toB, alpha: &toA) + + let r = fromR + (toR - fromR) * fraction + let g = fromG + (toG - fromG) * fraction + let b = fromB + (toB - fromB) * fraction + let a = fromA + (toA - fromA) * fraction + + return UIColor(red: r, green: g, blue: b, alpha: a) + } + + // MARK: - Contour Lines + + private func shouldDrawContours() -> Bool { + // Could be made configurable + return true + } + + private func drawContourLines( + on context: CGContext, + grid: [[Float]], + size: (width: Int, height: Int) + ) { + let contourLevels: [Float] = [-80, -70, -60, -50, -40] // dBm levels + + context.setLineWidth(0.5) + context.setAlpha(0.3) + + for level in contourLevels { + context.setStrokeColor(contourColorForLevel(level).cgColor) + + // Simple contour detection + for y in 1..= level { + // Check neighbors + if x > 0 && grid[y][x-1] < level { + // Vertical line on left + context.move(to: CGPoint(x: x, y: y)) + context.addLine(to: CGPoint(x: x, y: y + 1)) + context.strokePath() + } + if y > 0 && grid[y-1][x] < level { + // Horizontal line on top + context.move(to: CGPoint(x: x, y: y)) + context.addLine(to: CGPoint(x: x + 1, y: y)) + context.strokePath() + } + } + } + } + } + + context.setAlpha(1.0) + } + + private func contourColorForLevel(_ level: Float) -> UIColor { + switch level { + case -40: return UIColor.green + case -50: return UIColor.yellow + case -60: return UIColor.orange + case -70: return UIColor.red + case -80: return UIColor.darkGray + default: return UIColor.black + } + } + + // MARK: - Helper Methods + + private func findNearestPoints( + target: (x: Int, y: Int), + points: [(x: Int, y: Int, value: Float)], + count: Int + ) -> [(x: Int, y: Int, value: Float)] { + let sorted = points.sorted { p1, p2 in + let d1 = pow(Float(p1.x - target.x), 2) + pow(Float(p1.y - target.y), 2) + let d2 = pow(Float(p2.x - target.x), 2) + pow(Float(p2.y - target.y), 2) + return d1 < d2 + } + + return Array(sorted.prefix(count)) + } + + private func bilinearInterpolate( + target: (x: Int, y: Int), + points: [(x: Int, y: Int, value: Float)] + ) -> Float { + // Simplified bilinear interpolation + return weightedAverage(target: target, points: points) + } + + private func weightedAverage( + target: (x: Int, y: Int), + points: [(x: Int, y: Int, value: Float)] + ) -> Float { + var weightSum: Float = 0 + var valueSum: Float = 0 + + for point in points { + let dx = Float(target.x - point.x) + let dy = Float(target.y - point.y) + let distance = sqrt(dx * dx + dy * dy) + + let weight = distance > 0 ? 1.0 / distance : 1.0 + weightSum += weight + valueSum += weight * point.value + } + + return weightSum > 0 ? valueSum / weightSum : -100.0 + } + + // MARK: - Export Methods + + /// Export heatmap as data for external processing + func exportHeatmapData() -> [String: Any] { + guard let propagationData = cachedPropagationData else { + return [:] + } + + let data: [String: Any] = [ + "points": propagationData.map { point in + [ + "x": point.position.x, + "y": point.position.y, + "z": point.position.z, + "signal": point.signalStrength, + "quality": "\(point.quality)" + ] + }, + "metadata": [ + "resolution": resolution, + "interpolation": "\(interpolationMethod)", + "colorScheme": "\(colorScheme)" + ] + ] + + return data + } +} \ No newline at end of file diff --git a/RoomPlanSimple/RFPropagationModel.swift b/RoomPlanSimple/RFPropagationModel.swift new file mode 100644 index 0000000..9807092 --- /dev/null +++ b/RoomPlanSimple/RFPropagationModel.swift @@ -0,0 +1,603 @@ +import Foundation +import simd +import RoomPlan + +// MARK: - RF Propagation Model +/// Advanced RF propagation model for WiFi signal analysis +class RFPropagationModel { + + // MARK: - Constants + struct Constants { + // Path loss exponents for different environments + static let freeSpacePathLossExponent: Float = 2.0 + static let indoorPathLossExponent: Float = 3.0 + static let obstructedPathLossExponent: Float = 4.0 + + // Frequency bands + static let frequency2_4GHz: Float = 2400.0 // MHz + static let frequency5GHz: Float = 5000.0 // MHz + static let frequency6GHz: Float = 6000.0 // MHz + + // Material attenuation (dB) + static let wallAttenuation: Float = 5.0 + static let floorAttenuation: Float = 15.0 + static let glassAttenuation: Float = 2.0 + static let doorAttenuation: Float = 3.0 + static let concreteAttenuation: Float = 10.0 + static let woodAttenuation: Float = 4.0 + static let metalAttenuation: Float = 20.0 + + // Signal quality thresholds (dBm) + static let excellentSignal: Float = -30.0 + static let goodSignal: Float = -50.0 + static let fairSignal: Float = -70.0 + static let poorSignal: Float = -85.0 + static let noSignal: Float = -100.0 + } + + // MARK: - Enums + enum MaterialType { + case air + case drywall + case concrete + case glass + case wood + case metal + case floor + case door + + var attenuationDB: Float { + switch self { + case .air: return 0.0 + case .drywall: return Constants.wallAttenuation + case .concrete: return Constants.concreteAttenuation + case .glass: return Constants.glassAttenuation + case .wood: return Constants.woodAttenuation + case .metal: return Constants.metalAttenuation + case .floor: return Constants.floorAttenuation + case .door: return Constants.doorAttenuation + } + } + } + + enum FrequencyBand { + case band2_4GHz + case band5GHz + case band6GHz + + var frequency: Float { + switch self { + case .band2_4GHz: return Constants.frequency2_4GHz + case .band5GHz: return Constants.frequency5GHz + case .band6GHz: return Constants.frequency6GHz + } + } + + var wavelength: Float { + // wavelength = c / f (c = 3e8 m/s) + return 300.0 / frequency // in meters + } + } + + // MARK: - Properties + private var rooms: [RoomAnalyzer.IdentifiedRoom] = [] + private var walls: [Wall] = [] + private var accessPoints: [AccessPoint] = [] + private var frequencyBand: FrequencyBand = .band2_4GHz + + // MARK: - Structures + struct Wall { + let startPoint: simd_float3 + let endPoint: simd_float3 + let height: Float + let material: MaterialType + + func intersects(ray: Ray) -> Bool { + // Simple ray-plane intersection for wall + let wallNormal = normalize(cross(endPoint - startPoint, simd_float3(0, height, 0))) + let denominator = dot(ray.direction, wallNormal) + + guard abs(denominator) > 0.0001 else { return false } + + let t = dot(startPoint - ray.origin, wallNormal) / denominator + guard t >= 0 else { return false } + + let intersectionPoint = ray.origin + t * ray.direction + + // Check if intersection point is within wall bounds + return isPointOnWall(intersectionPoint) + } + + private func isPointOnWall(_ point: simd_float3) -> Bool { + // Check if point is within wall boundaries + let minX = min(startPoint.x, endPoint.x) + let maxX = max(startPoint.x, endPoint.x) + let minZ = min(startPoint.z, endPoint.z) + let maxZ = max(startPoint.z, endPoint.z) + + return point.x >= minX && point.x <= maxX && + point.z >= minZ && point.z <= maxZ && + point.y >= 0 && point.y <= height + } + } + + struct Ray { + let origin: simd_float3 + let direction: simd_float3 + } + + struct AccessPoint { + let position: simd_float3 + let transmitPower: Float // dBm + let antennaGain: Float // dBi + let frequency: FrequencyBand + let name: String + } + + struct PropagationPoint { + let position: simd_float3 + let signalStrength: Float // dBm + let pathLoss: Float // dB + let quality: SignalQuality + let dominantAP: AccessPoint? + } + + enum SignalQuality { + case excellent + case good + case fair + case poor + case none + + var color: (r: Float, g: Float, b: Float, a: Float) { + switch self { + case .excellent: return (0.0, 1.0, 0.0, 0.8) // Green + case .good: return (0.5, 1.0, 0.0, 0.7) // Yellow-green + case .fair: return (1.0, 1.0, 0.0, 0.6) // Yellow + case .poor: return (1.0, 0.5, 0.0, 0.5) // Orange + case .none: return (1.0, 0.0, 0.0, 0.4) // Red + } + } + } + + // MARK: - Initialization + init() { + print("🔬 RF Propagation Model initialized") + } + + // MARK: - Configuration + func configureWithRooms(_ rooms: [RoomAnalyzer.IdentifiedRoom]) { + self.rooms = rooms + self.walls = extractWallsFromRooms(rooms) + print("📐 Configured with \(rooms.count) rooms and \(walls.count) walls") + } + + func addAccessPoint(position: simd_float3, transmitPower: Float = 20.0, name: String = "AP") { + let ap = AccessPoint( + position: position, + transmitPower: transmitPower, + antennaGain: 2.15, // Standard dipole antenna + frequency: frequencyBand, + name: name + ) + accessPoints.append(ap) + print("📡 Added access point '\(name)' at position \(position)") + } + + func setFrequencyBand(_ band: FrequencyBand) { + self.frequencyBand = band + print("📻 Set frequency band to \(band)") + } + + // MARK: - Path Loss Calculations + + /// Calculate free space path loss (FSPL) + private func calculateFreeSpacePathLoss(distance: Float, frequency: Float) -> Float { + guard distance > 0 else { return 0 } + // FSPL (dB) = 20 * log10(d) + 20 * log10(f) + 20 * log10(4π/c) + // Simplified: FSPL = 20 * log10(d) + 20 * log10(f) - 147.55 + return 20.0 * log10(distance) + 20.0 * log10(frequency) - 147.55 + } + + /// Calculate indoor path loss using ITU-R model + private func calculateIndoorPathLoss(distance: Float, frequency: Float, numWalls: Int, numFloors: Int) -> Float { + let fspl = calculateFreeSpacePathLoss(distance: distance, frequency: frequency) + + // Add wall and floor losses + let wallLoss = Float(numWalls) * Constants.wallAttenuation + let floorLoss = Float(numFloors) * Constants.floorAttenuation + + // Add distance-dependent indoor factor + let indoorFactor = 10.0 * log10(distance) * (Constants.indoorPathLossExponent - Constants.freeSpacePathLossExponent) + + return fspl + wallLoss + floorLoss + indoorFactor + } + + /// Calculate path loss with obstacles + private func calculatePathLoss(from source: simd_float3, to destination: simd_float3) -> Float { + let distance = simd_distance(source, destination) + let frequency = frequencyBand.frequency + + // Count obstacles + let obstacles = countObstacles(from: source, to: destination) + + // Calculate total path loss + let pathLoss = calculateIndoorPathLoss( + distance: distance, + frequency: frequency, + numWalls: obstacles.walls, + numFloors: obstacles.floors + ) + + // Add material-specific losses + let materialLoss = obstacles.materials.reduce(0.0) { $0 + $1.attenuationDB } + + return pathLoss + materialLoss + } + + /// Count obstacles between two points + private func countObstacles(from source: simd_float3, to destination: simd_float3) -> (walls: Int, floors: Int, materials: [MaterialType]) { + let ray = Ray( + origin: source, + direction: normalize(destination - source) + ) + + var wallCount = 0 + var floorCount = 0 + var materials: [MaterialType] = [] + + for wall in walls { + if wall.intersects(ray: ray) { + wallCount += 1 + materials.append(wall.material) + } + } + + // Check floor crossings + let heightDiff = abs(destination.y - source.y) + floorCount = Int(heightDiff / 3.0) // Assume 3m floor height + + return (wallCount, floorCount, materials) + } + + // MARK: - Signal Strength Calculation + + /// Calculate received signal strength at a point + func calculateSignalStrength(at point: simd_float3, from ap: AccessPoint) -> Float { + let pathLoss = calculatePathLoss(from: ap.position, to: point) + let receivedPower = ap.transmitPower + ap.antennaGain - pathLoss + return receivedPower + } + + /// Calculate signal quality from strength + private func signalQuality(from strength: Float) -> SignalQuality { + switch strength { + case Constants.excellentSignal...: + return .excellent + case Constants.goodSignal.. [PropagationPoint] { + var propagationPoints: [PropagationPoint] = [] + + guard !accessPoints.isEmpty else { + print("⚠️ No access points configured") + return propagationPoints + } + + // Calculate bounds + let bounds = calculateEnvironmentBounds() + + // Generate grid points + let gridPoints = generateGridPoints(bounds: bounds, resolution: resolution) + + print("🗺 Generating propagation map with \(gridPoints.count) points...") + + for point in gridPoints { + // Calculate signal from all APs + var maxSignal: Float = Constants.noSignal + var dominantAP: AccessPoint? + + for ap in accessPoints { + let signal = calculateSignalStrength(at: point, from: ap) + if signal > maxSignal { + maxSignal = signal + dominantAP = ap + } + } + + let quality = signalQuality(from: maxSignal) + let pathLoss = dominantAP != nil ? + calculatePathLoss(from: dominantAP!.position, to: point) : 0 + + let propagationPoint = PropagationPoint( + position: point, + signalStrength: maxSignal, + pathLoss: pathLoss, + quality: quality, + dominantAP: dominantAP + ) + + propagationPoints.append(propagationPoint) + } + + print("✅ Generated \(propagationPoints.count) propagation points") + return propagationPoints + } + + /// Generate 3D propagation volume + func generate3DPropagationVolume(resolution: Float = 1.0, heightLevels: Int = 3) -> [PropagationPoint] { + var volumePoints: [PropagationPoint] = [] + + let bounds = calculateEnvironmentBounds() + let heightStep = 3.0 / Float(heightLevels) // Assume 3m ceiling + + for level in 0.. [PropagationPoint] { + var points: [PropagationPoint] = [] + + let xSteps = Int((bounds.max.x - bounds.min.x) / resolution) + let zSteps = Int((bounds.max.z - bounds.min.z) / resolution) + + for x in 0...xSteps { + for z in 0...zSteps { + let point = simd_float3( + bounds.min.x + Float(x) * resolution, + height, + bounds.min.z + Float(z) * resolution + ) + + // Check if point is inside any room + if isPointInsideRooms(point) { + var maxSignal: Float = Constants.noSignal + var dominantAP: AccessPoint? + + for ap in accessPoints { + let signal = calculateSignalStrength(at: point, from: ap) + if signal > maxSignal { + maxSignal = signal + dominantAP = ap + } + } + + let quality = signalQuality(from: maxSignal) + let pathLoss = dominantAP != nil ? + calculatePathLoss(from: dominantAP!.position, to: point) : 0 + + points.append(PropagationPoint( + position: point, + signalStrength: maxSignal, + pathLoss: pathLoss, + quality: quality, + dominantAP: dominantAP + )) + } + } + } + + return points + } + + // MARK: - Helper Methods + + private func extractWallsFromRooms(_ rooms: [RoomAnalyzer.IdentifiedRoom]) -> [Wall] { + var walls: [Wall] = [] + + for room in rooms { + let wallPoints = room.wallPoints + guard wallPoints.count >= 2 else { continue } + + // Create walls from consecutive points + for i in 0.. (min: simd_float3, max: simd_float3) { + guard !rooms.isEmpty else { + return (simd_float3(-10, 0, -10), simd_float3(10, 3, 10)) + } + + let allPoints = rooms.flatMap { $0.wallPoints } + + let minX = allPoints.map { $0.x }.min() ?? -10 + let maxX = allPoints.map { $0.x }.max() ?? 10 + let minZ = allPoints.map { $0.z }.min() ?? -10 + let maxZ = allPoints.map { $0.z }.max() ?? 10 + + return ( + simd_float3(minX - 1, 0, minZ - 1), + simd_float3(maxX + 1, 3, maxZ + 1) + ) + } + + private func generateGridPoints(bounds: (min: simd_float3, max: simd_float3), resolution: Float) -> [simd_float3] { + var points: [simd_float3] = [] + + let xSteps = Int((bounds.max.x - bounds.min.x) / resolution) + let zSteps = Int((bounds.max.z - bounds.min.z) / resolution) + + for x in 0...xSteps { + for z in 0...zSteps { + let point = simd_float3( + bounds.min.x + Float(x) * resolution, + 1.0, // Standard measurement height + bounds.min.z + Float(z) * resolution + ) + + // Only add points inside rooms + if isPointInsideRooms(point) { + points.append(point) + } + } + } + + return points + } + + private func isPointInsideRooms(_ point: simd_float3) -> Bool { + for room in rooms { + if isPointInsidePolygon(point, polygon: room.wallPoints) { + return true + } + } + return false + } + + private func isPointInsidePolygon(_ point: simd_float3, polygon: [simd_float3]) -> Bool { + guard polygon.count >= 3 else { return false } + + var inside = false + let p1x = point.x + let p1z = point.z + + for i in 0.. p1z) != (p3z > p1z)) && + (p1x < (p3x - p2x) * (p1z - p2z) / (p3z - p2z) + p2x) { + inside = !inside + } + } + + return inside + } + + // MARK: - Optimization Methods + + /// Find optimal access point placements + func findOptimalAPPlacements(targetCoverage: Float = 0.95, maxAPs: Int = 3) -> [simd_float3] { + var optimalPositions: [simd_float3] = [] + + let bounds = calculateEnvironmentBounds() + let candidatePositions = generateCandidatePositions(bounds: bounds) + + print("🎯 Finding optimal AP placements from \(candidatePositions.count) candidates...") + + // Greedy algorithm: place APs to maximize coverage + var uncoveredPoints = Set(generateGridPoints(bounds: bounds, resolution: 1.0)) + + while optimalPositions.count < maxAPs && !uncoveredPoints.isEmpty { + var bestPosition: simd_float3? + var bestCoverage = 0 + + for candidate in candidatePositions { + // Skip if too close to existing APs + if optimalPositions.contains(where: { simd_distance($0, candidate) < 3.0 }) { + continue + } + + // Count coverage for this candidate + let coverage = countCoverage(apPosition: candidate, points: Array(uncoveredPoints)) + + if coverage > bestCoverage { + bestCoverage = coverage + bestPosition = candidate + } + } + + if let position = bestPosition { + optimalPositions.append(position) + + // Remove covered points + uncoveredPoints = uncoveredPoints.filter { point in + let signal = calculateSignalStrength( + at: point, + from: AccessPoint( + position: position, + transmitPower: 20.0, + antennaGain: 2.15, + frequency: frequencyBand, + name: "Optimal" + ) + ) + return signal < Constants.fairSignal + } + + print(" Added AP at \(position), \(uncoveredPoints.count) points remaining") + } else { + break + } + } + + print("✅ Found \(optimalPositions.count) optimal AP positions") + return optimalPositions + } + + private func generateCandidatePositions(bounds: (min: simd_float3, max: simd_float3)) -> [simd_float3] { + var candidates: [simd_float3] = [] + + // Generate candidates on a coarse grid + let spacing: Float = 2.0 + let xSteps = Int((bounds.max.x - bounds.min.x) / spacing) + let zSteps = Int((bounds.max.z - bounds.min.z) / spacing) + + for x in 0...xSteps { + for z in 0...zSteps { + let point = simd_float3( + bounds.min.x + Float(x) * spacing, + 2.5, // Ceiling mount height + bounds.min.z + Float(z) * spacing + ) + + if isPointInsideRooms(point) { + candidates.append(point) + } + } + } + + return candidates + } + + private func countCoverage(apPosition: simd_float3, points: [simd_float3]) -> Int { + let ap = AccessPoint( + position: apPosition, + transmitPower: 20.0, + antennaGain: 2.15, + frequency: frequencyBand, + name: "Test" + ) + + return points.filter { point in + calculateSignalStrength(at: point, from: ap) >= Constants.fairSignal + }.count + } +} \ No newline at end of file diff --git a/RoomPlanSimple/USDZRFPropagationIntegrator.swift b/RoomPlanSimple/USDZRFPropagationIntegrator.swift new file mode 100644 index 0000000..2c7a3d1 --- /dev/null +++ b/RoomPlanSimple/USDZRFPropagationIntegrator.swift @@ -0,0 +1,946 @@ +import Foundation +import RoomPlan +import RealityKit +import ModelIO +import SceneKit +import simd +import MetalKit + +// MARK: - USDZ RF Propagation Integrator +/// Integrates RF propagation visualization into USDZ files from RoomPlan +class USDZRFPropagationIntegrator { + + // MARK: - Properties + private let propagationModel: RFPropagationModel + private let heatmapGenerator: RFHeatmapGenerator + private var capturedRoom: CapturedRoom? + private var propagationData: [RFPropagationModel.PropagationPoint] = [] + + // Visualization settings + private var visualizationStyle: VisualizationStyle = .volumetric + private var signalOpacity: Float = 0.6 + private var gridResolution: Float = 0.5 // meters + private var heightLevels: Int = 5 + + // MARK: - Enums + enum VisualizationStyle { + case planar // 2D heatmap on floor + case volumetric // 3D volume rendering + case particles // Particle-based visualization + case isosurfaces // Signal strength contours + case hybrid // Combination of styles + } + + enum ExportFormat { + case usdz + case usda + case usd + } + + // MARK: - Initialization + init(propagationModel: RFPropagationModel) { + self.propagationModel = propagationModel + self.heatmapGenerator = RFHeatmapGenerator(propagationModel: propagationModel) + print("🎨 USDZ RF Propagation Integrator initialized") + } + + // MARK: - Configuration + func configure(with capturedRoom: CapturedRoom) { + self.capturedRoom = capturedRoom + + // Extract rooms for propagation model + let rooms = extractRoomsFromCapturedRoom(capturedRoom) + propagationModel.configureWithRooms(rooms) + } + + func setVisualizationStyle(_ style: VisualizationStyle) { + self.visualizationStyle = style + } + + func setSignalOpacity(_ opacity: Float) { + self.signalOpacity = max(0.0, min(1.0, opacity)) + } + + // MARK: - USDZ Enhancement + + /// Enhance existing USDZ file with RF propagation + func enhanceUSDZ(at url: URL, outputURL: URL) async throws { + print("🔧 Enhancing USDZ with RF propagation...") + + // Load the USDZ file + let asset = try await loadUSDZAsset(from: url) + + // Generate propagation data + generatePropagationData() + + // Add RF visualization based on style + switch visualizationStyle { + case .planar: + try await addPlanarVisualization(to: asset) + case .volumetric: + try await addVolumetricVisualization(to: asset) + case .particles: + try await addParticleVisualization(to: asset) + case .isosurfaces: + try await addIsosurfaceVisualization(to: asset) + case .hybrid: + try await addHybridVisualization(to: asset) + } + + // Export enhanced USDZ + try await exportUSDZ(asset: asset, to: outputURL) + + print("✅ USDZ enhanced successfully") + } + + /// Create new USDZ with RF propagation from RoomPlan data + func createRFUSDZ(outputURL: URL) async throws { + guard let room = capturedRoom else { + throw IntegratorError.noCapturedRoom + } + + print("🏗 Creating RF-enhanced USDZ...") + + // Create base scene + let scene = createBaseScene(from: room) + + // Generate propagation data + generatePropagationData() + + // Add RF visualization + addRFVisualizationToScene(scene) + + // Export to USDZ + try await exportSceneToUSDZ(scene: scene, to: outputURL) + + print("✅ RF-enhanced USDZ created successfully") + } + + // MARK: - Propagation Data Generation + + private func generatePropagationData() { + // Generate 3D propagation volume + propagationData = propagationModel.generate3DPropagationVolume( + resolution: gridResolution, + heightLevels: heightLevels + ) + + print("📊 Generated \(propagationData.count) propagation points") + } + + // MARK: - Visualization Methods + + private func addPlanarVisualization(to asset: MDLAsset) async throws { + // Create floor heatmap mesh + let heatmapMesh = createHeatmapMesh() + + // Apply signal strength texture + let texture = try await createSignalTexture() + applyTextureToMesh(heatmapMesh, texture: texture) + + // Add to asset + asset.add(heatmapMesh) + } + + private func addVolumetricVisualization(to asset: MDLAsset) async throws { + // Create voxel grid for volumetric rendering + let voxelGrid = createVoxelGrid() + + // Apply signal strength colors + colorVoxelGrid(voxelGrid) + + // Add to asset + asset.add(voxelGrid) + } + + private func addParticleVisualization(to asset: MDLAsset) async throws { + // Create particle system + let particles = createSignalParticles() + + // Add to asset + asset.add(particles) + } + + private func addIsosurfaceVisualization(to asset: MDLAsset) async throws { + // Create isosurfaces for different signal levels + let signalLevels: [Float] = [-30, -50, -70, -85] // dBm + + for level in signalLevels { + let isosurface = createIsosurface(at: level) + asset.add(isosurface) + } + } + + private func addHybridVisualization(to asset: MDLAsset) async throws { + // Combine multiple visualization styles + try await addPlanarVisualization(to: asset) + try await addIsosurfaceVisualization(to: asset) + + // Add access point indicators + addAccessPointIndicators(to: asset) + } + + // MARK: - Mesh Creation + + private func createHeatmapMesh() -> MDLMesh { + let allocator = MDLMeshBufferDataAllocator() + + // Calculate mesh dimensions based on room bounds + let bounds = calculateRoomBounds() + let width = bounds.max.x - bounds.min.x + let depth = bounds.max.z - bounds.min.z + + // Create plane mesh + let mesh = MDLMesh( + planeWithExtent: vector3(width, depth, 0), + segments: vector2(Int32(width / gridResolution), Int32(depth / gridResolution)), + geometryType: .triangles, + allocator: allocator + ) + + mesh.name = "RF_Heatmap" + + // Position at floor level + let transform = MDLTransform() + transform.translation = vector3( + (bounds.min.x + bounds.max.x) / 2, + 0.01, // Slightly above floor + (bounds.min.z + bounds.max.z) / 2 + ) + mesh.transform = transform + + return mesh + } + + private func createVoxelGrid() -> MDLMesh { + let allocator = MDLMeshBufferDataAllocator() + var vertices: [Float] = [] + var normals: [Float] = [] + var colors: [Float] = [] + var indices: [UInt32] = [] + + let voxelSize: Float = gridResolution + var vertexIndex: UInt32 = 0 + + for point in propagationData { + // Skip weak signals for performance + guard point.signalStrength > -85 else { continue } + + // Create voxel cube at this point + let center = point.position + let halfSize = voxelSize / 2 + + // Define cube vertices + let cubeVertices: [simd_float3] = [ + center + simd_float3(-halfSize, -halfSize, -halfSize), + center + simd_float3( halfSize, -halfSize, -halfSize), + center + simd_float3( halfSize, halfSize, -halfSize), + center + simd_float3(-halfSize, halfSize, -halfSize), + center + simd_float3(-halfSize, -halfSize, halfSize), + center + simd_float3( halfSize, -halfSize, halfSize), + center + simd_float3( halfSize, halfSize, halfSize), + center + simd_float3(-halfSize, halfSize, halfSize) + ] + + // Add vertices + for vertex in cubeVertices { + vertices.append(contentsOf: [vertex.x, vertex.y, vertex.z]) + normals.append(contentsOf: [0, 1, 0]) // Simplified normals + + // Color based on signal strength + let color = signalStrengthToColor(point.signalStrength) + colors.append(contentsOf: [color.r, color.g, color.b, signalOpacity]) + } + + // Define cube faces (triangles) + let faceIndices: [UInt32] = [ + // Front face + 0, 1, 2, 0, 2, 3, + // Back face + 4, 6, 5, 4, 7, 6, + // Top face + 3, 2, 6, 3, 6, 7, + // Bottom face + 0, 5, 1, 0, 4, 5, + // Right face + 1, 5, 6, 1, 6, 2, + // Left face + 0, 3, 7, 0, 7, 4 + ] + + // Add indices with offset + for index in faceIndices { + indices.append(vertexIndex + index) + } + + vertexIndex += 8 + } + + // Create mesh from vertices + let vertexBuffer = allocator.newBuffer( + with: Data(bytes: vertices, count: vertices.count * MemoryLayout.size), + type: .vertex + ) + + let normalBuffer = allocator.newBuffer( + with: Data(bytes: normals, count: normals.count * MemoryLayout.size), + type: .vertex + ) + + let colorBuffer = allocator.newBuffer( + with: Data(bytes: colors, count: colors.count * MemoryLayout.size), + type: .vertex + ) + + let indexBuffer = allocator.newBuffer( + with: Data(bytes: indices, count: indices.count * MemoryLayout.size), + type: .index + ) + + let submesh = MDLSubmesh( + indexBuffer: indexBuffer, + indexCount: indices.count, + indexType: .uInt32, + geometryType: .triangles, + material: nil + ) + + let mesh = MDLMesh( + vertexBuffers: [vertexBuffer, normalBuffer, colorBuffer], + vertexCount: vertices.count / 3, + descriptor: createVoxelVertexDescriptor(), + submeshes: [submesh] + ) + + mesh.name = "RF_VoxelGrid" + + return mesh + } + + private func createSignalParticles() -> MDLMesh { + let allocator = MDLMeshBufferDataAllocator() + var vertices: [Float] = [] + var colors: [Float] = [] + + // Create particles at measurement points + for point in propagationData { + // Add particle position + vertices.append(contentsOf: [point.position.x, point.position.y, point.position.z]) + + // Add particle color based on signal strength + let color = signalStrengthToColor(point.signalStrength) + colors.append(contentsOf: [color.r, color.g, color.b, signalOpacity]) + } + + let vertexBuffer = allocator.newBuffer( + with: Data(bytes: vertices, count: vertices.count * MemoryLayout.size), + type: .vertex + ) + + let colorBuffer = allocator.newBuffer( + with: Data(bytes: colors, count: colors.count * MemoryLayout.size), + type: .vertex + ) + + let mesh = MDLMesh( + vertexBuffers: [vertexBuffer, colorBuffer], + vertexCount: vertices.count / 3, + descriptor: createParticleVertexDescriptor(), + submeshes: [] + ) + + mesh.name = "RF_Particles" + + return mesh + } + + private func createIsosurface(at signalLevel: Float) -> MDLMesh { + // Use marching cubes algorithm to create isosurface + let marchingCubes = MarchingCubesGenerator( + data: propagationData, + isoLevel: signalLevel, + resolution: gridResolution + ) + + return marchingCubes.generateMesh() + } + + // MARK: - Texture Creation + + private func createSignalTexture() async throws -> MDLTexture { + let textureSize = 1024 + + // Generate heatmap image + guard let heatmapImage = heatmapGenerator.generateHeatmapImage( + size: CGSize(width: textureSize, height: textureSize) + ) else { + throw IntegratorError.textureGenerationFailed + } + + // Convert to MDLTexture + guard let cgImage = heatmapImage.cgImage else { + throw IntegratorError.invalidImage + } + + let texture = MDLTexture( + cgImage: cgImage, + name: "RF_SignalTexture", + assetResolver: nil + ) + + return texture + } + + private func applyTextureToMesh(_ mesh: MDLMesh, texture: MDLTexture) { + let material = MDLMaterial(name: "RF_Material", scatteringFunction: MDLScatteringFunction()) + + let property = MDLMaterialProperty( + name: "baseColor", + semantic: .baseColor, + texture: texture + ) + + material.setProperty(property) + + // Set transparency + let opacityProperty = MDLMaterialProperty( + name: "opacity", + semantic: .opacity, + float: signalOpacity + ) + material.setProperty(opacityProperty) + + // Apply material to mesh + for submesh in mesh.submeshes as! [MDLSubmesh] { + submesh.material = material + } + } + + // MARK: - Scene Creation + + private func createBaseScene(from room: CapturedRoom) -> SCNScene { + let scene = SCNScene() + + // Add room geometry + addRoomGeometry(to: scene, room: room) + + // Add lighting + addLighting(to: scene) + + // Add camera + addCamera(to: scene) + + return scene + } + + private func addRoomGeometry(to scene: SCNScene, room: CapturedRoom) { + let roomNode = SCNNode() + roomNode.name = "Room" + + // Add walls + for wall in room.walls { + let wallNode = createWallNode(from: wall) + roomNode.addChildNode(wallNode) + } + + // Add floor + for floor in room.floors { + let floorNode = createFloorNode(from: floor) + roomNode.addChildNode(floorNode) + } + + // Add ceiling + for ceiling in room.ceilings { + let ceilingNode = createCeilingNode(from: ceiling) + roomNode.addChildNode(ceilingNode) + } + + // Add doors + for door in room.doors { + let doorNode = createDoorNode(from: door) + roomNode.addChildNode(doorNode) + } + + // Add windows + for window in room.windows { + let windowNode = createWindowNode(from: window) + roomNode.addChildNode(windowNode) + } + + scene.rootNode.addChildNode(roomNode) + } + + private func addRFVisualizationToScene(_ scene: SCNScene) { + let rfNode = SCNNode() + rfNode.name = "RF_Visualization" + + switch visualizationStyle { + case .planar: + addPlanarVisualizationToNode(rfNode) + case .volumetric: + addVolumetricVisualizationToNode(rfNode) + case .particles: + addParticleVisualizationToNode(rfNode) + case .isosurfaces: + addIsosurfaceVisualizationToNode(rfNode) + case .hybrid: + addHybridVisualizationToNode(rfNode) + } + + scene.rootNode.addChildNode(rfNode) + } + + private func addPlanarVisualizationToNode(_ node: SCNNode) { + // Create heatmap plane + let bounds = calculateRoomBounds() + let width = CGFloat(bounds.max.x - bounds.min.x) + let depth = CGFloat(bounds.max.z - bounds.min.z) + + let plane = SCNPlane(width: width, height: depth) + + // Generate and apply heatmap texture + if let heatmapImage = heatmapGenerator.generateHeatmapImage( + size: CGSize(width: 1024, height: 1024) + ) { + let material = SCNMaterial() + material.diffuse.contents = heatmapImage + material.transparency = CGFloat(signalOpacity) + material.isDoubleSided = true + material.writesToDepthBuffer = false + plane.materials = [material] + } + + let planeNode = SCNNode(geometry: plane) + planeNode.position = SCNVector3( + (bounds.min.x + bounds.max.x) / 2, + 0.01, + (bounds.min.z + bounds.max.z) / 2 + ) + planeNode.eulerAngles.x = -.pi / 2 + + node.addChildNode(planeNode) + } + + private func addVolumetricVisualizationToNode(_ node: SCNNode) { + // Create voxel visualization + for point in propagationData { + guard point.signalStrength > -85 else { continue } + + let voxel = SCNBox( + width: CGFloat(gridResolution), + height: CGFloat(gridResolution), + length: CGFloat(gridResolution), + chamferRadius: 0 + ) + + let material = SCNMaterial() + let color = signalStrengthToColor(point.signalStrength) + material.diffuse.contents = UIColor( + red: CGFloat(color.r), + green: CGFloat(color.g), + blue: CGFloat(color.b), + alpha: CGFloat(signalOpacity) + ) + material.transparency = CGFloat(signalOpacity) + voxel.materials = [material] + + let voxelNode = SCNNode(geometry: voxel) + voxelNode.position = SCNVector3(point.position) + + node.addChildNode(voxelNode) + } + } + + private func addParticleVisualizationToNode(_ node: SCNNode) { + let particleSystem = SCNParticleSystem() + + // Configure particle system + particleSystem.birthRate = 100 + particleSystem.particleLifeSpan = 5 + particleSystem.particleSize = 0.1 + particleSystem.particleColor = UIColor.white + particleSystem.particleColorVariation = SCNVector4(1, 1, 1, 0) + + // Custom particle positions based on propagation data + var particlePositions: [SCNVector3] = [] + for point in propagationData { + particlePositions.append(SCNVector3(point.position)) + } + + node.addParticleSystem(particleSystem) + } + + private func addIsosurfaceVisualizationToNode(_ node: SCNNode) { + let signalLevels: [Float] = [-30, -50, -70, -85] + let colors: [UIColor] = [.green, .yellow, .orange, .red] + + for (level, color) in zip(signalLevels, colors) { + let isosurfaceNode = createIsosurfaceNode(at: level, color: color) + node.addChildNode(isosurfaceNode) + } + } + + private func addHybridVisualizationToNode(_ node: SCNNode) { + addPlanarVisualizationToNode(node) + addIsosurfaceVisualizationToNode(node) + addAccessPointNodes(to: node) + } + + // MARK: - Helper Methods + + private func createWallNode(from surface: CapturedRoom.Surface) -> SCNNode { + let wall = SCNBox( + width: CGFloat(surface.dimensions.x), + height: CGFloat(surface.dimensions.y), + length: 0.1, + chamferRadius: 0 + ) + + let material = SCNMaterial() + material.diffuse.contents = UIColor.lightGray + wall.materials = [material] + + let node = SCNNode(geometry: wall) + node.position = SCNVector3(surface.transform.position) + node.orientation = SCNQuaternion(surface.transform.rotation) + + return node + } + + private func createFloorNode(from surface: CapturedRoom.Surface) -> SCNNode { + let floor = SCNPlane( + width: CGFloat(surface.dimensions.x), + height: CGFloat(surface.dimensions.y) + ) + + let material = SCNMaterial() + material.diffuse.contents = UIColor.darkGray + floor.materials = [material] + + let node = SCNNode(geometry: floor) + node.position = SCNVector3(surface.transform.position) + node.eulerAngles.x = -.pi / 2 + + return node + } + + private func createCeilingNode(from surface: CapturedRoom.Surface) -> SCNNode { + let ceiling = SCNPlane( + width: CGFloat(surface.dimensions.x), + height: CGFloat(surface.dimensions.y) + ) + + let material = SCNMaterial() + material.diffuse.contents = UIColor.white + ceiling.materials = [material] + + let node = SCNNode(geometry: ceiling) + node.position = SCNVector3(surface.transform.position) + node.eulerAngles.x = .pi / 2 + + return node + } + + private func createDoorNode(from surface: CapturedRoom.Surface) -> SCNNode { + let door = SCNBox( + width: CGFloat(surface.dimensions.x), + height: CGFloat(surface.dimensions.y), + length: 0.05, + chamferRadius: 0 + ) + + let material = SCNMaterial() + material.diffuse.contents = UIColor.brown + door.materials = [material] + + let node = SCNNode(geometry: door) + node.position = SCNVector3(surface.transform.position) + node.orientation = SCNQuaternion(surface.transform.rotation) + + return node + } + + private func createWindowNode(from surface: CapturedRoom.Surface) -> SCNNode { + let window = SCNBox( + width: CGFloat(surface.dimensions.x), + height: CGFloat(surface.dimensions.y), + length: 0.02, + chamferRadius: 0 + ) + + let material = SCNMaterial() + material.diffuse.contents = UIColor.cyan.withAlphaComponent(0.3) + material.transparency = 0.3 + window.materials = [material] + + let node = SCNNode(geometry: window) + node.position = SCNVector3(surface.transform.position) + node.orientation = SCNQuaternion(surface.transform.rotation) + + return node + } + + private func createIsosurfaceNode(at level: Float, color: UIColor) -> SCNNode { + // This would use marching cubes to create the isosurface geometry + // Simplified version for demonstration + let node = SCNNode() + node.name = "Isosurface_\(level)" + + // Add isosurface geometry here + + return node + } + + private func addAccessPointNodes(to node: SCNNode) { + // Add visual indicators for access points + // This would show the optimal AP placements from the propagation model + } + + private func addAccessPointIndicators(to asset: MDLAsset) { + // Add 3D models for access points + } + + private func addLighting(to scene: SCNScene) { + // Add ambient light + let ambientLight = SCNLight() + ambientLight.type = .ambient + ambientLight.intensity = 500 + let ambientNode = SCNNode() + ambientNode.light = ambientLight + scene.rootNode.addChildNode(ambientNode) + + // Add directional light + let directionalLight = SCNLight() + directionalLight.type = .directional + directionalLight.intensity = 1000 + let directionalNode = SCNNode() + directionalNode.light = directionalLight + directionalNode.eulerAngles = SCNVector3(-Float.pi/4, 0, 0) + scene.rootNode.addChildNode(directionalNode) + } + + private func addCamera(to scene: SCNScene) { + let camera = SCNCamera() + camera.fieldOfView = 60 + + let cameraNode = SCNNode() + cameraNode.camera = camera + cameraNode.position = SCNVector3(0, 5, 10) + cameraNode.look(at: SCNVector3(0, 0, 0)) + + scene.rootNode.addChildNode(cameraNode) + } + + private func calculateRoomBounds() -> (min: simd_float3, max: simd_float3) { + guard let room = capturedRoom else { + return (simd_float3(-5, 0, -5), simd_float3(5, 3, 5)) + } + + var minPoint = simd_float3(Float.infinity, Float.infinity, Float.infinity) + var maxPoint = simd_float3(-Float.infinity, -Float.infinity, -Float.infinity) + + for surface in room.walls + room.floors + room.ceilings { + let position = surface.transform.position + minPoint = simd_min(minPoint, position) + maxPoint = simd_max(maxPoint, position) + } + + return (minPoint, maxPoint) + } + + private func extractRoomsFromCapturedRoom(_ room: CapturedRoom) -> [RoomAnalyzer.IdentifiedRoom] { + // Convert CapturedRoom data to IdentifiedRoom format + // This is a simplified conversion + var rooms: [RoomAnalyzer.IdentifiedRoom] = [] + + // Extract wall points from surfaces + var wallPoints: [simd_float3] = [] + for wall in room.walls { + wallPoints.append(wall.transform.position) + } + + if !wallPoints.isEmpty { + let identifiedRoom = RoomAnalyzer.IdentifiedRoom( + type: .unknown, + wallPoints: wallPoints, + floorArea: calculateFloorArea(from: wallPoints), + objects: room.objects + ) + rooms.append(identifiedRoom) + } + + return rooms + } + + private func calculateFloorArea(from points: [simd_float3]) -> Float { + guard points.count >= 3 else { return 0 } + + // Calculate area using shoelace formula + var area: Float = 0 + for i in 0.. (r: Float, g: Float, b: Float) { + // Map signal strength to color + let normalized = (strength + 100) / 70 // Normalize -100 to -30 dBm to 0-1 + + if normalized < 0.25 { + // Red to orange + return (1.0, normalized * 4, 0.0) + } else if normalized < 0.5 { + // Orange to yellow + return (1.0, 1.0, (normalized - 0.25) * 4) + } else if normalized < 0.75 { + // Yellow to green + return (1.0 - (normalized - 0.5) * 4, 1.0, 0.0) + } else { + // Green + return (0.0, 1.0, 0.0) + } + } + + private func createVoxelVertexDescriptor() -> MDLVertexDescriptor { + let descriptor = MDLVertexDescriptor() + + // Position attribute + descriptor.attributes[0] = MDLVertexAttribute( + name: MDLVertexAttributePosition, + format: .float3, + offset: 0, + bufferIndex: 0 + ) + + // Normal attribute + descriptor.attributes[1] = MDLVertexAttribute( + name: MDLVertexAttributeNormal, + format: .float3, + offset: 0, + bufferIndex: 1 + ) + + // Color attribute + descriptor.attributes[2] = MDLVertexAttribute( + name: MDLVertexAttributeColor, + format: .float4, + offset: 0, + bufferIndex: 2 + ) + + // Layout + descriptor.layouts[0] = MDLVertexBufferLayout(stride: 12) + descriptor.layouts[1] = MDLVertexBufferLayout(stride: 12) + descriptor.layouts[2] = MDLVertexBufferLayout(stride: 16) + + return descriptor + } + + private func createParticleVertexDescriptor() -> MDLVertexDescriptor { + let descriptor = MDLVertexDescriptor() + + // Position attribute + descriptor.attributes[0] = MDLVertexAttribute( + name: MDLVertexAttributePosition, + format: .float3, + offset: 0, + bufferIndex: 0 + ) + + // Color attribute + descriptor.attributes[1] = MDLVertexAttribute( + name: MDLVertexAttributeColor, + format: .float4, + offset: 0, + bufferIndex: 1 + ) + + // Layout + descriptor.layouts[0] = MDLVertexBufferLayout(stride: 12) + descriptor.layouts[1] = MDLVertexBufferLayout(stride: 16) + + return descriptor + } + + // MARK: - Export Methods + + private func loadUSDZAsset(from url: URL) async throws -> MDLAsset { + return MDLAsset(url: url) + } + + private func exportUSDZ(asset: MDLAsset, to url: URL) async throws { + asset.export(to: url) + } + + private func exportSceneToUSDZ(scene: SCNScene, to url: URL) async throws { + scene.write(to: url, options: nil, delegate: nil, progressHandler: nil) + } + + // MARK: - Error Handling + + enum IntegratorError: Error { + case noCapturedRoom + case textureGenerationFailed + case invalidImage + case exportFailed + } +} + +// MARK: - Marching Cubes Generator +/// Generates isosurface meshes using marching cubes algorithm +class MarchingCubesGenerator { + private let data: [RFPropagationModel.PropagationPoint] + private let isoLevel: Float + private let resolution: Float + + init(data: [RFPropagationModel.PropagationPoint], isoLevel: Float, resolution: Float) { + self.data = data + self.isoLevel = isoLevel + self.resolution = resolution + } + + func generateMesh() -> MDLMesh { + // Simplified marching cubes implementation + // In a real implementation, this would use lookup tables and proper triangulation + + let allocator = MDLMeshBufferDataAllocator() + + // Create a simple sphere at the iso level for demonstration + let mesh = MDLMesh( + sphereWithExtent: vector3(1, 1, 1), + segments: vector2(32, 32), + inwardNormals: false, + geometryType: .triangles, + allocator: allocator + ) + + mesh.name = "Isosurface_\(isoLevel)" + + return mesh + } +} + +// MARK: - Extensions +extension SCNVector3 { + init(_ simdVector: simd_float3) { + self.init(simdVector.x, simdVector.y, simdVector.z) + } +} + +extension SCNQuaternion { + init(_ simdQuaternion: simd_quatf) { + self.init(simdQuaternion.imag.x, simdQuaternion.imag.y, simdQuaternion.imag.z, simdQuaternion.real) + } +} + +extension SCNNode { + func look(at target: SCNVector3) { + let constraint = SCNLookAtConstraint(target: SCNNode()) + constraint.target?.position = target + self.constraints = [constraint] + } +} \ No newline at end of file