Skip to content

Commit 18187fa

Browse files
authored
fix: render Tekla rebar IfcSweptDiskSolid bars correctly (#1348, #1350) (#1351)
* fix(geometry): sample trimmed-circle directrix arcs in 3D (#1348) IfcSweptDiskSolid rebar (IfcReinforcingBar) whose directrix is an IfcCompositeCurve with arc bends rendered mangled — the L-bar grew a spurious hook, the U-bar twisted into a crumpled mess. Root cause: a directrix segment that is an IfcTrimmedCurve over an IfcCircle/IfcEllipse was sampled through the 2D conic path and lifted with z=0, dropping the arc's out-of-plane component. The bend arcs in these files live in the XZ plane (the circle's IfcAxis2Placement3D has a non-Z axis), so the flattened arc landed in the wrong plane and broke the directrix that the disk is swept along. Fix: route trimmed circle/ellipse directrix segments through a new process_trimmed_conic_3d that samples the arc against the conic's full 3D placement (center + X/Y axes), honouring parameter and cartesian trim bounds and matching the 2D sampler's segment-density budget. The 3D placement reader is shared with the existing full-circle sampler; the planar (IfcAxis2Placement2D) branch now also honours RefDirection. Both the trimmed and non-trimmed composite-curve directrix paths route through the fixed sampler. Adds neutral, self-contained L-bar and U-bar fixtures (faithful copies of the reported geometry chains) plus regression tests asserting the directrix stays in its authored plane and the swept tube stays thin. * test(geometry): add crank-bar (shape 26) regression for #1350 Issue #1350 (crank bar renders nothing when viewed alone) shares the #1348 root cause: a trimmed-circle directrix arc flattened to z=0. Here the two bends are tiny (~4.7°), so the pre-fix mangling was subtle (directrix Y deflection ~10.7 mm) but the resulting self-intersecting tube degenerated enough that the viewer dropped it entirely. Adds a neutral crank-bar fixture (faithful copy of the reported geometry: line, tiny arc, offset run, tiny arc, line) plus a directrix-planarity test (catches the regression: max |y| 10.7 mm pre-fix vs ~0 fixed) and a renders-non-empty test. With the directrix sampled in 3D the crank produces a clean 384-triangle swept tube. * fix(viewer): keep small high-aspect elements compact-framed (#1350) A single IfcReinforcingBar viewed alone loaded correct geometry but rendered as nothing. The camera-fit policy classifies any bounding box with aspect > 50:1 as "linear infrastructure" and frames it from inside the bbox looking down the longest axis — the right pose for a 900 m railway alignment, but for a 4.86 m rebar (aspect ~130:1) it points the camera end-on down the bar so it projects to a sub-pixel smear. Gate the linear policy on an absolute size floor: the longest axis must be at least 100 m. Below that the compact SE-isometric pose frames the whole element (as it already did for the lower-aspect L-bar). The floor is overridable for tests. Railway/road alignments (hundreds of metres) are unaffected; the smallest existing linear-branch test is 100 m. Adds changesets: the swept-disk arc 3D fix ships in @ifc-lite/wasm and this camera-fit fix in @ifc-lite/renderer. Together with the geometry fix this closes #1350.
1 parent 0b73ebb commit 18187fa

9 files changed

Lines changed: 674 additions & 26 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ifc-lite/renderer": patch
3+
---
4+
5+
Keep small high-aspect elements on the compact camera-fit pose. The linear-infrastructure fit policy (camera positioned inside the bbox looking down the longest axis) is meant for railway / road alignments hundreds of metres long, but it triggered on any high-aspect bounding box regardless of absolute size. A single reinforcing bar viewed alone (e.g. a 4.86 m bar, aspect ~130:1) got framed end-on from inside its own bounding box and rendered as nothing (issue #1350). The linear policy now requires the longest axis to be at least 100 m; below that the compact SE-isometric pose frames the whole element. Fixes the rendering half of #1350.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ifc-lite/wasm": patch
3+
---
4+
5+
Sample `IfcSweptDiskSolid` directrix arcs in full 3D. A directrix segment that is an `IfcTrimmedCurve` over an `IfcCircle`/`IfcEllipse` was sampled through the 2D conic path and lifted with `z = 0`, dropping the arc's out-of-plane component. Rebar bend arcs (Tekla `IfcReinforcingBar` bodies) live in the XZ plane, so the flattened arc landed in the wrong plane and twisted the swept tube — L-bars grew a spurious hook and U-bars crumpled (issue #1348). The arc is now sampled against the conic's real 3D placement (centre + X/Y axes), honouring parameter and cartesian trim bounds. Fixes #1348 and the geometry half of #1350.

packages/renderer/src/camera-fit-policy.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,38 @@ describe('pickFitPolicy', () => {
5656
assert.strictEqual(policy.kind, 'compact');
5757
assert.strictEqual(policy.aspect, 10);
5858
});
59+
60+
it('keeps a small high-aspect element compact (single rebar, issue #1350)', () => {
61+
// A 4.86 m × 0.084 m × 0.037 m reinforcing bar viewed alone — aspect
62+
// ~131:1, well past the linear threshold, but only metres long. The
63+
// linear "look down the axis from inside the bbox" pose framed it
64+
// end-on and it rendered as nothing. The absolute size floor must keep
65+
// it compact so the SE-isometric pose shows the whole bar.
66+
const policy = pickFitPolicy(
67+
bounds(2005.06, 5961.1, 25.5, 2005.097, 5961.184, 30.36),
68+
{ fovY: FOV_45 },
69+
);
70+
assert.strictEqual(policy.kind, 'compact');
71+
assert.ok(policy.aspect > 100, `aspect ${policy.aspect} should exceed 100`);
72+
// distance = longest * 2; longest ≈ 4.86, so ≈ 9.72 — a normal
73+
// see-the-whole-bar framing, not an inside-the-bbox linear pose.
74+
assert.ok(
75+
policy.distance > 9 && policy.distance < 11,
76+
`distance ${policy.distance} should be ~2x the 4.86 m length`,
77+
);
78+
});
79+
80+
it('honours the linearMinLongest floor override', () => {
81+
// 80 m × 1 m × 1 m — aspect 80, above the aspect threshold but below
82+
// the 100 m default size floor → compact. Lowering the floor to 50 m
83+
// lets it cross into linear.
84+
const b = bounds(0, 0, 0, 80, 1, 1);
85+
assert.strictEqual(pickFitPolicy(b, { fovY: FOV_45 }).kind, 'compact');
86+
assert.strictEqual(
87+
pickFitPolicy(b, { fovY: FOV_45, linearMinLongest: 50 }).kind,
88+
'linear',
89+
);
90+
});
5991
});
6092

6193
describe('linear branch', () => {

packages/renderer/src/camera-fit-policy.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ import type { Vec3 } from './raycaster.js';
3030
/** Aspect ratio above which a model is treated as linear infrastructure. */
3131
const LINEAR_ASPECT_THRESHOLD = 50;
3232

33+
/**
34+
* Minimum longest-axis extent (world units, i.e. metres) for the linear
35+
* policy to apply. The linear "look down the longest axis from inside the
36+
* bbox" pose only makes sense for genuine infrastructure (railway / road
37+
* alignments are hundreds of metres). A small but high-aspect element — a
38+
* single 4.86 m reinforcing bar is aspect ~130:1 — would otherwise get the
39+
* linear pose, which places the camera *inside* its bounding box looking
40+
* end-on, so the bar projects to a sub-pixel smear and reads as "nothing
41+
* rendered" (issue #1350). Below this floor the compact SE-isometric pose
42+
* frames the whole element and keeps it visible. Picked so the longest
43+
* building elements (rebar, steel members, long beams ≲ tens of metres)
44+
* stay compact while alignments (≥ hundreds of metres) stay linear.
45+
*/
46+
const LINEAR_MIN_LONGEST = 100;
47+
3348
/**
3449
* Target on-screen projection for the smallest non-degenerate dim, in
3550
* pixels. The linear-policy distance is chosen so the shortest meaningful
@@ -78,6 +93,12 @@ export interface PickFitPolicyOptions {
7893
* at the default; exposed for tests that pin the threshold behaviour.
7994
*/
8095
linearAspectThreshold?: number;
96+
/**
97+
* Override the minimum longest-axis extent (world units) at/above which the
98+
* linear policy is allowed to apply. Production should leave this at the
99+
* default; exposed for tests that pin the size-floor behaviour.
100+
*/
101+
linearMinLongest?: number;
81102
}
82103

83104
/**
@@ -106,8 +127,14 @@ export function pickFitPolicy(
106127
const shortest = Math.min(sizeX, sizeY, sizeZ);
107128
const aspect = longest / Math.max(shortest, longest * 1e-6);
108129
const threshold = options.linearAspectThreshold ?? LINEAR_ASPECT_THRESHOLD;
109-
110-
if (aspect <= threshold) {
130+
const minLongest = options.linearMinLongest ?? LINEAR_MIN_LONGEST;
131+
132+
// Compact unless the bbox is BOTH high-aspect AND large enough to be real
133+
// infrastructure. The size floor stops a small high-aspect element (a
134+
// single rebar / steel member) from getting the "look down the axis from
135+
// inside the bbox" linear pose, which renders it end-on and invisible
136+
// (issue #1350).
137+
if (aspect <= threshold || longest < minLongest) {
111138
// Compact: reproduce the legacy SE isometric pose 1:1 so building
112139
// models frame exactly as before.
113140
const distance = longest * 2.0;

0 commit comments

Comments
 (0)