The first time I used RoomPlan, I scanned the same small room three times and got three different wall lengths: 3.62m, 3.57m, 3.64m. A few centimeters of difference, but if you are building an app that draws a floor plan, you cannot expose that drift to the user. People do not trust a tool whose numbers change every time they measure.
Now that Rork Max can generate native Swift, a LiDAR-based measuring app suddenly came within reach for indie developers. But ship the generated code as-is and that drift lands directly in the user's hands. Here I share how I learned to absorb scan variance by design, working through a small measuring app of my own.
Why RoomPlan numbers drift
RoomPlan sits on top of ARKit. It combines the LiDAR point cloud with planes estimated from the camera image to reconstruct the room's structure. In other words, the final dimensions are not a measurement but an estimate. Because it is an estimate, the result moves with the input conditions.
There are three main sources of drift. Lighting: against backlight or in the dark, feature points are scarce and plane estimation gets loose. Motion speed: sweep too fast and the point cloud thins out, delaying how edges are resolved. And the device itself: iPhone and iPad Pro differ in LiDAR resolution and field of view, so the same room reconstructs slightly differently.
The important thing is to not try to crush these as bugs. Estimation drift is the spec, and designing your app to absorb it produces a more stable result in the end.
A three-layer design that absorbs drift
I treat measurement data in three layers: raw, confirmed, and display.
The raw layer keeps the CapturedRoom that RoomPlan returns, untouched. Nothing is rounded here. The confirmed layer applies rounding and snapping to turn raw data into "dimensions the app takes responsibility for." The display layer shows the user only the confirmed values.
With this separation, when you later want to change the rounding rules, you can redo it without throwing away the raw data. Scanning is an effort for the user, so a design that reduces re-scans directly improves the experience.
Rounding and snapping in the confirmed layer
The confirmed layer has two core operations. One is rounding to 5cm units. The other is snapping toward right angles and parallels. Real room walls are made almost entirely of right angles and parallels, so if the estimate returns 88 or 92 degrees, nudging it to 90 produces a more natural drawing.
import RoomPlan
import simd
struct ConfirmedDimension {
let widthMeters: Double
let lengthMeters: Double
let cornerAngles: [Double] // each corner angle, in degrees
}
enum DimensionResolver {
// Round to 5cm so users see a granularity they can trust
static func snapLength(_ raw: Double) -> Double {
let grid = 0.05
return (raw / grid).rounded() * grid
}
// Snap toward right angles. Within +/-4 degrees, pull to a multiple of 90
static func snapAngle(_ rawDeg: Double) -> Double {
let nearest = (rawDeg / 90.0).rounded() * 90.0
return abs(rawDeg - nearest) <= 4.0 ? nearest : rawDeg
}
static func resolve(from room: CapturedRoom) -> ConfirmedDimension {
let walls = room.walls
let lengths = walls.map { Double($0.dimensions.x) }.sorted(by: >)
let width = snapLength(lengths.first ?? 0)
let length = snapLength(lengths.dropFirst().first ?? 0)
let angles = walls.map { wall -> Double in
let yaw = atan2(wall.transform.columns.0.z, wall.transform.columns.0.x)
return snapAngle(yaw * 180 / .pi)
}
return ConfirmedDimension(widthMeters: width, lengthMeters: length, cornerAngles: angles)
}
}The +/-4 degrees threshold is a value I settled on after scanning a few times on my own devices. Snap across too wide an angle and rooms with genuinely diagonal walls break. Too narrow and you fail to absorb the drift. Carry this as a number you will tune for the kinds of rooms your app handles.
Carry a confidence value alongside
If you hand over only the rounded numbers, the display layer cannot judge how much to trust them. I attach a confidence score to the confirmed value. Even a rough calculation from point-cloud density and scan time is enough to drive what the display shows.
struct DimensionWithConfidence {
let dimension: ConfirmedDimension
let confidence: Double // 0.0 to 1.0
var needsRescanHint: Bool { confidence < 0.6 }
}When needsRescanHint is set, the display layer adds "scanning again slowly will improve accuracy." Rather than hiding the numbers, honestly conveying their certainty is what earns the user's trust in the tool.