Repository navigation
ios: draw the AR result with the Canvas until RealityKit is seen drawing it #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: t3/ios-mvf
Are you sure you want to change the base?
Changes from all commits
41291e8
81d118b
7ab6c8c
acfb67e
74652a6
b6dc85c
7803e7b
31c0403
0ada11b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,7 +56,11 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The AR result in the camera view, while one is shown (`showResult`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var result: AnchorEntity? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var occludesPeople = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// An empty entity in the result at the point it has to put on screen (the battery's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// middle), for `resultIsDrawn`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var resultFocus: Entity? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The last `resultIsDrawn` findings logged, so they are logged when they change. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var lastDrawnReport: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| init(onFrame: @escaping @MainActor @Sendable (SourceFrame) -> Void, onEvent: @escaping @MainActor @Sendable (LiveEvent) -> Void) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -66,8 +70,9 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.renderOptions.insert(.disableMotionBlur) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// People occlusion runs only while the AR result is on screen: the capture has no virtual | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// content for it to hide. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// People occlusion is off: no caller asks for it. Turning it on while the AR result showed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// re-ran the session under the result's freshly added anchor entity, and build 4.1 drew no | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// result on the phone it was tested on (#67). Whether and how to bring it back is #41. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static func configuration(occludingPeople: Bool = false) -> ARWorldTrackingConfiguration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let configuration = ARWorldTrackingConfiguration() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // .gravity, not .gravityAndHeading: compass heading drifts near a house's metal and wiring. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -111,7 +116,7 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func start() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeLog.engine.info("LiDAR: scene depth \(Self.supportsDepth ? "on" : "not available", privacy: .public), mesh \(Self.supportsMesh ? "on" : "not available", privacy: .public)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.run(Self.configuration(occludingPeople: occludesPeople)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.run(Self.configuration()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func pause() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,7 +126,7 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Starts world tracking over with a fresh map, after relocalization failed. The old mesh goes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// with the old anchors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func restart() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.run(Self.configuration(occludingPeople: occludesPeople), options: [.resetTracking, .removeExistingAnchors]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.run(Self.configuration(), options: [.resetTracking, .removeExistingAnchors]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delegate.shared.withLock { $0.meterAnchorID = nil } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -130,22 +135,63 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// when the anchor was at `pose` (`MeterAnchorTracking.pose`, the pose the engine's wall | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// agrees with). It is attached in that pose's frame, so a turn ARKit has made to the anchor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// since carries the model with it. Cancelling the anchor's current turn instead drew the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// wall as it stood before the turn. People and, on a phone with LiDAR, the scene mesh hide | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// it where they stand in front of it. False when the meter has no anchor in the session; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// nothing is shown then. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func showResult(_ model: Entity, builtFor pose: simd_float4x4) -> Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// wall as it stood before the turn. On a phone with LiDAR the scene mesh hides it where | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// something stands in front of it. `focus` is the point the model has to put on screen, in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the model's coordinates (`resultIsDrawn`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// True means the model was handed to RealityKit, not that anything is drawn: build 4.1 took | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// it for that and hid the screen's own drawing on a phone where nothing appeared (#67). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// False when the meter has no anchor in the session; nothing is shown then. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func showResult(_ model: Entity, builtFor pose: simd_float4x4, focus: SIMD3<Float>) -> Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removeResult() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let id = delegate.shared.withLock({ $0.meterAnchorID }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.currentFrame?.anchors.contains(where: { $0.identifier == id }) == true else { return false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.currentFrame?.anchors.contains(where: { $0.identifier == id }) == true else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeLog.engine.info("AR result not shown: the meter's anchor is not in the session") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let holder = AnchorEntity(.anchor(identifier: id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // See-through until the engine hands the result to the AR scene (`setResultOwned`): a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // new model starts behind the screen's own drawing, and only one of them may show. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| holder.components.set(OpacityComponent(opacity: 0)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.orientation = simd_quatf(pose).inverse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let marker = Entity() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| marker.position = focus | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.addChild(marker) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| holder.addChild(model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.scene.addAnchor(holder) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = holder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultFocus = marker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastDrawnReport = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOcclusion(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Whether RealityKit holds the shown result now, and whether it can be drawing it: `held` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// when its anchor entity is anchored to the meter's ARKit anchor and enabled, `drawn` when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the result's focus point also lands in front of the camera and inside the view. Both are | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// read the same whether the model is see-through or shown (`setResultOwned`). A check of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the chain, not of pixels: RealityKit says nothing about what it drew. `drawn` confirms the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// model; `held` keeps it once confirmed (`ResultOverlayPolicy`). The findings are logged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// when they change. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func resultIsDrawn() -> (drawn: Bool, held: Bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let result, let resultFocus else { return (false, false) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let id = delegate.shared.withLock { $0.meterAnchorID } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let inSession = id.map { id in arView.session.currentFrame?.anchors.contains(where: { $0.identifier == id }) == true } ?? false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let anchored = result.isAnchored | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let enabled = result.isEnabledInHierarchy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let world = resultFocus.position(relativeTo: nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let inCamera = arView.cameraTransform.matrix.inverse * SIMD4(world, 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let point = inCamera.z < 0 ? arView.project(world) : nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let onScreen = point.map { arView.bounds.contains($0) } ?? false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let report = "anchor in session \(inSession), anchored \(anchored), enabled \(enabled), on screen \(onScreen)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if report != lastDrawnReport { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastDrawnReport = report | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let at = point.map { String(format: "(%.0f, %.0f) in %.0f x %.0f", Double($0.x), Double($0.y), Double(arView.bounds.width), Double(arView.bounds.height)) } ?? "none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeLog.engine.info("AR result: \(report, privacy: .public), projected at \(at, privacy: .public)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (anchored && enabled && onScreen, anchored && enabled) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+176
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add Proposed fix- return (anchored && enabled && onScreen, anchored && enabled)
+ let held = inSession && anchored && enabled
+ return (held && onScreen, held)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func hideResult() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removeResult() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOcclusion(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -156,23 +202,29 @@ final class LiveCapture { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result?.isEnabled = visible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Whether the AR scene owns the result (`ResultOverlayPolicy.usesRealityKit`): the model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// shows only then, and is see-through otherwise, while the screen draws it. See-through | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// rather than disabled, so it stays anchored and enabled and `resultIsDrawn` can still | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// confirm it. The opacity applies to the model and everything under it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func setResultOwned(_ owned: Bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result?.components.set(OpacityComponent(opacity: owned ? 1 : 0)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func removeResult() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result?.removeFromParent() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultFocus = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The LiDAR mesh hides the result where something real stands in front of it. A RealityKit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// setting only: the session is not re-run (see `configuration`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func setOcclusion(_ on: Bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if Self.supportsMesh { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if on { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.environment.sceneUnderstanding.options.insert(.occlusion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.environment.sceneUnderstanding.options.remove(.occlusion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard Self.supportsMesh else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if on { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.environment.sceneUnderstanding.options.insert(.occlusion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.environment.sceneUnderstanding.options.remove(.occlusion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard Self.supportsPeopleOcclusion, occludesPeople != on else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| occludesPeople = on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Without reset options the session keeps its tracking, anchors and mesh. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arView.session.run(Self.configuration(occludingPeople: on)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// ARKit's mesh in world meters, with one `ARMeshClassification` raw value per triangle (0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,6 +125,14 @@ final class ScanEngine { | |
| // Tracking recovery | ||
| private var relocalizingSince: Double? | ||
|
|
||
| // AR result | ||
| /// Watches whether the AR scene draws the result (`watchResultInCamera`) while "See it on | ||
| /// your wall" is up on the live camera. | ||
| private var resultWatch: Task<Void, Never>? | ||
| /// Which layer draws the result, and the wall the model in the AR scene was built for | ||
| /// (`showResultInCamera`, `watchResultInCamera`). | ||
| private var resultPolicy = ResultOverlayPolicy() | ||
|
|
||
| // Upload | ||
| let resultClient: any ResultClient | ||
| private var uploadTask: Task<Void, Never>? | ||
|
|
@@ -358,6 +366,8 @@ final class ScanEngine { | |
| RuntimeLog.capture.info("tracking \(Self.name(self.state.tracking), privacy: .public) -> \(Self.name(frame.tracking), privacy: .public)") | ||
| if state.tracking == .normal { breakWalkedPath(because: "tracking left normal") } | ||
| state.tracking = frame.tracking | ||
| // The model stays see-through until the next look hands it the result | ||
| // (`setResultInCamera`), so enabling it here never shows it over the screen's drawing. | ||
| live?.setResultVisible(frame.tracking == .normal) | ||
| } | ||
| if let planes = frame.groundPlanes { groundPlanes = planes } | ||
|
|
@@ -1204,20 +1214,85 @@ final class ScanEngine { | |
| refreshSpotPhoto() | ||
| } | ||
|
|
||
| /// "See it on your wall" on the live camera: the result goes into the AR scene on the meter's | ||
| /// anchor, where people and objects in front of it hide it. A replay, or a meter without an | ||
| /// anchor, leaves it to the screen's overlay. | ||
| /// "See it on your wall" on the live camera. The screen draws the result over the camera | ||
| /// itself (`BatteryOverlay`) unless the AR scene is seen drawing it (`watchResultInCamera`). | ||
| /// Build 4.1 trusted the AR scene as soon as it had the model and showed no battery at all | ||
| /// (#67). Only a phone with LiDAR tries the AR scene, where the mesh can hide the result | ||
| /// behind things in front of it; without LiDAR the screen draws it, as build 3.1 did. A | ||
| /// replay, or a meter without an anchor, leaves it to the screen too. | ||
| private func showResultInCamera(rising: Bool) { | ||
| guard let live, let wall = state.wall, let result = state.result, let pose = meterTracking?.pose, | ||
| state.spatialResultAvailable else { return } | ||
| guard LiveCapture.supportsMesh, let live, let wall = state.wall, let result = state.result, | ||
| let pose = meterTracking?.pose, state.spatialResultAvailable else { return } | ||
| if rising || resultWatch == nil { watchResultInCamera() } | ||
| // Each rebuild takes the model out of the scene and puts a new one in, so a wall that | ||
| // moved by a centimeter or two keeps the one it has. A new model starts on the screen's | ||
| // drawing and has to be seen drawn again (`ResultOverlayPolicy.needsModel`, review of | ||
| // #100); it goes in see-through (`LiveCapture.showResult`), so only one layer shows. | ||
| guard resultPolicy.needsModel(for: Self.modelShape(wall), rising: rising) else { return } | ||
| setResultInCamera(resultPolicy.usesRealityKit) | ||
| let model = ResultARModel.build(wall: wall, result: result) | ||
| state.resultInCamera = live.showResult(model, builtFor: pose) | ||
| guard state.resultInCamera else { return } | ||
| // The battery's middle, or the meter without a spot, in the model's coordinates. | ||
| let focus = (result.spotCenter(on: wall) ?? wall.meter) - wall.meter | ||
| guard live.showResult(model, builtFor: pose, focus: focus) else { | ||
|
Comment on lines
1233
to
+1236
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AidenJohnston Confirmed at 41291e8 and beta 5bc81fb. A LiDAR anchor/ground correction beyond the rebuild tolerance replaces the entity while the watcher retains its previous confirmation. A replacement that looks anchored/enabled/onscreen by the next poll never receives a fresh 0.5-second confirmation. Please reset confirmation and restore Canvas before an actual replacement, after the no-rebuild early return, and cover confirmed-model replacement. Non-LiDAR uses the separate Canvas path; a visible blank frame has not been reproduced here.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AidenJohnston Rechecked acfb67e. The actual-replacement reset fixes inherited confirmation, and ordinary offscreen movement keeps RealityKit ownership. One conditional duplicate-render path remains: ScanEngine.swift:1221-1231 resets to Canvas, then installs/enables the replacement while its battery focus is offscreen. held=true/drawn=false keeps Canvas active indefinitely until the focus returns, while RealityKit can still draw the visible cable/clearance geometry. Please make that replacement handoff exclusive and cover offscreen replacement with visible non-focus geometry. The heldOnly policy test alone does not check both renderer states. LiDAR-only source finding; no device reproduction. This refines the existing duplicate-layer finding, not a claim of new blank-render evidence. |
||
| resultPolicy.modelRemoved() | ||
| return | ||
| } | ||
| live.setResultVisible(state.tracking == .normal) | ||
| if rising { ResultARModel.rise(model) } | ||
| } | ||
|
|
||
| /// What the AR result's model is built from, for `ResultOverlayPolicy.needsModel`: the | ||
| /// meter, the ground, the ends and each piece of wall. | ||
| private static func modelShape(_ wall: WallGeometry) -> ResultModelShape { | ||
| var shape = ResultModelShape( | ||
| points: [wall.meter], values: [wall.groundY, wall.leftEnd, wall.rightEnd], | ||
| directions: [wall.along, wall.outward] | ||
| ) | ||
| for piece in wall.cornerSegments { | ||
| shape.points.append(piece.anchor) | ||
| shape.values += [piece.span.lowerBound, piece.span.upperBound, piece.anchorS] | ||
| shape.directions += [piece.along, piece.outward] | ||
| } | ||
| return shape | ||
| } | ||
|
|
||
| /// Looks at the AR scene about ten times a second while "See it on your wall" is up, and | ||
| /// lets the screen's drawing step aside once the scene is seen drawing the result, for as | ||
| /// long as the scene holds it (`ResultOverlayPolicy`). On its own clock, not in `ingest`: it | ||
| /// has to notice a scene that stopped holding it whether frames arrive or not. | ||
| private func watchResultInCamera() { | ||
| resultWatch?.cancel() | ||
| resultWatch = Task { [weak self] in | ||
| while !Task.isCancelled { | ||
| guard let self else { return } | ||
| guard self.state.phase == .resultAR, let live = self.live else { | ||
| // Nothing left to watch: the screen draws the result (review of #100). | ||
| self.setResultInCamera(false) | ||
| return | ||
| } | ||
| let look = live.resultIsDrawn() | ||
| self.setResultInCamera(self.resultPolicy.update(drawn: look.drawn, held: look.held, time: self.screenTime)) | ||
| try? await Task.sleep(for: .milliseconds(100)) | ||
|
Comment on lines
+1274
to
+1275
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When tracking returns to normal after this model was confirmed, Useful? React with 👍 / 👎.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AidenJohnston Source check agrees at acfb67e: ingest enables the entity synchronously at ScanEngine.swift:369, while ownership changes only in the 100 ms watcher at1244-1246. Please update ownership and model visibility together on tracking recovery. This is a short overlap distinct from the already-requested indefinite offscreen-replacement overlap. No device timing measurement. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Which layer draws the result: the AR scene (true) or the screen's overlay (false). The | ||
| /// AR scene's model shows only while it owns the result (`LiveCapture.setResultOwned`), and | ||
| /// both change here together, so the two layers are never both on screen: not while a new | ||
| /// model waits to be seen, and not while tracking comes back and `ingest` enables the model | ||
| /// before the next look (review of #100). | ||
| private func setResultInCamera(_ usesRealityKit: Bool) { | ||
| live?.setResultOwned(usesRealityKit) | ||
| guard state.resultInCamera != usesRealityKit else { return } | ||
| RuntimeLog.engine.info("AR result drawn by \(usesRealityKit ? "the AR scene" : "the screen overlay", privacy: .public)") | ||
| state.resultInCamera = usesRealityKit | ||
| } | ||
|
|
||
| private func hideResultInCamera() { | ||
| resultWatch?.cancel() | ||
| resultWatch = nil | ||
| resultPolicy = ResultOverlayPolicy() | ||
| live?.hideResult() | ||
| state.resultInCamera = false | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log prints the projected position, but
lastDrawnReportcompares only the four booleans. As someone moves the phone, the position can change substantially without another log entry. That removes useful location evidence when diagnosing a missing AR result; consider logging meaningful position changes or sampling them at a bounded interval.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!