Skip to content

Draw the prototype's fog and live dots over the walk - #158

Open
SamGu-NRX wants to merge 6 commits into
t3/ios-mvffrom
t3/live-fog
Open

SamGu-NRX wants to merge 6 commits into
t3/ios-mvffrom
t3/live-fog

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

What changed

During the wall walk and a gap request, the camera view now draws the prototype's look (experiments/live-dots on t3/experience, PR #86) in place of the frosted strip. Sam tested the beta and the strip, a blurred band over the wall, looked bad.

  • One soft fog over what the scan hasn't counted. A Metal layer over the camera draws the prototype's chalk-grey fog, textured with slow noise. It lifts only where the coverage map counts a cell, the same rule the scan's decisions use, not wherever LiDAR returns a depth: unseen is full fog, a cell seen from one position is thinned, a covered cell is clear. The rule is FogValue.target in LiveFogScene.swift. Each coverage cell is drawn as a wall column and a stretch of ground into a small mask, which is blurred and read through noise so the edges come out as ragged fog banks rather than strips. The fog reaches above the 4.5 ft wall band and past the 4 ft ground band, so walking can clear what is on screen. A cell narrower than the mask's texels can't drop out: the mask is drawn at three times the size, max-pooled, and sets a floor the blur can't erase.
  • Live dots on LiDAR phones. HouseScanKit's new SurfaceDots fuses the depth of each kept keyframe, the same frames coverage observes, into 5 cm voxels and marks creases and silhouettes as edges. It is a port of the prototype's DotField without its colour-step edges, which need the photo at depth resolution. LiveDotsFeed runs it off the main actor. The dots are guidance only; nothing reads them back. Dots under fog draw at 20%, so they arrive as the fog lifts. Dots on something in front of the wall draw violet.
  • Hidden and requested stretches stay readable. Behind a bin, the fog stays, thinner and violet, with the tape map's dashed violet outline. A gap request turns its stretch amber with an amber outline. "I can't get there" keeps its slate hatch.
  • Marks get the prototype's corner brackets. The meter is in Signal blue, doors, windows, gas meters and AC units in Hologram white, each with a 13 pt caption. A one-tap mark has no size, so its bracket uses a typical one.

Phones without LiDAR get the same fog without dots. The prototype's no-LiDAR dots come from ARKit's raw feature points, which the app doesn't collect yet. The frosted strip still draws while the shaders compile at launch and if Metal fails. The walk screen's card, buttons and tape map are unchanged.

ScanEngine gains only the hooks that feed and expose the dots: integrate a kept keyframe's depth, apply an anchor correction, reset.

Why

The fog is guidance, never proof, so it follows coverage rather than depth. With LiDAR, depth reaches everything within 5 m, so a fog that lifted on depth barely showed and would have called unseen wall clear.

UI changes

Hosted-CI Simulator, iPhone 17, on the synthetic LiDAR fixture. Before is t3/ios-mvf at f14947e, run 36298253331. After is this branch at f57310c, the first attempt of this pull request's run 36310379917.

Before After
Before: the frosted strip over the wall during the walk After: soft fog lifting as the walk covers the wall, dots on the wall and ground
Before: a gap request over the frosted strip After: a gap request, the ground in front of the bin in amber, the bin outlined in violet

The bin in front of the wall, beside the prototype's still of the same keyframe:

Prototype After
Prototype keyframe 31: the bin outlined in dots After: the bin in violet dots with a dashed violet outline

The full comparison, from the start of the walk to near done against the prototype's keyframes, is on Sam's Mac at ~/house-scanning-data/reports/experience/app-port/index.html.

Verification

  • swift test --package-path ios/HouseScanKit --filter SurfaceDotsTests: 8 passed. They cover a plain wall giving only flat dots, the wall-to-ground crease, a box as a violet occluder with silhouette edges, opacity rising with views, low confidence and far depth ignored, the voxel and dot caps, corrections of 2.1 cm moving dots exactly, and determinism.
  • Hosted iOS run 36310379917 at f57310c, labelled full-ui: the second attempt passed everything, the accessibility audit included. The first attempt failed the audit on the non-LiDAR walk, with "Can't get there" reported low-contrast, though the card and the button are opaque, so the fog behind them can't reach them. This button failed the same way before, on frames caught mid-transition (8a9ee81, 397b6d6). The earlier run 36307517628 at 9defff1 failed only testAddSomethingFromTheReviewMarksOnTheCamera (feature.ac below the review's fold), which draws no fog and fails the same way on beta without this change. The new testLiveFogWalkScreenshots screenshots the LiDAR walk.
  • Offscreen renders of the same renderer on the fixture, on an M4 Pro Mac: the fog and 4,920 dots take 0.2 to 0.7 ms of GPU per 1170 x 2532 frame, and fusing one keyframe's depth 3 to 4 ms. Nothing was measured on a phone. Additive dots on a transparent layer and Reduce Motion are untested on a device.

Risk

  • Bounds, all allocated once: at most 5,000 dots, 60,000 voxels (about 6 MB), about 4.5 MB of buffers and 1 MB of textures. A keyframe that arrives while two wait to be fused is dropped.
  • The overlay reads the pose at 30 Hz, as the other overlays do, so the dots can trail the camera slightly during a fast turn.
  • t3/live-fog-beta (6836db7) is this branch's live-fog commits on beta with its three conflicts resolved, for the beta QA merge. This branch also carries a merge of the current t3/ios-mvf, which beta doesn't have.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • I included before/after images for visible changes
  • I included a video for motion or interaction changes: the fog's lift and the dots' births are motion, and no video is attached. The hosted runner produces stills, and no phone recording exists yet.

🤖 Generated with Claude Code

SamGu-NRX and others added 6 commits September 27, 2026 03:33
The fog lifts only where the coverage map counts a cell, in wall columns and
ground stretches, feathered and textured as in the prototype. LiDAR phones get
dots fused from the depth of the keyframes coverage observes. Marks get the
prototype's corner brackets. The frosted strip stays as the fallback while the
shaders compile and if Metal fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The offscreen preview on the LiDAR fixture showed lifted columns as straight
strips and a hidden stretch's violet climbing the whole column. The mask is
now read through a noise offset and blurred at 64 texels across, and only the
coverage band carries the violet and amber.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review: a cell narrower than a mask texel could drop out of the fog, so
the mask is drawn at three times the size, max-pooled, and sets a floor the
blur and noise can't erase. The dot field now keeps its own frame and a
cumulative transform, so anchor corrections no longer snap to the 5 cm grid.
A renderer that can't allocate hands back to the frosted strip, every piece of
a split gap request gets its outline, and corrections coalesce their republish.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	ios/HouseScan/Runtime/ScanEngine.swift
A taste check of the hosted screenshots against the prototype found partial
fog smearing, the hidden state greying out the bin, and dots too small to
glow. The fog now has a tighter feather, finer ragged edges, erosion only
while it lifts, a thinner seen-once veil and a whiter colour; hidden is a
light violet tint; dots are larger with a visible halo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T09:50:07.563250Z f57310c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the size:XXL 1,000+ effective changed lines. label Sep 27, 2026
@SamGu-NRX SamGu-NRX added the full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). label Sep 27, 2026
@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add house-scanning-master to your team's repositories.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c8e96038-5d6f-4bd5-9368-d56bd6bd98d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@greptileai

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds live fog rendering and LiDAR dot visualization to the camera overlay.

The PR is not ready to merge until a relocalization reset also clears the old-world dot field.

Fix All in CodexFindings

  1. P1 Dots survive spatial reset ▶
  2. P2 Screenshots cannot detect rendering failures ▶
Fix with agent prompt
### Issue 1
ios/HouseScan/Runtime/ScanEngine.swift:1598
When relocalization times out during a wall walk, `resetSpatialState` discards the old wall and coverage, but unlike this reset path, it does not reset the dot field or clear `state.liveDots`. When the homeowner starts again from the meter, new keyframes fuse depth into dots from the old world frame, so dots can appear in the wrong places. Reset the feed and published dots during spatial reset too.

### Issue 2
ios/HouseScanUITests/FullFlowUITests.swift:76-83
This test checks that the walk screen appears, then attaches screenshots without checking what they show. It passes if shader preparation fails and the fallback strip appears, or if the Metal layer draws no fog or dots. An assertion about the rendered state or pixels would let the test catch regressions in the feature it covers.

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!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

The PR replaces the walk’s frosted strip with a coverage-driven Metal fog, adds depth-fused LiDAR dots and camera marks, and retains the strip as a fallback.

  • The dot field needs to be cleared when relocalization restarts the scan’s world frame.
  • The new screenshot test captures the walk but does not verify that the live fog rendered.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  K[Kept keyframe] --> C[Coverage map]
  K --> D[LiveDotsFeed]
  C --> F[Fog mask]
  D --> V[Surface dots]
  F --> R[Metal camera overlay]
  V --> R
  X[Spatial reset] --> C
  X -. missing reset .-> D
Loading

Reviews (1) · Last reviewed commit: "Tune the fog, the hidden tint and the do..."

uploadTask?.cancel()
replay?.stop()
coverage = nil
liveDots.reset()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dots survive spatial reset When relocalization times out during a wall walk, resetSpatialState discards the old wall and coverage, but unlike this reset path, it does not reset the dot field or clear state.liveDots. When the homeowner starts again from the meter, new keyframes fuse depth into dots from the old world frame, so dots can appear in the wrong places. Reset the feed and published dots during spatial reset too.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScan/Runtime/ScanEngine.swift
Line: 1598

Comment:
**Dots survive spatial reset** When relocalization times out during a wall walk, `resetSpatialState` discards the old wall and coverage, but unlike this reset path, it does not reset the dot field or clear `state.liveDots`. When the homeowner starts again from the meter, new keyframes fuse depth into dots from the old world frame, so dots can appear in the wrong places. Reset the feed and published dots during spatial reset too.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caretaker check at f57310c: confirmed, and new in this PR. A relocalization timeout calls resetSpatialState (ScanEngine.swift:1113-1169), which restarts ARKit with .resetTracking but keeps LiveDotsFeed's SurfaceDots field and state.liveDots. Depth from the new world frame then fuses into old-frame dots. resetAll already clears both (lines 1598-1599). The effect is on the overlay only; capture, coverage and export never read the dots. This is a reset bug, separate from the deferred dots/veil redesign.

The three-line fix (the same two calls in resetSpatialState) is prepared on a caretaker branch for the owner to pick up; it isn't pushed here. The app has no unit-test target, so the fix has only been checked against the source: the serial queue and the generation guard make reset() drop queued old-frame publishes, as they do for Start over.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: the prepared fix, 010f322 on the caretaker branch codex/greptile-158-dot-reset (local, not pushed), builds. A Debug iphonesimulator xcodebuild of the HouseScan scheme succeeded through the shared QA queue (job greptile-158-build-010f322-r2, 20:07–20:09 UTC). That shows it compiles, not that the reset behaves correctly on a device.

Comment on lines +76 to +83
let start = Date()
var index = 0
while Date().timeIntervalSince(start) < 16 {
let shot = XCTAttachment(screenshot: app.screenshot())
shot.name = String(format: "liveFog-%02d-%04.1fs", index, Date().timeIntervalSince(start))
shot.lifetime = .keepAlways
add(shot)
index += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Screenshots cannot detect rendering failures This test checks that the walk screen appears, then attaches screenshots without checking what they show. It passes if shader preparation fails and the fallback strip appears, or if the Metal layer draws no fog or dots. An assertion about the rendered state or pixels would let the test catch regressions in the feature it covers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanUITests/FullFlowUITests.swift
Line: 76-83

Comment:
**Screenshots cannot detect rendering failures** This test checks that the walk screen appears, then attaches screenshots without checking what they show. It passes if shader preparation fails and the fallback strip appears, or if the Metal layer draws no fog or dots. An assertion about the rendered state or pixels would let the test catch regressions in the feature it covers.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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!

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caretaker check at f57310c: a valid test gap. FullFlowUITests.swift:61-86 asserts only that screen.wallWalk appears, so a fallback strip or blank Metal output passes. LiveFogSupport.status (LiveFogView.swift:10-43) already tracks preparing, ready and failed, but it isn't exposed to tests. Exposing it, for example as an accessibility value, and asserting ready would catch shader failure. Proving that pixels were drawn would need an offscreen check. Low severity; not blocking.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is a valid, low-severity test gap: testLiveFogWalkScreenshots only proves that screen.wallWalk appears, so shader failure (or an empty Metal layer) can still pass while the screenshots are retained for visual comparison. Exposing LiveFogSupport.status through a test-visible accessibility value and asserting ready would catch the shader/fallback path; pixel-level verification is better handled by the renderer’s offscreen tests.

SamGu-NRX added a commit that referenced this pull request Oct 2, 2026
A relocalization timeout (ScanEngine.swift:1500) and camera access after a failure (:477) call
resetSpatialState, which restarts ARKit's world frame but kept the SurfaceDots field, so depth
from the new frame fused into dots from the old one. resetAll already clears both
(:2211-2212); the spatial reset now does the same. These are #195's three lines (010f322),
handed over by the legacy caretaker as c6184ff2 and accepted by root; nothing from #158 comes
with them.

Co-Authored-By: Claude Code <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). size:XXL 1,000+ effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant