Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ios/HouseScan/Contract/ScanContract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ final class ScanViewState {
var lastCapture: CaptureEvent?

var features: [MarkedFeature] = []
/// Marks lying wholly past a marked end, where the scan doesn't cover them. The review says
/// so; they are still exported, since a hazard just past an end can be within clearance of a
/// spot at it.
var featuresPastEnds: Set<UUID> = []
var marking: MarkingState?

var gap: GapRequest?
Expand Down
1 change: 1 addition & 0 deletions ios/HouseScan/Runtime/ScanEngine+Actions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ extension ScanEngine: ScanActions {
let marked = feature(marking.kind, taps: pendingTaps, wall: wall)
markTimes[MarkKey.feature(marked.id)] = captureClock
state.features.append(marked)
publishFeaturesPastEnds()
state.marking = nil
pendingTaps = []
}
Expand Down
9 changes: 9 additions & 0 deletions ios/HouseScan/Runtime/ScanEngine+Ends.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ extension ScanEngine {
let covered = GuidancePlanner().reach(side.walk, coverage: map)
RuntimeLog.engine.info("\(action, privacy: .public) on the \(side.rawValue, privacy: .public): end at s=\(s) (phone at s=\(phone), walked \(walked) m, covered reach \(covered) m)")
}

/// `ScanViewState.featuresPastEnds`, refreshed when the ends move (`publishWall`), the spans
/// follow the wall (`reprojectFeatures`) and a mark is added.
func publishFeaturesPastEnds() {
let left = coverage?.leftEnd
let right = coverage?.rightEnd
let past = Set(state.features.filter { WalkedEnd.liesPastAnEnd($0.span, leftEnd: left, rightEnd: right) }.map(\.id))
if past != state.featuresPastEnds { state.featuresPastEnds = past }
}
}

extension WallSide {
Expand Down
2 changes: 2 additions & 0 deletions ios/HouseScan/Runtime/ScanEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ final class ScanEngine {
var features = state.features
for index in features.indices { Self.project(&features[index], onto: wall) }
if features != state.features { state.features = features }
publishFeaturesPastEnds()
}

private func refreshMeterFromAnchor(_ frame: SourceFrame) {
Expand Down Expand Up @@ -1151,6 +1152,7 @@ final class ScanEngine {
}
)
if state.phase == .resultAR { showResultInCamera(rising: false) }
publishFeaturesPastEnds()
}

/// "See it on your wall" on the live camera: the result goes into the AR scene on the meter's
Expand Down
3 changes: 3 additions & 0 deletions ios/HouseScan/UI/Copy/ScanCopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ enum ScanCopy {
}
}

/// Under a reviewed mark that lies wholly past a marked end (`ScanViewState.featuresPastEnds`).
static let featurePastEnd = "Past the end of your scan"

static func markingPrompt(_ marking: MarkingState) -> Instruction {
let item = noun(marking.kind)
switch (marking.kind, marking.step) {
Expand Down
14 changes: 13 additions & 1 deletion ios/HouseScan/UI/Screens/MarkFeaturesScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ struct MarkFeaturesScreen: View {
} else {
VStack(spacing: 10) {
ForEach(state.features) { feature in
FeatureRow(feature: feature, actions: actions)
FeatureRow(feature: feature, pastEnd: state.featuresPastEnds.contains(feature.id), actions: actions)
.transition(.opacity.combined(with: .scale(scale: 0.98)))
}
}
Expand Down Expand Up @@ -147,6 +147,7 @@ struct MarkFeaturesScreen: View {

private struct FeatureRow: View {
var feature: MarkedFeature
var pastEnd: Bool
var actions: any ScanActions

var body: some View {
Expand All @@ -164,6 +165,17 @@ private struct FeatureRow: View {
Text(Distance.aroundFromMeter(feature.span).prefix(1).uppercased() + Distance.aroundFromMeter(feature.span).dropFirst())
.font(.subheadline)
.foregroundStyle(Palette.muted)
if pastEnd {
Label {
Text(ScanCopy.featurePastEnd)
} icon: {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Palette.caution)
.accessibilityHidden(true)
}
.font(.subheadline)
.foregroundStyle(.primary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
Expand Down
11 changes: 11 additions & 0 deletions ios/HouseScanKit/Sources/HouseScanKit/Guidance/WalkedEnd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,14 @@ extension CoverageMap {
return visibleCells(from: camera).isEmpty ? side : nil
}
}

extension WalkedEnd {
/// Whether a mark's span (meters of s) lies wholly past a marked end. The scan doesn't cover
/// it there: the wall may turn or stop at that end, so the review says so (issue #42). A mark
/// reaching an end, or with any of it between the ends, is on the scanned wall.
public static func liesPastAnEnd(_ span: ClosedRange<Float>, leftEnd: Float?, rightEnd: Float?) -> Bool {
if let leftEnd, span.upperBound < leftEnd { return true }
if let rightEnd, span.lowerBound > rightEnd { return true }
return false
}
}
18 changes: 18 additions & 0 deletions ios/HouseScanKit/Tests/HouseScanKitTests/WalkedEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,22 @@ import Testing
#expect(groundSeen.contains { $0.span.upperBound < -3 || $0.span.lowerBound < -3 })
#expect(groundSeen.contains { $0.span.upperBound > 3 })
}

/// Field run 2: the wall ran from 7 ft 6 in left of the meter (-2.29 m) to 6 in right
/// (0.15 m), and the review still listed an AC unit about 16 ft right and a window about 7 ft
/// right as if they were on it (issue #42).
@Test func marksWhollyPastAnEndLiePastIt() {
let left: Float = -2.29
let right: Float = 0.15
#expect(WalkedEnd.liesPastAnEnd(4.8...5.0, leftEnd: left, rightEnd: right))
#expect(WalkedEnd.liesPastAnEnd(1.8...2.4, leftEnd: left, rightEnd: right))
#expect(WalkedEnd.liesPastAnEnd(-3.5 ... -2.5, leftEnd: left, rightEnd: right))
// Reaching an end, straddling one, or between them: on the scanned wall.
#expect(!WalkedEnd.liesPastAnEnd(0.15...0.6, leftEnd: left, rightEnd: right))
#expect(!WalkedEnd.liesPastAnEnd(-2.6 ... -2.0, leftEnd: left, rightEnd: right))
#expect(!WalkedEnd.liesPastAnEnd(-0.5...0.1, leftEnd: left, rightEnd: right))
// A side without an end has nothing to lie past.
#expect(!WalkedEnd.liesPastAnEnd(4.8...5.0, leftEnd: left, rightEnd: nil))
#expect(!WalkedEnd.liesPastAnEnd(-3.5 ... -2.5, leftEnd: nil, rightEnd: nil))
}
}
Loading