iOS Development

ios – Swift – Detect Faucet on baby SCNNode not Mother or father

Hiya I’ve a easy interactive globe represented by dots. Each dot is a SCNNode and the globe can also be a SCNNode. The Whole globe node is the father or mother and the smaller dots inside the globe are youngsters. I need to detect faucets on the kid nodes. Once I strive doing so the faucet gestures are detected just for the father or mother node and due to this I can not decide which baby node is clicked. In the end I need to click on on a baby node (dot) and decide the SCNVector3 of the place clicked so I can convert to coordinates.

Ive been engaged on the operate touchesBegan however this picks up clicks on the father or mother earth node.

import Basis
import SceneKit
import CoreImage
import SwiftUI
import MapKit

public typealias GenericController = UIViewController
public typealias GenericColor = UIColor
public typealias GenericImage = UIImage

public class GlobeViewController: GenericController {
    //@Binding var showProf: Bool
    var nodePos: CGPoint? = nil
    public var earthNode: SCNNode!
    non-public var sceneView : SCNView!
    non-public var cameraNode: SCNNode!
    non-public var worldMapImage: CGImage {
        guard let picture = UIImage(named: "earth-dark")?.cgImage else {
            fatalError("Not discovered")
        }
        return picture
    }

    non-public lazy var imgData: CFData = {
        guard let imgData = worldMapImage.dataProvider?.information else { fatalError("Couldn't fetch information from world map picture.") }
        return imgData
    }()

    non-public lazy var worldMapWidth: Int = {
        return worldMapImage.width
    }()

    public var earthRadius: Double = 1.0 {
        didSet {
            if let earthNode = earthNode {
                earthNode.removeFromParentNode()
                setupGlobe()
            }
        }
    }
   
    public var dotSize: CGFloat = 0.005 {
        didSet {
            if dotSize != oldValue {
                setupDotGeometry()
            }
        }
    }
    
    public var enablesParticles: Bool = true {
        didSet {
            if enablesParticles {
                setupParticles()
            } else {
                sceneView.scene?.rootNode.removeAllParticleSystems()
            }
        }
    }
    
    public var particles: SCNParticleSystem? {
        didSet {
            if let particles = particles {
                sceneView.scene?.rootNode.removeAllParticleSystems()
                sceneView.scene?.rootNode.addParticleSystem(particles)
            }
        }
    }
    
    public var earthColor: Colour = .earthColor {
        didSet {
            if let earthNode = earthNode {
                earthNode.geometry?.firstMaterial?.diffuse.contents = earthColor
            }
        }
    }
    
    public var glowColor: Colour = .earthGlow {
        didSet {
            if let earthNode = earthNode {
                earthNode.geometry?.firstMaterial?.emission.contents = glowColor
            }
        }
    }
    
    public var reflectionColor: Colour = .earthReflection {
        didSet {
            if let earthNode = earthNode {
                earthNode.geometry?.firstMaterial?.emission.contents = glowColor
            }
        }
    }

    public var glowShininess: CGFloat = 1.0 {
        didSet {
            if let earthNode = earthNode {
                earthNode.geometry?.firstMaterial?.shininess = glowShininess
            }
        }
    }

    non-public var dotRadius: CGFloat {
        if dotSize > 0 {
             return dotSize
        }
        else {
            return 0.01 * CGFloat(earthRadius) / 1.0
        }
    }

    non-public var dotCount = 50000
    
    public init(earthRadius: Double) {//, showProf: Binding<Bool>
        self.earthRadius = earthRadius
        //self._showProf = showProf
        tremendous.init(nibName: nil, bundle: nil)
    }
    
    public init(earthRadius: Double, dotCount: Int) {//, showProf: Binding<Bool>
        self.earthRadius = earthRadius
        self.dotCount = dotCount
        //self._showProf = showProf
        tremendous.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been carried out")
    }

    public override func touchesBegan(_ touches: Set<UITouch>, with occasion: UIEvent?) {
        guard let contact = touches.first, contact.view == self.sceneView else {
            return
        }

        let touchLocation = contact.location(in: sceneView)
        let hitTestResults = sceneView.hitTest(touchLocation, choices: nil)
        
        let touchLocation3D = sceneView.unprojectPoint(SCNVector3(Float(touchLocation.x), Float(touchLocation.y), 0.0))
        
        print(touchLocation3D)
        
        if let tappedNode = hitTestResults.first?.node {
            print(tappedNode)
            // Deal with the tapped node
            if tappedNode.identify == "NewYorkDot" {
                // That is the New York dot, carry out your motion right here
                print("Tapped on New York! Place: (tappedNode.place)")
            } else if tappedNode.identify == "RegularDot" {
                // Deal with different nodes if wanted
                print("Tapped on an everyday dot. Place: (tappedNode.place)")
            }
        }
    }

    public override func viewDidLoad() {
        tremendous.viewDidLoad()
        setupScene()
        
        setupParticles()
        
        setupCamera()
        setupGlobe()
        
        setupDotGeometry()
    }
    
    non-public func setupScene() {
        let scene = SCNScene()
        sceneView = SCNView(body: view.body)
    
        sceneView.scene = scene
        
//        let tapGesture = UITapGestureRecognizer(goal: self, motion: #selector(handleTap(_:)))
//        sceneView.addGestureRecognizer(tapGesture)

        sceneView.showsStatistics = true
        sceneView.backgroundColor = .clear
        sceneView.allowsCameraControl = true
        sceneView.isUserInteractionEnabled = true
        
        self.view.addSubview(sceneView)
    }
        
    non-public func setupParticles() {
        guard let stars = SCNParticleSystem(named: "StarsParticles.scnp", inDirectory: nil) else { return }
        stars.isLightingEnabled = false
                
        if sceneView != nil {
            sceneView.scene?.rootNode.addParticleSystem(stars)
        }
    }
    
    non-public func setupCamera() {
        self.cameraNode = SCNNode()
        cameraNode.digital camera = SCNCamera()
        cameraNode.place = SCNVector3(x: 0, y: 0, z: 5)
        
        sceneView.scene?.rootNode.addChildNode(cameraNode)
        
//        let textureMap = generateTextureMap(dots: dotCount, sphereRadius: CGFloat(earthRadius))
//        let newYork = CLLocationCoordinate2D(latitude: 44.0682, longitude: -121.3153)
//        let newYorkDot = closestDotPosition(to: newYork, in: textureMap)
//        var place: SCNVector3?
//
//        if let pos = textureMap.first(the place: { $0.x == newYorkDot.x && $0.y == newYorkDot.y }) {
//            place = pos.place
//        }

//        Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false) { _ in
//            if let pos = place {
//                self.centerCameraOnDot(dotPosition: pos)
//            }
//        }
    }

    non-public func setupGlobe() {
        self.earthNode = EarthNode(radius: earthRadius, earthColor: earthColor, earthGlow: glowColor, earthReflection: reflectionColor)
        sceneView.scene?.rootNode.addChildNode(earthNode)
    }

    non-public func setupDotGeometry() {
        let textureMap = generateTextureMap(dots: dotCount, sphereRadius: CGFloat(earthRadius))

        let newYork = CLLocationCoordinate2D(latitude: 44.0682, longitude: -121.3153)
        let newYorkDot = closestDotPosition(to: newYork, in: textureMap)

        let dotColor = GenericColor(white: 1, alpha: 1)
        let oceanColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
        let highlightColor = GenericColor(cgColor: UIColor.systemRed.cgColor)
        
        // threshold to find out if the pixel within the earth-dark.jpg represents terrain (0.03 represents rgb(7.65,7.65,7.65), which is nearly black)
        let threshold: CGFloat = 0.03
        
        let dotGeometry = SCNSphere(radius: dotRadius)
        dotGeometry.firstMaterial?.diffuse.contents = dotColor
        dotGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.fixed
        
        let highlightGeometry = SCNSphere(radius: dotRadius)
        highlightGeometry.firstMaterial?.diffuse.contents = highlightColor
        highlightGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.fixed
        
        let oceanGeometry = SCNSphere(radius: dotRadius)
        oceanGeometry.firstMaterial?.diffuse.contents = oceanColor
        oceanGeometry.firstMaterial?.lightingModel = SCNMaterial.LightingModel.fixed
        
        var positions = [SCNVector3]()
        var dotNodes = [SCNNode]()
        
        var highlightedNode: SCNNode? = nil
        
        for i in 0...textureMap.rely - 1 {
            let u = textureMap[i].x
            let v = textureMap[i].y
            
            let pixelColor = self.getPixelColor(x: Int(u), y: Int(v))
            let isHighlight = u == newYorkDot.x && v == newYorkDot.y
            
            if (isHighlight) {
                let dotNode = SCNNode(geometry: highlightGeometry)
                dotNode.identify = "NewYorkDot"
                dotNode.place = textureMap[i].place
                positions.append(dotNode.place)
                dotNodes.append(dotNode)
                
                print("myloc (textureMap[i].place)")
                
                highlightedNode = dotNode
            } else if (pixelColor.pink < threshold && pixelColor.inexperienced < threshold && pixelColor.blue < threshold) {
                let dotNode = SCNNode(geometry: dotGeometry)
                dotNode.identify = "Different"
                dotNode.place = textureMap[i].place
                positions.append(dotNode.place)
                dotNodes.append(dotNode)
            }
        }
        
        DispatchQueue.major.async {
            let dotPositions = positions as NSArray
            let dotIndices = NSArray()
            let supply = SCNGeometrySource(vertices: dotPositions as! [SCNVector3])
            let ingredient = SCNGeometryElement(indices: dotIndices as! [Int32], primitiveType: .level)
            
            let pointCloud = SCNGeometry(sources: [source], components: [element])
            
            let pointCloudNode = SCNNode(geometry: pointCloud)
            for dotNode in dotNodes {
                pointCloudNode.addChildNode(dotNode)
            }
     
            self.sceneView.scene?.rootNode.addChildNode(pointCloudNode)
            
//            DispatchQueue.major.asyncAfter(deadline: .now() + 3) {
//                if let highlightedNode = highlightedNode {
//                    self.alignPointToPositiveZ(for: pointCloudNode, targetPoint: highlightedNode.place)
//                }
//            }
        }
    }
    
    func centerCameraOnDot(dotPosition: SCNVector3) {
        let targetPhi = atan2(dotPosition.x, dotPosition.z)
        let targetTheta = asin(dotPosition.y / dotPosition.size())

        // Convert spherical coordinates again to Cartesian
        let newX = 1 * sin(targetTheta) * sin(targetPhi)
        let newY = 1 * cos(targetTheta)
        let newZ = 1 * sin(targetTheta) * cos(targetPhi)

        let fixedDistance: Float = 6.0
        let newCameraPosition = SCNVector3(newX, newY, newZ).normalized().scaled(to: fixedDistance)

        let moveAction = SCNAction.transfer(to: newCameraPosition, length: 0.8)

        // Create extra actions as wanted
        //let rotateAction = SCNAction.rotateBy(x: 0, y: 0, z: 0, length: 0.5)

        // Create an array of actions
        let sequenceAction = SCNAction.sequence([moveAction])

        // Run the sequence motion on the cameraNode
        cameraNode.runAction(sequenceAction)
    }

    func alignPointToPositiveZ(for sphereNode: SCNNode, targetPoint: SCNVector3) {
        
        // Compute normalized vector from Earth's heart to the goal level
        let targetDirection = targetPoint.normalized()
        
        // Compute quaternion rotation
        let up = SCNVector3(0, 0, 1)
        let rotationQuaternion = SCNQuaternion.fromVectorRotate(from: up, to: targetDirection)
        
        sphereNode.orientation = rotationQuaternion
        
    }
    
    typealias MapDot = (place: SCNVector3, x: Int, y: Int)
    
    non-public func generateTextureMap(dots: Int, sphereRadius: CGFloat) -> [MapDot] {

        let phi = Double.pi * (sqrt(5) - 1)
        var positions = [MapDot]()

        for i in 0..<dots {

            let y = 1.0 - (Double(i) / Double(dots - 1)) * 2.0 // y is 1 to -1
            let radiusY = sqrt(1 - y * y)
            let theta = phi * Double(i) // Golden angle increment
            
            let x = cos(theta) * radiusY
            let z = sin(theta) * radiusY

            let vector = SCNVector3(x: Float(sphereRadius * x),
                                    y: Float(sphereRadius * y),
                                    z: Float(sphereRadius * z))

            let pixel = equirectangularProjection(level: Point3D(x: x, y: y, z: z),
                                                  imageWidth: 2048,
                                                  imageHeight: 1024)

            let place = MapDot(place: vector, x: pixel.u, y: pixel.v)
            positions.append(place)
        }
        return positions
    }
    
    struct Point3D {
        let x: Double
        let y: Double
        let z: Double
    }

    struct Pixel {
        let u: Int
        let v: Int
    }

    func equirectangularProjection(level: Point3D, imageWidth: Int, imageHeight: Int) -> Pixel {
        let theta = asin(level.y)
        let phi = atan2(level.x, level.z)
        
        let u = Double(imageWidth) / (2.0 * .pi) * (phi + .pi)
        let v = Double(imageHeight) / .pi * (.pi / 2.0 - theta)
        
        return Pixel(u: Int(u), v: Int(v))
    }
    
    non-public func distanceBetweenPoints(x1: Int, y1: Int, x2: Int, y2: Int) -> Double {
        let dx = Double(x2 - x1)
        let dy = Double(y2 - y1)
        return sqrt(dx * dx + dy * dy)
    }
    
    non-public func closestDotPosition(to coordinate: CLLocationCoordinate2D, in positions: [(position: SCNVector3, x: Int, y: Int)]) -> (x: Int, y: Int) {
        let pixelPositionDouble = getEquirectangularProjectionPosition(for: coordinate)
        let pixelPosition = (x: Int(pixelPositionDouble.x), y: Int(pixelPositionDouble.y))

                
        let nearestDotPosition = positions.min { p1, p2 in
            distanceBetweenPoints(x1: pixelPosition.x, y1: pixelPosition.y, x2: p1.x, y2: p1.y) <
                distanceBetweenPoints(x1: pixelPosition.x, y1: pixelPosition.y, x2: p2.x, y2: p2.y)
        }
        
        return (x: nearestDotPosition?.x ?? 0, y: nearestDotPosition?.y ?? 0)
    }
    
    /// Convert a coordinate to an (x, y) coordinate on the world map picture
    non-public func getEquirectangularProjectionPosition(
        for coordinate: CLLocationCoordinate2D
    ) -> CGPoint {
        let imageHeight = CGFloat(worldMapImage.top)
        let imageWidth = CGFloat(worldMapImage.width)

        // Normalize longitude to [0, 360). Longitude in MapKit is [-180, 180)
        let normalizedLong = coordinate.longitude + 180
        // Calculate x and y positions
        let xPosition = (normalizedLong / 360) * imageWidth
        // Note: Latitude starts from top, hence the `-` sign
        let yPosition = (-(coordinate.latitude - 90) / 180) * imageHeight
        return CGPoint(x: xPosition, y: yPosition)
    }

    private func getPixelColor(x: Int, y: Int) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(imgData)
        let pixelInfo: Int = ((worldMapWidth * y) + x) * 4

        let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
        let g = CGFloat(information[pixelInfo + 1]) / CGFloat(255.0)
        let b = CGFloat(information[pixelInfo + 2]) / CGFloat(255.0)
        let a = CGFloat(information[pixelInfo + 3]) / CGFloat(255.0)

        return (r, g, b, a)
    }
}

non-public extension Colour {
    static var earthColor: Colour {
        return Colour(pink: 0.227, inexperienced: 0.133, blue: 0.541)
    }
    
    static var earthGlow: Colour {
        Colour(pink: 0.133, inexperienced: 0.0, blue: 0.22)
    }
    
    static var earthReflection: Colour {
        Colour(pink: 0.227, inexperienced: 0.133, blue: 0.541)
    }
}

extension SCNVector3 {
    func size() -> Float {
        return sqrtf(x*x + y*y + z*z)
    }

    func normalized() -> SCNVector3 {
        let len = size()
        return SCNVector3(x: x/len, y: y/len, z: z/len)
    }

    func scaled(to size: Float) -> SCNVector3 {
        return SCNVector3(x: x * size, y: y * size, z: z * size)
    }

    func dot(_ v: SCNVector3) -> Float {
        return x * v.x + y * v.y + z * v.z
    }

    func cross(_ v: SCNVector3) -> SCNVector3 {
        return SCNVector3(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x)
    }
}

extension SCNQuaternion {
    static func fromVectorRotate(from begin: SCNVector3, to finish: SCNVector3) -> SCNQuaternion {
        let c = begin.cross(finish)
        let d = begin.dot(finish)
        let s = sqrt((1 + d) * 2)
        let invs = 1 / s

        return SCNQuaternion(x: c.x * invs, y: c.y * invs, z: c.z * invs, w: s * 0.5)
    }
}

typealias GenericControllerRepresentable = UIViewControllerRepresentable

@accessible(iOS 13.0, *)
non-public struct GlobeViewControllerRepresentable: GenericControllerRepresentable {
    var particles: SCNParticleSystem? = nil
    //@Binding public var showProf: Bool

    func makeUIViewController(context: Context) -> GlobeViewController {
        let globeController = GlobeViewController(earthRadius: 1.0)//, showProf: $showProf
        updateGlobeController(globeController)
        return globeController
    }
    
    func updateUIViewController(_ uiViewController: GlobeViewController, context: Context) {
        updateGlobeController(uiViewController)
    }
    
    non-public func updateGlobeController(_ globeController: GlobeViewController) {
        globeController.dotSize = CGFloat(0.005)
              
        globeController.enablesParticles = true
        
        if let particles = particles {
            globeController.particles = particles
        }
    }
}

@accessible(iOS 13.0, *)
public struct GlobeView: View {
    //@Binding public var showProf: Bool
    
    public var physique: some View {
        GlobeViewControllerRepresentable()//showProf: $showProf
    }
}

Credit: www.ismmailgsm.com

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button