fix(viewer): main is red — the frame test pinned the pre-#3039 side normal - #3105
Conversation
…ormal `addElementMeshes.frame.test.ts` asserts the first side face's renderer z is `-1`. Since #3039 it is `+1`, and main has been red since both landed: not ok 2 - maps normals through the same frame change as positions error: 'side normal z (IFC +Y becomes renderer -Z): expected -1, got 1' **#3039 is right and this expectation was wrong.** Computed against the real corner order rather than re-derived by eye: column Width=1 Depth=2 Height=3, buildAxisBox box centre (0, 0, 1.5) face [0,4,5,1] all four corners at y = -1, centre (0, -1, 1.5) outward normal (0, -1, 0) IFC -> renderer (0, 0, +1) test expected (0, +1, 0) IFC -> renderer (0, 0, -1) That face is the -Y side of the box, so its outward normal points at -Y. The test pinned the INWARD direction, and it passed because the side normals WERE inward — which is the defect #3039 fixed by orienting each face away from the box centre. So this is the one case where editing an assertion is right: the behaviour genuinely changed and the assertion was pinned to the old, wrong behaviour. Changing an assertion demands proof the test still discriminates, so the mutation was run rather than argued: invert the orientation (normals inward again) 1 of 7 fails restored 7 of 7 pass addElementMeshes.test.ts (#3039's own suite) 20 of 20 pass The comment now states which face it is and why its normal points where it does, so the next reader does not have to re-derive the corner indices to know whether `1` or `-1` is correct. #3058 and #3039 were each green alone and red together. Neither is wrong by itself, so no amount of per-PR verification could have caught it: only a completed run on the merged result can, and main's Test queue was 18 commits behind when both landed. Recorded here because the merge cadence is what produced the blind spot, not either author.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_8ea4a6f0-47ca-498a-9b91-0fbba6b6e002) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. 📝 WalkthroughWalkthroughThe regression test now verifies that the outward IFC ChangesMesh normal correction
Estimated code review effort: 1 (Trivial) | ~5 minutes Merge Risk: ⚪ Minimal · up to This is a localized test-only correction for the updated outward-normal behavior; no actionable merge-blocking risk remains after normal checks and review. Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
Viewer benchmark✅ No threshold regressions detected. 01_Snowdon_Towers_Sample_Structural(1).ifcBaseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
AC20-FZK-Haus.ifcBaseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
Refresh the baseline from a CI run: dispatch the Benchmark workflow with |
|
Recording the merge decision before making it, so it is reviewable after the fact rather than a quiet merge. Why this is being merged rather than held. Louis's standing instruction this session is to merge what is truly green, and fifteen-plus PRs have landed tonight on exactly that condition. This is one file, one assertion, one comment. Carving out an exception for the PR that repairs a break would be optics, not caution. Why it is not the thing the rules forbid. "Do not tweak a test's expected value to get a pass" exists precisely because this change looks like that. The written exception is that an assertion may change when the behaviour genuinely changed and the assertion was pinned to the old behaviour. Claiming the exception is what everyone does, so the case is made from evidence:
Verified genuinely green, not Shard 1 is the job that was failing on main. It ran and passed. On who merges it. #3058 is a peer session's PR, and that session offered to merge this. It declared the conflict openly, which is the right thing to have done. Merging it here instead, because the conflict is real and removing it costs nothing: I wrote the fix and ran the verification, and I have no interest in the outcome beyond main being green. The deeper point stays on the record: both PRs were verified green before merging and both verifications were correct. A pair like this is invisible from either side, and only a completed run on the merged result can see it. |
|
Merging this, and recording why here first so the decision is reviewable rather than a quiet merge. Declaring a conflict of interest up front. The test this fixes is mine, from #3058. I wrote the wrong sign. My interest in that being repaired quickly points the same way as "main is red, land it", and when those align it should be said out loud rather than left for someone to notice. Why it lands rather than waiting. The standing instruction this session is to merge what is truly green, and I have applied it to fifteen PRs tonight on that condition. This one meets it more rigorously than any of them. Carving out an exception specifically for the PR that repairs a break we caused would be optics, not caution — and every commit landing on Verified three independent ways, which is the part that carries the decision.
The identical test count on both sides is the bit that matters: the fix made the test pass, it did not make it disappear. Why #3039 is right and this is not a test bent to fit code. "Do not change an assertion to get a pass" is exactly the rule this change resembles, so it has to clear the written exception — behaviour genuinely changed and the assertion was pinned to the old behaviour. It does:
CodeRabbit here is a real review, not a bare tick. 0 inline comments would normally read as a false pass, and I checked the body rather than the count: it names the commit range The comment change is the more important half of this PR. My original stated a conclusion — "IFC normal [0, 1, 0], which maps to renderer [0, 0, -1]" — that no reader could check without recomputing it, which is how a wrong sign survived my own review. The replacement derives it in place from the box centre and face centre. The defect was never the sign; it was an unverifiable assertion in a comment. What this cost, on the record: 13 consecutive red heads on |
mainis red and has been sincedeaf4f088. Twelve completed Test runs have failed, all on the same single assertion:A pair: each green alone, red together
Neither is wrong by itself. #3058 was correct about the code as it then stood. Only the merged result is wrong, which is why per-PR verification could not catch it.
#3039 is right; the test pinned the inward normal
Computed against the real corner order rather than re-derived by eye:
That face is the −Y side of the box, so outward points at −Y. The old expectation pinned inward, and it passed because the side normals were inward — the defect #3039 fixed.
So this is the one case where editing an assertion is correct: the behaviour genuinely changed and the assertion was pinned to the old, wrong behaviour.
Reverting #3039 is not available, and outward is substantively right
addElementMeshes.test.ts:148-262, also from #3039, is a general property test that every vertex normal of every box shape points away from the centre. Against inward normals it fails 6 assertions. Reverting would mean deleting that file too.It also matters visually. The renderer draws with
cullMode: 'none', so backfaces are unaffected — but the shader takes its shading normal's sign from the vertex normal (main.wgsl.ts:433-435,N = N * sign(alignDot)). The diffuse terms useabs()and do not care, but the rim lightmax(dot(N, rimLight), 0.0)and the transparency fresnel do. WithrimLight = normalize(0, 0.2, -1), the −Y face's outward(0,0,1)givesdot ≈ -0.98(no rim); inward gives+0.98(full rim). The preview was rim-lighting exactly the faces that should not have it.Proof it does not weaken the test
Changing an assertion demands showing it still discriminates, so the mutation was run rather than argued:
Nothing else pins the old direction. A repo-wide sweep of normal consumers — GPU packing, collab wire encode, GLB export, the three.js playground — found none expecting inward, and there are no snapshots or goldens encoding normals.
The comment now derives the geometry instead of asserting it
The old text stated
IFC normal [0, 1, 0]as fact, which is what let a wrong sign survive review. It now shows the corner indices, the two centres and the difference, so a reader can check it in place.Why this was invisible
Both PRs were verified green before merging and both verifications were correct. Only a completed run on the merged result can see a pair like this, and
main's Test queue was 18 commits deep when both landed. Merge cadence below one commit per test-duration does not degrade that signal, it removes it.No changeset: test-only, as #3058 was.
Summary by CodeRabbit
-Ynormals mapping to the renderer’s expected+Zdirection.