From e444218fbe3e3cea12f4ff90f684a36c4bd889ef Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Thu, 25 Nov 2021 11:59:00 -0800 Subject: [PATCH 1/3] update all styling to 4 space tabs --- .../FocusEntity/FocusEntity+Alignment.swift | 284 ++++----- Sources/FocusEntity/FocusEntity+Classic.swift | 190 +++--- Sources/FocusEntity/FocusEntity+Colored.swift | 47 +- Sources/FocusEntity/FocusEntity+Segment.swift | 270 ++++----- Sources/FocusEntity/FocusEntity.swift | 540 +++++++++--------- .../FocusEntity/FocusEntityComponent.swift | 116 ++-- Sources/FocusEntity/float4x4+Extension.swift | 36 +- 7 files changed, 741 insertions(+), 742 deletions(-) diff --git a/Sources/FocusEntity/FocusEntity+Alignment.swift b/Sources/FocusEntity/FocusEntity+Alignment.swift index 32173d5..5eda5f5 100644 --- a/Sources/FocusEntity/FocusEntity+Alignment.swift +++ b/Sources/FocusEntity/FocusEntity+Alignment.swift @@ -13,162 +13,162 @@ import Combine extension FocusEntity { - // MARK: Helper Methods - - /// Update the position of the focus square. - internal func updatePosition() { - // Average using several most recent positions. - recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10)) - - // Move to average of recent positions to avoid jitter. - let average = recentFocusEntityPositions.reduce( - SIMD3.zero, { $0 + $1 } - ) / Float(recentFocusEntityPositions.count) - self.position = average - } - - /// Update the transform of the focus square to be aligned with the camera. - internal func updateTransform(raycastResult: ARRaycastResult) { - self.updatePosition() - - if state != .initializing { - updateAlignment(for: raycastResult) + // MARK: Helper Methods + + /// Update the position of the focus square. + internal func updatePosition() { + // Average using several most recent positions. + recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10)) + + // Move to average of recent positions to avoid jitter. + let average = recentFocusEntityPositions.reduce( + SIMD3.zero, { $0 + $1 } + ) / Float(recentFocusEntityPositions.count) + self.position = average } - } - - internal func updateAlignment(for raycastResult: ARRaycastResult) { - - var targetAlignment = raycastResult.worldTransform.orientation - - // Determine current alignment - var alignment: ARPlaneAnchor.Alignment? - if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor { - alignment = planeAnchor.alignment - // Catching case when looking at ceiling - if targetAlignment.act([0, 1, 0]).y < -0.9 { - targetAlignment *= simd_quatf(angle: .pi, axis: [0, 1, 0]) - } - } else if raycastResult.targetAlignment == .horizontal { - alignment = .horizontal - } else if raycastResult.targetAlignment == .vertical { - alignment = .vertical + + /// Update the transform of the focus square to be aligned with the camera. + internal func updateTransform(raycastResult: ARRaycastResult) { + self.updatePosition() + + if state != .initializing { + updateAlignment(for: raycastResult) + } } - // add to list of recent alignments - if alignment != nil { - self.recentFocusEntityAlignments.append(alignment!) + internal func updateAlignment(for raycastResult: ARRaycastResult) { + + var targetAlignment = raycastResult.worldTransform.orientation + + // Determine current alignment + var alignment: ARPlaneAnchor.Alignment? + if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor { + alignment = planeAnchor.alignment + // Catching case when looking at ceiling + if targetAlignment.act([0, 1, 0]).y < -0.9 { + targetAlignment *= simd_quatf(angle: .pi, axis: [0, 1, 0]) + } + } else if raycastResult.targetAlignment == .horizontal { + alignment = .horizontal + } else if raycastResult.targetAlignment == .vertical { + alignment = .vertical + } + + // add to list of recent alignments + if alignment != nil { + self.recentFocusEntityAlignments.append(alignment!) + } + + // Average using several most recent alignments. + self.recentFocusEntityAlignments = Array(self.recentFocusEntityAlignments.suffix(20)) + + let alignCount = self.recentFocusEntityAlignments.count + let horizontalHistory = recentFocusEntityAlignments.filter({ $0 == .horizontal }).count + let verticalHistory = recentFocusEntityAlignments.filter({ $0 == .vertical }).count + + // Alignment is same as most of the history - change it + if alignment == .horizontal && horizontalHistory > alignCount * 3/4 || + alignment == .vertical && verticalHistory > alignCount / 2 || + raycastResult.anchor is ARPlaneAnchor { + if alignment != self.currentAlignment || + (alignment == .vertical && self.shouldContinueAlignAnim(to: targetAlignment) + ) { + isChangingAlignment = true + self.currentAlignment = alignment + } + } else { + // Alignment is different than most of the history - ignore it + return + } + + // Change the focus entity's alignment + if isChangingAlignment { + // Uses interpolation. + // Needs to be called on every frame that the animation is desired, Not just the first frame. + performAlignmentAnimation(to: targetAlignment) + } else { + orientation = targetAlignment + } } - // Average using several most recent alignments. - self.recentFocusEntityAlignments = Array(self.recentFocusEntityAlignments.suffix(20)) + internal func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float { + // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal + var normalized = angle + while abs(normalized - ref) > .pi / 4 { + if angle > ref { + normalized -= .pi / 2 + } else { + normalized += .pi / 2 + } + } + return normalized + } - let alignCount = self.recentFocusEntityAlignments.count - let horizontalHistory = recentFocusEntityAlignments.filter({ $0 == .horizontal }).count - let verticalHistory = recentFocusEntityAlignments.filter({ $0 == .vertical }).count + internal func getCamVector() -> (position: SIMD3, direciton: SIMD3)? { + guard let camTransform = self.arView?.cameraTransform else { + return nil + } + let camDirection = camTransform.matrix.columns.2 + return (camTransform.translation, -[camDirection.x, camDirection.y, camDirection.z]) + } - // Alignment is same as most of the history - change it - if alignment == .horizontal && horizontalHistory > alignCount * 3/4 || - alignment == .vertical && verticalHistory > alignCount / 2 || - raycastResult.anchor is ARPlaneAnchor { - if alignment != self.currentAlignment || - (alignment == .vertical && self.shouldContinueAlignAnim(to: targetAlignment) + /// - Parameters: + /// - Returns: ARRaycastResult if an existing plane geometry or an estimated plane are found, otherwise nil. + internal func smartRaycast() -> ARRaycastResult? { + // Perform the hit test. + guard let (camPos, camDir) = self.getCamVector() else { + return nil + } + let rcQuery = ARRaycastQuery( + origin: camPos, direction: camDir, + allowing: self.allowedRaycast, alignment: .any + ) + let results = self.arView?.session.raycast(rcQuery) ?? [] + + // 1. Check for a result on an existing plane using geometry. + if let existingPlaneUsingGeometryResult = results.first( + where: { $0.target == .existingPlaneGeometry } ) { - isChangingAlignment = true - self.currentAlignment = alignment - } - } else { - // Alignment is different than most of the history - ignore it - return - } + return existingPlaneUsingGeometryResult + } - // Change the focus entity's alignment - if isChangingAlignment { - // Uses interpolation. - // Needs to be called on every frame that the animation is desired, Not just the first frame. - performAlignmentAnimation(to: targetAlignment) - } else { - orientation = targetAlignment - } - } - - internal func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float { - // Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal - var normalized = angle - while abs(normalized - ref) > .pi / 4 { - if angle > ref { - normalized -= .pi / 2 - } else { - normalized += .pi / 2 - } + // 2. As a fallback, check for a result on estimated planes. + return results.first(where: { $0.target == .estimatedPlane }) } - return normalized - } - internal func getCamVector() -> (position: SIMD3, direciton: SIMD3)? { - guard let camTransform = self.arView?.cameraTransform else { - return nil - } - let camDirection = camTransform.matrix.columns.2 - return (camTransform.translation, -[camDirection.x, camDirection.y, camDirection.z]) - } - - /// - Parameters: - /// - Returns: ARRaycastResult if an existing plane geometry or an estimated plane are found, otherwise nil. - internal func smartRaycast() -> ARRaycastResult? { - // Perform the hit test. - guard let (camPos, camDir) = self.getCamVector() else { - return nil - } - let rcQuery = ARRaycastQuery( - origin: camPos, direction: camDir, - allowing: self.allowedRaycast, alignment: .any - ) - let results = self.arView?.session.raycast(rcQuery) ?? [] - - // 1. Check for a result on an existing plane using geometry. - if let existingPlaneUsingGeometryResult = results.first( - where: { $0.target == .existingPlaneGeometry } - ) { - return existingPlaneUsingGeometryResult + /// Uses interpolation between orientations to create a smooth `easeOut` orientation adjustment animation. + internal func performAlignmentAnimation(to newOrientation: simd_quatf) { + // Interpolate between current and target orientations. + orientation = simd_slerp(orientation, newOrientation, 0.15) + // This length creates a normalized vector (of length 1) with all 3 components being equal. + self.isChangingAlignment = self.shouldContinueAlignAnim(to: newOrientation) } - // 2. As a fallback, check for a result on estimated planes. - return results.first(where: { $0.target == .estimatedPlane }) - } + func shouldContinueAlignAnim(to newOrientation: simd_quatf) -> Bool { + let testVector = simd_float3(repeating: 1 / sqrtf(3)) + let point1 = orientation.act(testVector) + let point2 = newOrientation.act(testVector) + let vectorsDot = simd_dot(point1, point2) + // Stop interpolating when the rotations are close enough to each other. + return vectorsDot < 0.999 + } - /// Uses interpolation between orientations to create a smooth `easeOut` orientation adjustment animation. - internal func performAlignmentAnimation(to newOrientation: simd_quatf) { - // Interpolate between current and target orientations. - orientation = simd_slerp(orientation, newOrientation, 0.15) - // This length creates a normalized vector (of length 1) with all 3 components being equal. - self.isChangingAlignment = self.shouldContinueAlignAnim(to: newOrientation) - } - - func shouldContinueAlignAnim(to newOrientation: simd_quatf) -> Bool { - let testVector = simd_float3(repeating: 1 / sqrtf(3)) - let point1 = orientation.act(testVector) - let point2 = newOrientation.act(testVector) - let vectorsDot = simd_dot(point1, point2) - // Stop interpolating when the rotations are close enough to each other. - return vectorsDot < 0.999 - } - - /** - Reduce visual size change with distance by scaling up when close and down when far away. - - These adjustments result in a scale of 1.0x for a distance of 0.7 m or less - (estimated distance when looking at a table), and a scale of 1.2x - for a distance 1.5 m distance (estimated distance when looking at the floor). - */ - internal func scaleBasedOnDistance(camera: ARCamera?) -> Float { - guard let camera = camera else { return 1.0 } - - let distanceFromCamera = simd_length(self.convert(position: .zero, to: nil) - camera.transform.translation) - if distanceFromCamera < 0.7 { - return distanceFromCamera / 0.7 - } else { - return 0.25 * distanceFromCamera + 0.825 + /** + Reduce visual size change with distance by scaling up when close and down when far away. + + These adjustments result in a scale of 1.0x for a distance of 0.7 m or less + (estimated distance when looking at a table), and a scale of 1.2x + for a distance 1.5 m distance (estimated distance when looking at the floor). + */ + internal func scaleBasedOnDistance(camera: ARCamera?) -> Float { + guard let camera = camera else { return 1.0 } + + let distanceFromCamera = simd_length(self.convert(position: .zero, to: nil) - camera.transform.translation) + if distanceFromCamera < 0.7 { + return distanceFromCamera / 0.7 + } else { + return 0.25 * distanceFromCamera + 0.825 + } } - } } #endif diff --git a/Sources/FocusEntity/FocusEntity+Classic.swift b/Sources/FocusEntity/FocusEntity+Classic.swift index 35bbb90..cb64ce3 100644 --- a/Sources/FocusEntity/FocusEntity+Classic.swift +++ b/Sources/FocusEntity/FocusEntity+Classic.swift @@ -12,115 +12,105 @@ import RealityKit /// An extension of FocusEntity holding the methods for the "classic" style. internal extension FocusEntity { - // MARK: - Configuration Properties - - /// Original size of the focus square in meters. Not currently customizable - static let size: Float = 0.17 - - /// Thickness of the focus square lines in meters. Not currently customizable - static let thickness: Float = 0.018 - - /// Scale factor for the focus square when it is closed, w.r.t. the original size. - static let scaleForClosedSquare: Float = 0.97 - - /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). -// static let sideLengthForOpenSegments: CGFloat = 0.2 - - /// Duration of the open/close animation. Not currently used. - static let animationDuration = 0.7 - - /// Color of the focus square fill. Not currently used. -// static var fillColor = #colorLiteral(red: 1, green: 0.9254901961, blue: 0.4117647059, alpha: 1) - - /// Indicates whether the segments of the focus square are disconnected. -// private var isOpen = true - - /// List of the segments in the focus square. - - // MARK: - Initialization - - func setupClassic(_ classicStyle: ClassicStyle) { -// opacity = 0.0 - /* - The focus square consists of eight segments as follows, which can be individually animated. - - s0 s1 - _ _ - s2 | | s3 - - s4 | | s5 - - - - s6 s7 - */ - - let segCorners: [(Corner, Alignment)] = [ - (.topLeft, .horizontal), (.topRight, .horizontal), - (.topLeft, .vertical), (.topRight, .vertical), - (.bottomLeft, .vertical), (.bottomRight, .vertical), - (.bottomLeft, .horizontal), (.bottomRight, .horizontal) - ] - self.segments = segCorners.enumerated().map { (index, cornerAlign) -> Segment in - Segment( - name: "s\(index)", - corner: cornerAlign.0, - alignment: cornerAlign.1, - color: classicStyle.color - ) - } - - let sl: Float = 0.5 // segment length - let c: Float = FocusEntity.thickness / 2 // correction to align lines perfectly - segments[0].position += [-(sl / 2 - c), 0, -(sl - c)] - segments[1].position += [sl / 2 - c, 0, -(sl - c)] - segments[2].position += [-sl, 0, -sl / 2] - segments[3].position += [sl, 0, -sl / 2] - segments[4].position += [-sl, 0, sl / 2] - segments[5].position += [sl, 0, sl / 2] - segments[6].position += [-(sl / 2 - c), 0, sl - c] - segments[7].position += [sl / 2 - c, 0, sl - c] - - for segment in segments { - self.positioningEntity.addChild(segment) - segment.open() + // MARK: - Configuration Properties + + /// Original size of the focus square in meters. Not currently customizable + static let size: Float = 0.17 + + /// Thickness of the focus square lines in meters. Not currently customizable + static let thickness: Float = 0.018 + + /// Scale factor for the focus square when it is closed, w.r.t. the original size. + static let scaleForClosedSquare: Float = 0.97 + + /// Duration of the open/close animation. Not currently used. + static let animationDuration = 0.7 + + /// List of the segments in the focus square. + + // MARK: - Initialization + + func setupClassic(_ classicStyle: ClassicStyle) { + // opacity = 0.0 + /* + The focus square consists of eight segments as follows, which can be individually animated. + + s0 s1 + _ _ + s2 | | s3 + + s4 | | s5 + - - + s6 s7 + */ + + let segCorners: [(Corner, Alignment)] = [ + (.topLeft, .horizontal), (.topRight, .horizontal), + (.topLeft, .vertical), (.topRight, .vertical), + (.bottomLeft, .vertical), (.bottomRight, .vertical), + (.bottomLeft, .horizontal), (.bottomRight, .horizontal) + ] + self.segments = segCorners.enumerated().map { (index, cornerAlign) -> Segment in + Segment( + name: "s\(index)", + corner: cornerAlign.0, + alignment: cornerAlign.1, + color: classicStyle.color + ) + } + + let sl: Float = 0.5 // segment length + let c: Float = FocusEntity.thickness / 2 // correction to align lines perfectly + segments[0].position += [-(sl / 2 - c), 0, -(sl - c)] + segments[1].position += [sl / 2 - c, 0, -(sl - c)] + segments[2].position += [-sl, 0, -sl / 2] + segments[3].position += [sl, 0, -sl / 2] + segments[4].position += [-sl, 0, sl / 2] + segments[5].position += [sl, 0, sl / 2] + segments[6].position += [-(sl / 2 - c), 0, sl - c] + segments[7].position += [sl / 2 - c, 0, sl - c] + + for segment in segments { + self.positioningEntity.addChild(segment) + segment.open() + } + + self.positioningEntity.scale = SIMD3(repeating: FocusEntity.size * FocusEntity.scaleForClosedSquare) + + // Always render focus square on top of other content. +// self.displayNodeHierarchyOnTop(true) } -// self.positioningEntity.addChild(fillPlane) - self.positioningEntity.scale = SIMD3(repeating: FocusEntity.size * FocusEntity.scaleForClosedSquare) + // MARK: Animations - // Always render focus square on top of other content. -// self.displayNodeHierarchyOnTop(true) - } + func offPlaneAniation() { + // Open animation + guard !isOpen else { + return + } + isOpen = true - // MARK: Animations - - func offPlaneAniation() { - // Open animation - guard !isOpen else { - return + for segment in segments { + segment.open() + } + positioningEntity.scale = .init(repeating: FocusEntity.size) } - isOpen = true - for segment in segments { - segment.open() - } - positioningEntity.scale = .init(repeating: FocusEntity.size) - } + func onPlaneAnimation(newPlane: Bool = false) { + guard isOpen else { + return + } + self.isOpen = false - func onPlaneAnimation(newPlane: Bool = false) { - guard isOpen else { - return - } - self.isOpen = false - - // Close animation - for segment in self.segments { - segment.close() - } + // Close animation + for segment in self.segments { + segment.close() + } - if newPlane { - // New plane animation not implemented + if newPlane { + // New plane animation not implemented + } } - } } #endif diff --git a/Sources/FocusEntity/FocusEntity+Colored.swift b/Sources/FocusEntity/FocusEntity+Colored.swift index 322587b..595b198 100644 --- a/Sources/FocusEntity/FocusEntity+Colored.swift +++ b/Sources/FocusEntity/FocusEntity+Colored.swift @@ -12,25 +12,34 @@ import RealityKit /// An extension of FocusEntity holding the methods for the "colored" style. public extension FocusEntity { - internal func coloredStateChanged() { - guard let coloredStyle = self.focus.coloredStyle else { - return + internal func coloredStateChanged() { + guard let coloredStyle = self.focus.coloredStyle else { + return + } + var endColor: MaterialColorParameter + if self.state == .initializing { + endColor = coloredStyle.nonTrackingColor + } else { + endColor = self.onPlane ? coloredStyle.onColor : coloredStyle.offColor + } + if self.fillPlane?.model?.materials.count == 0 { + self.fillPlane?.model?.materials = [SimpleMaterial()] + } + var modelMaterial = UnlitMaterial(color: .clear) + if #available(iOS 15, *) { + switch endColor { + case .color(let uikitColour): + modelMaterial.color = .init(tint: uikitColour, texture: nil) + case .texture(let tex): + modelMaterial.color = .init(tint: .white, texture: .init(tex)) + @unknown default: break + } + } else { + modelMaterial.baseColor = endColor + // Necessary for transparency. + modelMaterial.tintColor = Material.Color.white.withAlphaComponent(0.995) + } + self.fillPlane?.model?.materials[0] = modelMaterial } - var endColor: MaterialColorParameter - if self.state == .initializing { - endColor = coloredStyle.nonTrackingColor - } else { - endColor = self.onPlane ? coloredStyle.onColor : coloredStyle.offColor - } - if self.fillPlane?.model?.materials.count == 0 { - self.fillPlane?.model?.materials = [SimpleMaterial()] - } - // Necessary for transparency. - var modelMaterial = UnlitMaterial(color: .clear) - modelMaterial.baseColor = endColor - // Necessary for transparency. - modelMaterial.tintColor = Material.Color.white.withAlphaComponent(0.995) - self.fillPlane?.model?.materials[0] = modelMaterial - } } #endif diff --git a/Sources/FocusEntity/FocusEntity+Segment.swift b/Sources/FocusEntity/FocusEntity+Segment.swift index 2f59e45..f4227ef 100644 --- a/Sources/FocusEntity/FocusEntity+Segment.swift +++ b/Sources/FocusEntity/FocusEntity+Segment.swift @@ -9,147 +9,147 @@ import RealityKit internal extension FocusEntity { - /* - The focus square consists of eight segments as follows, which can be individually animated. - - s0 s1 - _ _ - s2 | | s3 - - s4 | | s5 - - - - s6 s7 - */ - enum Corner { - case topLeft // s0, s2 - case topRight // s1, s3 - case bottomRight // s5, s7 - case bottomLeft // s4, s6 - } - - enum Alignment { - case horizontal // s0, s1, s6, s7 - case vertical // s2, s3, s4, s5 - } - - enum Direction { - case up, down, left, right - - var reversed: Direction { - switch self { - case .up: return .down - case .down: return .up - case .left: return .right - case .right: return .left - } - } - } - - class Segment: Entity, HasModel { - - // MARK: - Configuration & Initialization - - /// Thickness of the focus square lines in m. - static let thickness: Float = 0.018 - - /// Length of the focus square lines in m. - static let length: Float = 0.5 // segment length - - /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). - static let openLength: Float = 0.2 - - let corner: Corner - let alignment: Alignment - let plane: ModelComponent - - init(name: String, corner: Corner, alignment: Alignment, color: Material.Color) { - self.corner = corner - self.alignment = alignment - - switch alignment { - case .vertical: - plane = ModelComponent( - mesh: .generatePlane(width: 1, depth: 1), - materials: [UnlitMaterial(color: color)] - ) - case .horizontal: - plane = ModelComponent( - mesh: .generatePlane(width: 1, depth: 1), - materials: [UnlitMaterial(color: color)] - ) - } - super.init() - - switch alignment { - case .vertical: - self.scale = [Segment.thickness, 1, Segment.length] - case .horizontal: - self.scale = [Segment.length, 1, Segment.thickness] - } -// self.orientation = .init(angle: .pi / 2, axis: [1, 0, 0]) - self.name = name - -// let material = plane.firstMaterial! -// material.diffuse.contents = FocusSquare.primaryColor -// material.isDoubleSided = true -// material.ambient.contents = UIColor.black -// material.lightingModel = .constant -// material.emission.contents = FocusSquare.primaryColor - model = plane - } - - required init() { - fatalError("init() has not been implemented") + /* + The focus square consists of eight segments as follows, which can be individually animated. + + s0 s1 + _ _ + s2 | | s3 + + s4 | | s5 + - - + s6 s7 + */ + enum Corner { + case topLeft // s0, s2 + case topRight // s1, s3 + case bottomRight // s5, s7 + case bottomLeft // s4, s6 } - // MARK: - Animating Open/Closed - - var openDirection: Direction { - switch (corner, alignment) { - case (.topLeft, .horizontal): return .left - case (.topLeft, .vertical): return .up - case (.topRight, .horizontal): return .right - case (.topRight, .vertical): return .up - case (.bottomLeft, .horizontal): return .left - case (.bottomLeft, .vertical): return .down - case (.bottomRight, .horizontal): return .right - case (.bottomRight, .vertical): return .down - } + enum Alignment { + case horizontal // s0, s1, s6, s7 + case vertical // s2, s3, s4, s5 } - func open() { - if alignment == .horizontal { - self.scale[0] = Segment.openLength - } else { - self.scale[2] = Segment.openLength - } - - let offset = Segment.length / 2 - Segment.openLength / 2 - updatePosition(withOffset: Float(offset), for: openDirection) + enum Direction { + case up, down, left, right + + var reversed: Direction { + switch self { + case .up: return .down + case .down: return .up + case .left: return .right + case .right: return .left + } + } } - func close() { - let oldLength: Float - if alignment == .horizontal { - oldLength = self.scale[0] - self.scale[0] = Segment.length - } else { - oldLength = self.scale[2] - self.scale[2] = Segment.length - } - - let offset = Segment.length / 2 - oldLength / 2 - updatePosition(withOffset: offset, for: openDirection.reversed) - } + class Segment: Entity, HasModel { + + // MARK: - Configuration & Initialization + + /// Thickness of the focus square lines in m. + static let thickness: Float = 0.018 + + /// Length of the focus square lines in m. + static let length: Float = 0.5 // segment length + + /// Side length of the focus square segments when it is open (w.r.t. to a 1x1 square). + static let openLength: Float = 0.2 + + let corner: Corner + let alignment: Alignment + let plane: ModelComponent + + init(name: String, corner: Corner, alignment: Alignment, color: Material.Color) { + self.corner = corner + self.alignment = alignment + + switch alignment { + case .vertical: + plane = ModelComponent( + mesh: .generatePlane(width: 1, depth: 1), + materials: [UnlitMaterial(color: color)] + ) + case .horizontal: + plane = ModelComponent( + mesh: .generatePlane(width: 1, depth: 1), + materials: [UnlitMaterial(color: color)] + ) + } + super.init() + + switch alignment { + case .vertical: + self.scale = [Segment.thickness, 1, Segment.length] + case .horizontal: + self.scale = [Segment.length, 1, Segment.thickness] + } + // self.orientation = .init(angle: .pi / 2, axis: [1, 0, 0]) + self.name = name + + // let material = plane.firstMaterial! + // material.diffuse.contents = FocusSquare.primaryColor + // material.isDoubleSided = true + // material.ambient.contents = UIColor.black + // material.lightingModel = .constant + // material.emission.contents = FocusSquare.primaryColor + model = plane + } + + required init() { + fatalError("init() has not been implemented") + } + + // MARK: - Animating Open/Closed + + var openDirection: Direction { + switch (corner, alignment) { + case (.topLeft, .horizontal): return .left + case (.topLeft, .vertical): return .up + case (.topRight, .horizontal): return .right + case (.topRight, .vertical): return .up + case (.bottomLeft, .horizontal): return .left + case (.bottomLeft, .vertical): return .down + case (.bottomRight, .horizontal): return .right + case (.bottomRight, .vertical): return .down + } + } + + func open() { + if alignment == .horizontal { + self.scale[0] = Segment.openLength + } else { + self.scale[2] = Segment.openLength + } + + let offset = Segment.length / 2 - Segment.openLength / 2 + updatePosition(withOffset: Float(offset), for: openDirection) + } + + func close() { + let oldLength: Float + if alignment == .horizontal { + oldLength = self.scale[0] + self.scale[0] = Segment.length + } else { + oldLength = self.scale[2] + self.scale[2] = Segment.length + } + + let offset = Segment.length / 2 - oldLength / 2 + updatePosition(withOffset: offset, for: openDirection.reversed) + } + + private func updatePosition(withOffset offset: Float, for direction: Direction) { + switch direction { + case .left: position.x -= offset + case .right: position.x += offset + case .up: position.z -= offset + case .down: position.z += offset + } + } - private func updatePosition(withOffset offset: Float, for direction: Direction) { - switch direction { - case .left: position.x -= offset - case .right: position.x += offset - case .up: position.z -= offset - case .down: position.z += offset - } } - - } } diff --git a/Sources/FocusEntity/FocusEntity.swift b/Sources/FocusEntity/FocusEntity.swift index 3fbb702..6da1ddd 100644 --- a/Sources/FocusEntity/FocusEntity.swift +++ b/Sources/FocusEntity/FocusEntity.swift @@ -18,327 +18,327 @@ import Combine public protocol HasFocusEntity: Entity {} public extension HasFocusEntity { - var focus: FocusEntityComponent { - get { self.components[FocusEntityComponent.self] ?? .classic } - set { self.components[FocusEntityComponent.self] = newValue } - } - var isOpen: Bool { - get { self.focus.isOpen } - set { self.focus.isOpen = newValue } - } - internal var segments: [FocusEntity.Segment] { - get { self.focus.segments } - set { self.focus.segments = newValue } - } - var allowedRaycast: ARRaycastQuery.Target { - get { self.focus.allowedRaycast } - set { self.focus.allowedRaycast = newValue } - } + var focus: FocusEntityComponent { + get { self.components[FocusEntityComponent.self] ?? .classic } + set { self.components[FocusEntityComponent.self] = newValue } + } + var isOpen: Bool { + get { self.focus.isOpen } + set { self.focus.isOpen = newValue } + } + internal var segments: [FocusEntity.Segment] { + get { self.focus.segments } + set { self.focus.segments = newValue } + } + var allowedRaycast: ARRaycastQuery.Target { + get { self.focus.allowedRaycast } + set { self.focus.allowedRaycast = newValue } + } } @objc public protocol FocusEntityDelegate { - /// Called when the FocusEntity is now in world space - @objc optional func toTrackingState() + /// Called when the FocusEntity is now in world space + @objc optional func toTrackingState() - /// Called when the FocusEntity is tracking the camera - @objc optional func toInitializingState() + /// Called when the FocusEntity is tracking the camera + @objc optional func toInitializingState() } /** -An `Entity` which is used to provide uses with visual cues about the status of ARKit world tracking. -*/ + An `Entity` which is used to provide uses with visual cues about the status of ARKit world tracking. + */ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity { - public enum FEError: Error { - case noScene - } - - private var myScene: Scene? { - self.arView?.scene - } - - internal weak var arView: ARView? - - /// For moving the FocusEntity to a whole new ARView - /// - Parameter view: The destination `ARView` - public func moveTo(view: ARView) { - let wasUpdating = self.isAutoUpdating - self.setAutoUpdate(to: false) - self.arView = view - view.scene.addAnchor(self) - if wasUpdating { - self.setAutoUpdate(to: true) + public enum FEError: Error { + case noScene } - } - - /// Destroy this FocusEntity and its references to any ARViews - /// Without calling this, your ARView could stay in memory. - public func destroy() { - self.setAutoUpdate(to: false) - self.delegate = nil - self.arView = nil - for child in children { - child.removeFromParent() + + private var myScene: Scene? { + self.arView?.scene } - self.removeFromParent() - } - private var updateCancellable: Cancellable? - public private(set) var isAutoUpdating: Bool = false + internal weak var arView: ARView? + + /// For moving the FocusEntity to a whole new ARView + /// - Parameter view: The destination `ARView` + public func moveTo(view: ARView) { + let wasUpdating = self.isAutoUpdating + self.setAutoUpdate(to: false) + self.arView = view + view.scene.addAnchor(self) + if wasUpdating { + self.setAutoUpdate(to: true) + } + } - public func setAutoUpdate(to autoUpdate: Bool) { - guard autoUpdate != self.isAutoUpdating, - !(autoUpdate && self.arView == nil) else { - return + /// Destroy this FocusEntity and its references to any ARViews + /// Without calling this, your ARView could stay in memory. + public func destroy() { + self.setAutoUpdate(to: false) + self.delegate = nil + self.arView = nil + for child in children { + child.removeFromParent() + } + self.removeFromParent() } - self.updateCancellable?.cancel() - if autoUpdate { - self.updateCancellable = self.myScene?.subscribe( - to: SceneEvents.Update.self, self.updateFocusEntity - ) + + private var updateCancellable: Cancellable? + public private(set) var isAutoUpdating: Bool = false + + public func setAutoUpdate(to autoUpdate: Bool) { + guard autoUpdate != self.isAutoUpdating, + !(autoUpdate && self.arView == nil) else { + return + } + self.updateCancellable?.cancel() + if autoUpdate { + self.updateCancellable = self.myScene?.subscribe( + to: SceneEvents.Update.self, self.updateFocusEntity + ) + } + self.isAutoUpdating = autoUpdate } - self.isAutoUpdating = autoUpdate - } - public weak var delegate: FocusEntityDelegate? - - // MARK: - Types - public enum State: Equatable { - case initializing - case tracking(raycastResult: ARRaycastResult, camera: ARCamera?) - } - - // MARK: - Properties - - /// The most recent position of the focus square based on the current state. - var lastPosition: SIMD3? { - switch state { - case .initializing: return nil - case .tracking(let raycastResult, _): return raycastResult.worldTransform.translation + public weak var delegate: FocusEntityDelegate? + + // MARK: - Types + public enum State: Equatable { + case initializing + case tracking(raycastResult: ARRaycastResult, camera: ARCamera?) } - } - - fileprivate func entityOffPlane(_ raycastResult: ARRaycastResult, _ camera: ARCamera?) { - self.onPlane = false - displayOffPlane(for: raycastResult) - } - - public var state: State = .initializing { - didSet { - guard state != oldValue else { return } - - switch state { - case .initializing: - if oldValue != .initializing { - displayAsBillboard() - self.delegate?.toInitializingState?() - } - case let .tracking(raycastResult, camera): - let stateChanged = oldValue == .initializing - if stateChanged && self.anchor != nil { - self.anchoring = AnchoringComponent(.world(transform: Transform.identity.matrix)) - } - if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor { - entityOnPlane(for: raycastResult, planeAnchor: planeAnchor) - currentPlaneAnchor = planeAnchor - } else { - entityOffPlane(raycastResult, camera) - currentPlaneAnchor = nil + + // MARK: - Properties + + /// The most recent position of the focus square based on the current state. + var lastPosition: SIMD3? { + switch state { + case .initializing: return nil + case .tracking(let raycastResult, _): return raycastResult.worldTransform.translation } - if stateChanged { - self.delegate?.toTrackingState?() + } + + fileprivate func entityOffPlane(_ raycastResult: ARRaycastResult, _ camera: ARCamera?) { + self.onPlane = false + displayOffPlane(for: raycastResult) + } + + public var state: State = .initializing { + didSet { + guard state != oldValue else { return } + + switch state { + case .initializing: + if oldValue != .initializing { + displayAsBillboard() + self.delegate?.toInitializingState?() + } + case let .tracking(raycastResult, camera): + let stateChanged = oldValue == .initializing + if stateChanged && self.anchor != nil { + self.anchoring = AnchoringComponent(.world(transform: Transform.identity.matrix)) + } + if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor { + entityOnPlane(for: raycastResult, planeAnchor: planeAnchor) + currentPlaneAnchor = planeAnchor + } else { + entityOffPlane(raycastResult, camera) + currentPlaneAnchor = nil + } + if stateChanged { + self.delegate?.toTrackingState?() + } + } } - } } - } - public internal(set) var onPlane: Bool = false + public internal(set) var onPlane: Bool = false + + /// Indicates if the square is currently being animated. + public internal(set) var isAnimating = false - /// Indicates if the square is currently being animated. - public internal(set) var isAnimating = false + /// Indicates if the square is currently changing its alignment. + public internal(set) var isChangingAlignment = false - /// Indicates if the square is currently changing its alignment. - public internal(set) var isChangingAlignment = false + /// A camera anchor used for placing the focus entity in front of the camera. + internal var cameraAnchor: AnchorEntity! - /// A camera anchor used for placing the focus entity in front of the camera. - internal var cameraAnchor: AnchorEntity! + /// The focus square's current alignment. + internal var currentAlignment: ARPlaneAnchor.Alignment? - /// The focus square's current alignment. - internal var currentAlignment: ARPlaneAnchor.Alignment? + /// The current plane anchor if the focus square is on a plane. + public internal(set) var currentPlaneAnchor: ARPlaneAnchor? - /// The current plane anchor if the focus square is on a plane. - public internal(set) var currentPlaneAnchor: ARPlaneAnchor? + /// The focus square's most recent positions. + internal var recentFocusEntityPositions: [SIMD3] = [] - /// The focus square's most recent positions. - internal var recentFocusEntityPositions: [SIMD3] = [] + /// The focus square's most recent alignments. + internal var recentFocusEntityAlignments: [ARPlaneAnchor.Alignment] = [] - /// The focus square's most recent alignments. - internal var recentFocusEntityAlignments: [ARPlaneAnchor.Alignment] = [] + /// Previously visited plane anchors. + internal var anchorsOfVisitedPlanes: Set = [] - /// Previously visited plane anchors. - internal var anchorsOfVisitedPlanes: Set = [] + /// The primary node that controls the position of other `FocusEntity` nodes. + internal let positioningEntity = Entity() - /// The primary node that controls the position of other `FocusEntity` nodes. - internal let positioningEntity = Entity() + internal var fillPlane: ModelEntity? - internal var fillPlane: ModelEntity? + public var scaleEntityBasedOnDistance = true { + didSet { + if self.scaleEntityBasedOnDistance == false { + self.scale = .one + } + } + } + + // MARK: - Initialization - public var scaleEntityBasedOnDistance = true { - didSet { - if self.scaleEntityBasedOnDistance == false { - self.scale = .one - } + public convenience init(on arView: ARView, style: FocusEntityComponent.Style) { + self.init(on: arView, focus: FocusEntityComponent(style: style)) } - } - - // MARK: - Initialization - - public convenience init(on arView: ARView, style: FocusEntityComponent.Style) { - self.init(on: arView, focus: FocusEntityComponent(style: style)) - } - public required init(on arView: ARView, focus: FocusEntityComponent) { - self.arView = arView - super.init() - self.focus = focus - self.name = "FocusEntity" - self.orientation = simd_quatf(angle: .pi / 2, axis: [1, 0, 0]) - - self.addChild(self.positioningEntity) - - cameraAnchor = AnchorEntity(.camera) - arView.scene.addAnchor(cameraAnchor) - - // Start the focus square as a billboard. - displayAsBillboard() - self.delegate?.toInitializingState?() - arView.scene.addAnchor(self) - self.setAutoUpdate(to: true) - switch self.focus.style { - case .colored(_, _, _, let mesh): - let fillPlane = ModelEntity(mesh: mesh) - self.positioningEntity.addChild(fillPlane) - self.fillPlane = fillPlane - self.coloredStateChanged() - case .classic: - guard let classicStyle = self.focus.classicStyle else { - return - } - self.setupClassic(classicStyle) + public required init(on arView: ARView, focus: FocusEntityComponent) { + self.arView = arView + super.init() + self.focus = focus + self.name = "FocusEntity" + self.orientation = simd_quatf(angle: .pi / 2, axis: [1, 0, 0]) + + self.addChild(self.positioningEntity) + + cameraAnchor = AnchorEntity(.camera) + arView.scene.addAnchor(cameraAnchor) + + // Start the focus square as a billboard. + displayAsBillboard() + self.delegate?.toInitializingState?() + arView.scene.addAnchor(self) + self.setAutoUpdate(to: true) + switch self.focus.style { + case .colored(_, _, _, let mesh): + let fillPlane = ModelEntity(mesh: mesh) + self.positioningEntity.addChild(fillPlane) + self.fillPlane = fillPlane + self.coloredStateChanged() + case .classic: + guard let classicStyle = self.focus.classicStyle else { + return + } + self.setupClassic(classicStyle) + } } - } - required public init() { - fatalError("init() has not been implemented") - } + required public init() { + fatalError("init() has not been implemented") + } - // MARK: - Appearance + // MARK: - Appearance - /// Hides the focus square. - func hide() { - self.isEnabled = false -// runAction(.fadeOut(duration: 0.5), forKey: "hide") - } + /// Hides the focus square. + func hide() { + self.isEnabled = false + // runAction(.fadeOut(duration: 0.5), forKey: "hide") + } - /// Displays the focus square parallel to the camera plane. - private func displayAsBillboard() { - self.onPlane = false - self.currentAlignment = .none - stateChangedSetup() - } + /// Displays the focus square parallel to the camera plane. + private func displayAsBillboard() { + self.onPlane = false + self.currentAlignment = .none + stateChangedSetup() + } /// Places the focus entity in front of the camera instead of on a plane. private func putInFrontOfCamera() { - // Works better than arView.ray() - let newPosition = cameraAnchor.convert(position: [0, 0, -1], to: nil) - recentFocusEntityPositions.append(newPosition) - updatePosition() - // --// - // Make focus entity face the camera with a smooth animation. - var newRotation = arView?.cameraTransform.rotation ?? simd_quatf() - newRotation *= simd_quatf(angle: .pi / 2, axis: [1, 0, 0]) - performAlignmentAnimation(to: newRotation) + // Works better than arView.ray() + let newPosition = cameraAnchor.convert(position: [0, 0, -1], to: nil) + recentFocusEntityPositions.append(newPosition) + updatePosition() + // --// + // Make focus entity face the camera with a smooth animation. + var newRotation = arView?.cameraTransform.rotation ?? simd_quatf() + newRotation *= simd_quatf(angle: .pi / 2, axis: [1, 0, 0]) + performAlignmentAnimation(to: newRotation) } - /// Called when a surface has been detected. - private func displayOffPlane(for raycastResult: ARRaycastResult) { - self.stateChangedSetup() - let position = raycastResult.worldTransform.translation - if self.currentAlignment != .none { - // It is ready to move over to a new surface. - recentFocusEntityPositions.append(position) - performAlignmentAnimation(to: raycastResult.worldTransform.orientation) - } else { - putInFrontOfCamera() + /// Called when a surface has been detected. + private func displayOffPlane(for raycastResult: ARRaycastResult) { + self.stateChangedSetup() + let position = raycastResult.worldTransform.translation + if self.currentAlignment != .none { + // It is ready to move over to a new surface. + recentFocusEntityPositions.append(position) + performAlignmentAnimation(to: raycastResult.worldTransform.orientation) + } else { + putInFrontOfCamera() + } + updateTransform(raycastResult: raycastResult) } - updateTransform(raycastResult: raycastResult) - } - - /// Called when a plane has been detected. - private func entityOnPlane( - for raycastResult: ARRaycastResult, planeAnchor: ARPlaneAnchor - ) { - self.onPlane = true - self.stateChangedSetup(newPlane: !anchorsOfVisitedPlanes.contains(planeAnchor)) - anchorsOfVisitedPlanes.insert(planeAnchor) - let position = raycastResult.worldTransform.translation - if self.currentAlignment != .none { - // It is ready to move over to a new surface. - recentFocusEntityPositions.append(position) - } else { - putInFrontOfCamera() + + /// Called when a plane has been detected. + private func entityOnPlane( + for raycastResult: ARRaycastResult, planeAnchor: ARPlaneAnchor + ) { + self.onPlane = true + self.stateChangedSetup(newPlane: !anchorsOfVisitedPlanes.contains(planeAnchor)) + anchorsOfVisitedPlanes.insert(planeAnchor) + let position = raycastResult.worldTransform.translation + if self.currentAlignment != .none { + // It is ready to move over to a new surface. + recentFocusEntityPositions.append(position) + } else { + putInFrontOfCamera() + } + updateTransform(raycastResult: raycastResult) } - updateTransform(raycastResult: raycastResult) - } - - /// Called whenever the state of the focus entity changes - /// - /// - Parameter newPlane: If the entity is directly on a plane, is it a new plane to track - public func stateChanged(newPlane: Bool = false) { - switch self.focus.style { - case .colored: - self.coloredStateChanged() - case .classic: - if self.onPlane { - self.onPlaneAnimation(newPlane: newPlane) - } else { - self.offPlaneAniation() - } + + /// Called whenever the state of the focus entity changes + /// + /// - Parameter newPlane: If the entity is directly on a plane, is it a new plane to track + public func stateChanged(newPlane: Bool = false) { + switch self.focus.style { + case .colored: + self.coloredStateChanged() + case .classic: + if self.onPlane { + self.onPlaneAnimation(newPlane: newPlane) + } else { + self.offPlaneAniation() + } + } } - } - - private func stateChangedSetup(newPlane: Bool = false) { - guard !isAnimating else { return } - self.stateChanged(newPlane: newPlane) - } - - public func updateFocusEntity(event: SceneEvents.Update? = nil) { - // Perform hit testing only when ARKit tracking is in a good state. - guard let camera = self.arView?.session.currentFrame?.camera, - case .normal = camera.trackingState, - let result = self.smartRaycast() - else { - // We should place the focus entity in front of the camera instead of on a plane. - putInFrontOfCamera() - self.state = .initializing - return + + private func stateChangedSetup(newPlane: Bool = false) { + guard !isAnimating else { return } + self.stateChanged(newPlane: newPlane) } - self.state = .tracking(raycastResult: result, camera: camera) - } + public func updateFocusEntity(event: SceneEvents.Update? = nil) { + // Perform hit testing only when ARKit tracking is in a good state. + guard let camera = self.arView?.session.currentFrame?.camera, + case .normal = camera.trackingState, + let result = self.smartRaycast() + else { + // We should place the focus entity in front of the camera instead of on a plane. + putInFrontOfCamera() + self.state = .initializing + return + } + + self.state = .tracking(raycastResult: result, camera: camera) + } } #else /** -FocusEntity is only enabled for environments which can import ARKit. -*/ + FocusEntity is only enabled for environments which can import ARKit. + */ open class FocusEntity { - public convenience init(on arView: ARView, style: FocusEntityComponent.Style) { - self.init(on: arView, focus: FocusEntityComponent(style: style)) - } - public convenience init(on arView: ARView, focus: FocusEntityComponent) { - self.init() - } - internal init() { - print("This is not supported when ARKit cannot be imported or using the simulator.") - } + public convenience init(on arView: ARView, style: FocusEntityComponent.Style) { + self.init(on: arView, focus: FocusEntityComponent(style: style)) + } + public convenience init(on arView: ARView, focus: FocusEntityComponent) { + self.init() + } + internal init() { + print("This is not supported when ARKit cannot be imported or using the simulator.") + } } #endif diff --git a/Sources/FocusEntity/FocusEntityComponent.swift b/Sources/FocusEntity/FocusEntityComponent.swift index 770d3c9..4508e9c 100644 --- a/Sources/FocusEntity/FocusEntityComponent.swift +++ b/Sources/FocusEntity/FocusEntityComponent.swift @@ -10,78 +10,78 @@ import RealityKit import ARKit internal struct ClassicStyle { - var color: Material.Color + var color: Material.Color } /// When using colored style, first material of a mesh will be replaced with the chosen color internal struct ColoredStyle { - /// Color when tracking the surface of a known plane - var onColor: MaterialColorParameter - /// Color when tracking an estimated plane - var offColor: MaterialColorParameter - /// Color when no surface tracking is achieved - var nonTrackingColor: MaterialColorParameter - var mesh: MeshResource + /// Color when tracking the surface of a known plane + var onColor: MaterialColorParameter + /// Color when tracking an estimated plane + var offColor: MaterialColorParameter + /// Color when no surface tracking is achieved + var nonTrackingColor: MaterialColorParameter + var mesh: MeshResource } public struct FocusEntityComponent: Component { - public enum Style { - case classic(color: Material.Color) - case colored( - onColor: MaterialColorParameter, - offColor: MaterialColorParameter, - nonTrackingColor: MaterialColorParameter, - mesh: MeshResource = MeshResource.generatePlane(width: 0.1, depth: 0.1) - ) - } + public enum Style { + case classic(color: Material.Color) + case colored( + onColor: MaterialColorParameter, + offColor: MaterialColorParameter, + nonTrackingColor: MaterialColorParameter, + mesh: MeshResource = MeshResource.generatePlane(width: 0.1, depth: 0.1) + ) + } - let style: Style - var classicStyle: ClassicStyle? { - switch self.style { - case .classic(let color): - return ClassicStyle(color: color) - default: - return nil + let style: Style + var classicStyle: ClassicStyle? { + switch self.style { + case .classic(let color): + return ClassicStyle(color: color) + default: + return nil + } } - } - var coloredStyle: ColoredStyle? { - switch self.style { - case .colored(let onColor, let offColor, let nonTrackingColor, let mesh): - return ColoredStyle( - onColor: onColor, offColor: offColor, - nonTrackingColor: nonTrackingColor, mesh: mesh - ) - default: - return nil + var coloredStyle: ColoredStyle? { + switch self.style { + case .colored(let onColor, let offColor, let nonTrackingColor, let mesh): + return ColoredStyle( + onColor: onColor, offColor: offColor, + nonTrackingColor: nonTrackingColor, mesh: mesh + ) + default: + return nil + } } - } - /// Convenient presets - public static let classic = FocusEntityComponent(style: .classic(color: #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1))) - public static let plane = FocusEntityComponent( - style: .colored( - onColor: .color(.green), - offColor: .color(.orange), - nonTrackingColor: .color(Material.Color.red.withAlphaComponent(0.2)), - mesh: FocusEntityComponent.defaultPlane + /// Convenient presets + public static let classic = FocusEntityComponent(style: .classic(color: #colorLiteral(red: 1, green: 0.8, blue: 0, alpha: 1))) + public static let plane = FocusEntityComponent( + style: .colored( + onColor: .color(.green), + offColor: .color(.orange), + nonTrackingColor: .color(Material.Color.red.withAlphaComponent(0.2)), + mesh: FocusEntityComponent.defaultPlane + ) ) - ) - internal var isOpen = true - internal var segments: [FocusEntity.Segment] = [] - public var allowedRaycast: ARRaycastQuery.Target = .estimatedPlane + internal var isOpen = true + internal var segments: [FocusEntity.Segment] = [] + public var allowedRaycast: ARRaycastQuery.Target = .estimatedPlane - static var defaultPlane = MeshResource.generatePlane( - width: 0.1, depth: 0.1 - ) + static var defaultPlane = MeshResource.generatePlane( + width: 0.1, depth: 0.1 + ) - public init(style: Style) { - self.style = style - // If the device has LiDAR, then default behaviour is to only allow - // existing detected planes - if #available(iOS 13.4, *), - ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { - self.allowedRaycast = .existingPlaneGeometry + public init(style: Style) { + self.style = style + // If the device has LiDAR, then default behaviour is to only allow + // existing detected planes + if #available(iOS 13.4, *), + ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { + self.allowedRaycast = .existingPlaneGeometry + } } - } } diff --git a/Sources/FocusEntity/float4x4+Extension.swift b/Sources/FocusEntity/float4x4+Extension.swift index 8de8a30..e338223 100644 --- a/Sources/FocusEntity/float4x4+Extension.swift +++ b/Sources/FocusEntity/float4x4+Extension.swift @@ -9,24 +9,24 @@ import simd internal extension float4x4 { - /** - Treats matrix as a (right-hand column-major convention) transform matrix - and factors out the translation component of the transform. - */ - var translation: SIMD3 { - get { - let translation = columns.3 - return SIMD3(translation.x, translation.y, translation.z) + /** + Treats matrix as a (right-hand column-major convention) transform matrix + and factors out the translation component of the transform. + */ + var translation: SIMD3 { + get { + let translation = columns.3 + return SIMD3(translation.x, translation.y, translation.z) + } + set(newValue) { + columns.3 = SIMD4(newValue.x, newValue.y, newValue.z, columns.3.w) + } } - set(newValue) { - columns.3 = SIMD4(newValue.x, newValue.y, newValue.z, columns.3.w) - } - } - /** - Factors out the orientation component of the transform. - */ - var orientation: simd_quatf { - return simd_quaternion(self) - } + /** + Factors out the orientation component of the transform. + */ + var orientation: simd_quatf { + return simd_quaternion(self) + } } From a2e0c1e9be25d1998fe00e9714235286d7703291 Mon Sep 17 00:00:00 2001 From: Max Cobb Date: Mon, 10 Jan 2022 16:06:20 +0000 Subject: [PATCH 2/3] bump xcode version to 13 for new color properties --- .github/workflows/swift-build.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/swift-build.yml b/.github/workflows/swift-build.yml index f2eb03e..730a846 100644 --- a/.github/workflows/swift-build.yml +++ b/.github/workflows/swift-build.yml @@ -17,10 +17,9 @@ jobs: - uses: actions/checkout@v1 - name: Build Package run: | - swift package generate-xcodeproj - xcodebuild clean build -project $PROJECT -scheme $SCHEME -sdk $SDK CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO + sudo xcode-select --switch $DEVELOPER_DIR + xcodebuild -scheme $SCHEME -destination $DESTINATION | xcpretty env: - DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer - PROJECT: FocusEntity.xcodeproj - SCHEME: FocusEntity-Package - SDK: iphoneos + DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer + SCHEME: FocusEntity + DESTINATION: generic/platform=iOS From 28bfdd5c8fd579d77f7bf7e61d42a8025653a63a Mon Sep 17 00:00:00 2001 From: Max Cobb <5754073+maxxfrazer@users.noreply.github.com> Date: Sun, 27 Feb 2022 12:07:29 +0000 Subject: [PATCH 3/3] allow macos build for cross platform apps, but added warning when building for simulator or macOS --- Package.swift | 4 ++-- Sources/FocusEntity/FocusEntity.swift | 6 +++++- Sources/FocusEntity/FocusEntityComponent.swift | 6 ++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index e29e1ff..20419a7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "FocusEntity", - platforms: [.iOS("13.0")], + platforms: [.iOS(.v13), .macOS(.v10_15)], products: [ .library(name: "FocusEntity", targets: ["FocusEntity"]) ], diff --git a/Sources/FocusEntity/FocusEntity.swift b/Sources/FocusEntity/FocusEntity.swift index 6da1ddd..102095a 100644 --- a/Sources/FocusEntity/FocusEntity.swift +++ b/Sources/FocusEntity/FocusEntity.swift @@ -11,6 +11,10 @@ import RealityKit import RealityFoundation #endif +#if os(macOS) || targetEnvironment(simulator) +#warning("FocusEntity: This package is only fully available with physical iOS devices") +#endif + #if canImport(ARKit) && !targetEnvironment(simulator) import ARKit import Combine @@ -338,7 +342,7 @@ open class FocusEntity { self.init() } internal init() { - print("This is not supported when ARKit cannot be imported or using the simulator.") + print("This is only supported on a physical iOS device.") } } #endif diff --git a/Sources/FocusEntity/FocusEntityComponent.swift b/Sources/FocusEntity/FocusEntityComponent.swift index 4508e9c..dae8530 100644 --- a/Sources/FocusEntity/FocusEntityComponent.swift +++ b/Sources/FocusEntity/FocusEntityComponent.swift @@ -7,7 +7,9 @@ // import RealityKit +#if !os(macOS) import ARKit +#endif internal struct ClassicStyle { var color: Material.Color @@ -69,7 +71,9 @@ public struct FocusEntityComponent: Component { ) internal var isOpen = true internal var segments: [FocusEntity.Segment] = [] + #if !os(macOS) public var allowedRaycast: ARRaycastQuery.Target = .estimatedPlane + #endif static var defaultPlane = MeshResource.generatePlane( width: 0.1, depth: 0.1 @@ -79,9 +83,11 @@ public struct FocusEntityComponent: Component { self.style = style // If the device has LiDAR, then default behaviour is to only allow // existing detected planes + #if !os(macOS) if #available(iOS 13.4, *), ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { self.allowedRaycast = .existingPlaneGeometry } + #endif } }