-
-
Notifications
You must be signed in to change notification settings - Fork 97
fix(viewer): collapse IfcGridAxis line-overlay ownership to the symbolic path #3376
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
Open
BIMvoice
wants to merge
2
commits into
main
Choose a base branch
from
fix-3368
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+170
−169
Open
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@ifc-lite/viewer': patch | ||
| --- | ||
|
|
||
| Stop drawing every `IfcGridAxis` twice in the 3D viewport, which also made section-clipping of grid lines inert and let the two copies disagree in elevation. | ||
|
|
||
| The viewport fed the `ifcGrid`-visibility toggle from two independent sources at once: `useSymbolicAnnotations` (its grid buckets, section-clipped against the active cut plane and rebased by the TS-side `originShift`) and `useGridLines3D` (the wasm `parseGridLines` API, unclipped and rebased only by RTC). Both uploaded to their own renderer line-overlay channel whenever the toggle was on, so every axis drew twice, issue #862's grid section-clipping never had any effect (the unclipped copy always drew the full grid), and a federated or re-aligned model with a nonzero `originShift` could show the two copies at different elevations. | ||
|
|
||
| Grid lines in the viewport now draw only from `useSymbolicAnnotations`, which already section-clips and origin-shift-rebases its grid buckets. The redundant `useGridLines3D` hook is removed. `parseGridLines`/`parseGridAxes` themselves are unchanged — they remain published `@ifc-lite/geometry` SDK surface for embedders who want raw, unclipped grid geometry with no annotation/storey semantics. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
apps/viewer/src/components/viewer/grid-overlay-single-owner.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| /** | ||
| * Issue #3368: two independent extractors both produced `IfcGridAxis` line | ||
| * geometry for the 3D viewport, gated on the same `ifcGrid` toggle, and both | ||
| * drew: | ||
| * | ||
| * - `useSymbolicAnnotations` (backed by `rust/processing/src/symbolic/grid.rs`) | ||
| * section-clips its grid buckets against the active cut plane and applies | ||
| * the TS-side `originShift` elevation rebase (`elevationRebaseFor` in | ||
| * `symbolic-parse-cache.ts`) to every primitive, grid included — see | ||
| * `symbolic-parse.elevationFrame.test.ts`. | ||
| * - `useGridLines3D` (backed by `rust/wasm-bindings/src/api/grid_lines.rs`'s | ||
| * `parseGridLines`) drew the SAME axes unclipped and without that rebase. | ||
| * | ||
| * `Viewport.tsx` uploaded both as separate renderer line-overlay channels | ||
| * ('annotation' and 'grid') whenever `ifcGridVisible` was on. Consequences: | ||
| * every axis was drawn twice, issue #862's section-clipping of grid lines | ||
| * was inert (the unclipped copy always drew the full grid), and for a | ||
| * federated/re-aligned model with nonzero `originShift` the two copies sat | ||
| * at different elevations. | ||
| * | ||
| * The fix collapses viewer grid-line ownership onto the symbolic path only | ||
| * (already clip-aware and origin-shift-aware) and retires the redundant | ||
| * `useGridLines3D` hook and its upload in `Viewport.tsx`. This test pins | ||
| * that collapse structurally: it fails as long as a second, independent | ||
| * grid-line source is wired into the viewport. | ||
| * | ||
| * `parseGridLines` / `parseGridAxes` themselves stay — they're published SDK | ||
| * surface (`packages/geometry`) for embedders who want raw, unclipped grid | ||
| * geometry with no annotation/storey semantics. This test is about the | ||
| * VIEWER's internal wiring, not the wasm API. | ||
| */ | ||
|
|
||
| import { describe, it } from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import { existsSync, readFileSync } from 'node:fs'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import path from 'node:path'; | ||
| import { buildParseResult } from '../../lib/overlay-parse/symbolic-parse.js'; | ||
| import { createEmptyFlatSymbolic, type FlatSymbolic } from '../../lib/overlay-parse/symbolic-flat.js'; | ||
|
|
||
| const dir = path.dirname(fileURLToPath(import.meta.url)); | ||
| const viewportSource = readFileSync(path.join(dir, 'Viewport.tsx'), 'utf8'); | ||
| const hookPath = path.join(dir, '../../hooks/useGridLines3D.ts'); | ||
|
|
||
| describe('grid line overlay has one owner (issue #3368)', () => { | ||
| it('Viewport does not import the redundant raw grid-line hook', () => { | ||
| assert.ok( | ||
| !/from ['"][^'"]*useGridLines3D(\.js)?['"]/.test(viewportSource) && | ||
| !/\buseGridLines3D\s*\(/.test(viewportSource), | ||
| 'Viewport.tsx still imports/calls useGridLines3D — a second, unclipped, ' + | ||
| 'origin-shift-unaware IfcGridAxis source is wired into the viewport ' + | ||
| "alongside useSymbolicAnnotations' clipped grid buckets, so section " + | ||
| 'clipping stays inert and the two copies can disagree in Y (#3368).', | ||
| ); | ||
| }); | ||
|
|
||
| it("Viewport does not upload a separate, unclipped 'grid' line-overlay channel", () => { | ||
| assert.ok( | ||
| !/setLineOverlay\(\s*['"]grid['"]/.test(viewportSource), | ||
| "Viewport.tsx still uploads a 'grid' line-overlay channel independent " + | ||
| "of the clipped 'annotation' channel that already carries grid " + | ||
| 'buckets when ifcGrid is visible (#3368).', | ||
| ); | ||
| }); | ||
|
|
||
| it('the redundant useGridLines3D hook has been retired', () => { | ||
| assert.ok( | ||
| !existsSync(hookPath), | ||
| 'useGridLines3D.ts still exists — it has no remaining call site once ' + | ||
| 'the viewer draws grid lines from a single (symbolic) owner, so it ' + | ||
| 'is dead code that could be re-wired back into a second draw path.', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * Consequence 3 (issue #3368): "the copies can disagree in Y". Quantify the | ||
| * mechanism directly from the real code both sides used. | ||
| * | ||
| * - The surviving (symbolic) path bucketed a grid axis's `worldY` through | ||
| * `buildParseResult`'s `ensureBucket`, which subtracts | ||
| * `elevationRebase.primitive` — the TS-side `originShift` component | ||
| * (`elevationRebaseFor` in `symbolic-parse-cache.ts`) that a wasm | ||
| * primitive never carries, since the wasm extractor only ever removes the | ||
| * RTC Z (`rust/processing/src/symbolic/rebase.rs`). | ||
| * - The retired raw path (`useGridLines3D` -> wasm `parseGridLines`) handed | ||
| * that same RTC-only `worldY` straight to `renderer.setLineOverlay('grid', ...)` | ||
| * with NO further processing: `useGridLines3D.ts` (now deleted) never | ||
| * referenced `elevationRebase`/`originShift`/`totalYupOffset`, and neither | ||
| * did the `Viewport.tsx` effect that used to upload its output. | ||
| * | ||
| * A federated/re-aligned model sets a nonzero `originShift`, so | ||
| * `elevationRebase.primitive !== 0`, and the two Y values genuinely diverge — | ||
| * this is not hypothetical. A model needing no rebase (`primitive === 0`, | ||
| * the fixture's second case) hides the bug by accident, which is exactly why | ||
| * origin-frame symmetry must be avoided when reproducing this class of | ||
| * defect. | ||
| */ | ||
| describe('the two owners disagreed in Y for a re-aligned model (issue #3368)', () => { | ||
| const RTC_ONLY_WORLD_Y = 12.5; // wasm primitive.worldY: RTC Z already removed, nothing else. | ||
| const ORIGIN_SHIFT_Y = 3.75; // originShift.y for a re-aligned/federated model. | ||
|
|
||
| function flatWithOneGridAxis(worldY: number): FlatSymbolic { | ||
| const flat = createEmptyFlatSymbolic(); | ||
| flat.typeNames = ['IfcGridAxis']; | ||
| flat.polyPoints = Float32Array.from([0, 0, 10, 0]); | ||
| flat.polyStart = Uint32Array.from([0, 2]); | ||
| flat.polyOwner = Uint32Array.from([7]); | ||
| flat.polyWorldY = Float32Array.from([worldY]); | ||
| flat.polyFlags = Uint8Array.from([0]); | ||
| flat.polyType = Uint16Array.from([0]); | ||
| return flat; | ||
| } | ||
|
|
||
| it('a nonzero originShift moves the symbolic (clipped) copy away from the raw (unclipped) one', () => { | ||
| const result = buildParseResult(flatWithOneGridAxis(RTC_ONLY_WORLD_Y), { | ||
| elevationRebase: { primitive: ORIGIN_SHIFT_Y, storeyTable: ORIGIN_SHIFT_Y }, | ||
| }); | ||
| const buckets = [...result.gridByStorey.values()]; | ||
| assert.strictEqual(buckets.length, 1, 'one grid axis makes one bucket'); | ||
| const symbolicY = buckets[0].storeyElevation; | ||
| assert.ok(symbolicY !== null); | ||
| // The retired raw path returned RTC_ONLY_WORLD_Y verbatim -- no rebase. | ||
| const rawPathY = RTC_ONLY_WORLD_Y; | ||
| assert.ok( | ||
| Math.abs((symbolicY as number) - rawPathY) > 1e-6, | ||
| `the symbolic bucket (${symbolicY}) must diverge from the raw path's ` + | ||
| `unrebased value (${rawPathY}) by the origin shift (${ORIGIN_SHIFT_Y}), ` + | ||
| "reproducing #3368's \"copies can disagree in Y\"", | ||
| ); | ||
| assert.ok( | ||
| Math.abs((symbolicY as number) - (rawPathY - ORIGIN_SHIFT_Y)) < 1e-6, | ||
| `expected symbolic Y = raw Y - originShift.y = ${rawPathY - ORIGIN_SHIFT_Y}, got ${symbolicY}`, | ||
| ); | ||
| }); | ||
|
|
||
| it('a model needing no rebase hides the divergence by accident (why symmetry must be avoided)', () => { | ||
| const result = buildParseResult(flatWithOneGridAxis(RTC_ONLY_WORLD_Y), { | ||
| elevationRebase: { primitive: 0, storeyTable: 0 }, | ||
| }); | ||
| const buckets = [...result.gridByStorey.values()]; | ||
| const symbolicY = buckets[0].storeyElevation; | ||
| assert.ok( | ||
| Math.abs((symbolicY as number) - RTC_ONLY_WORLD_Y) < 1e-6, | ||
| 'with no origin shift the two paths coincidentally agree -- this is the ' + | ||
| 'symmetric case the reproduction must not rely on', | ||
| ); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Replace source-text assertions with behavior-level coverage.
This TypeScript test reads
Viewport.tsxand checks regular expressions against its source text. That violates the repository rule and makes the test sensitive to formatting and comments instead of runtime behavior.Test the rendered overlay ownership through a mocked renderer or a typed ownership seam. Keep the direct
buildParseResultregression tests.As per coding guidelines, TypeScript files must never assert on a source file's text.
🤖 Prompt for AI Agents
Source: Coding guidelines