|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +//! The `IfcLocalPlacement.PlacementRelTo` walk is implemented twice — once in |
| 6 | +//! `router::transforms` (mesh path) and once in `profile_extractor` (2D drawing |
| 7 | +//! path) — and both bound it with a depth cap whose exceed-branch returns the |
| 8 | +//! IDENTITY. Two caps over one chain therefore do not disagree loudly: they |
| 9 | +//! disagree by putting the same element in two different places, with no error |
| 10 | +//! on either side. #2873 |
| 11 | +//! |
| 12 | +//! These tests pin (a) each site's cap IS `ifc_lite_core::limits::MAX_PLACEMENT_DEPTH` |
| 13 | +//! and (b) the two walks agree on a chain deeper than that cap. (a) alone is not |
| 14 | +//! enough: a private `const MAX_PLACEMENT_DEPTH` shadows the import and nothing |
| 15 | +//! else notices, which is exactly how #2955's mapped-item family failed. |
| 16 | +
|
| 17 | +use super::*; |
| 18 | +use ifc_lite_core::limits::MAX_PLACEMENT_DEPTH; |
| 19 | +use ifc_lite_core::EntityDecoder; |
| 20 | + |
| 21 | +/// `links` chained `IfcLocalPlacement`s, each translating +1.0 in X, so the |
| 22 | +/// composed world X of the deepest one equals the number of placements the walk |
| 23 | +/// actually composed. That makes the truncation directly readable off the |
| 24 | +/// result instead of inferred. |
| 25 | +/// |
| 26 | +/// Ids: `#1` axis placement, `#2` its origin, `#10..=#(10 + links)` the chain |
| 27 | +/// (`#10` is the root, `#(10 + links)` the leaf). |
| 28 | +fn deep_placement_chain(links: usize) -> String { |
| 29 | + let mut s = String::from( |
| 30 | + "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\ |
| 31 | +FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');\n\ |
| 32 | +FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\ |
| 33 | +#1=IFCAXIS2PLACEMENT3D(#2,$,$);\n\ |
| 34 | +#2=IFCCARTESIANPOINT((1.,0.,0.));\n\ |
| 35 | +#10=IFCLOCALPLACEMENT($,#1);\n", |
| 36 | + ); |
| 37 | + for i in 1..=links { |
| 38 | + s.push_str(&format!("#{}=IFCLOCALPLACEMENT(#{},#1);\n", 10 + i, 9 + i)); |
| 39 | + } |
| 40 | + s.push_str("ENDSEC;\nEND-ISO-10303-21;\n"); |
| 41 | + s |
| 42 | +} |
| 43 | + |
| 44 | +/// World X of the leaf of a `links`-long chain, from each of the two walks. |
| 45 | +/// Each walk gets its OWN decoder: the router memoises composed placements on |
| 46 | +/// the decoder, and sharing one would let the first walk's answer be handed to |
| 47 | +/// the second, which is the one thing this test must not do. |
| 48 | +fn both_walks(links: usize) -> (f64, f64) { |
| 49 | + let content = deep_placement_chain(links); |
| 50 | + let leaf_id = (10 + links) as u32; |
| 51 | + |
| 52 | + let mut router_decoder = EntityDecoder::new(&content); |
| 53 | + let leaf = router_decoder.decode_by_id(leaf_id).expect("leaf placement"); |
| 54 | + let router = GeometryRouter::new(); |
| 55 | + let router_x = router |
| 56 | + .get_placement_transform(&leaf, &mut router_decoder) |
| 57 | + .expect("router placement transform") |
| 58 | + .column(3)[0]; |
| 59 | + |
| 60 | + let mut extractor_decoder = EntityDecoder::new(&content); |
| 61 | + let leaf = extractor_decoder |
| 62 | + .decode_by_id(leaf_id) |
| 63 | + .expect("leaf placement"); |
| 64 | + let extractor_x = |
| 65 | + crate::profile_extractor::get_placement_recursive(&leaf, &mut extractor_decoder, 0) |
| 66 | + .column(3)[0]; |
| 67 | + |
| 68 | + (router_x, extractor_x) |
| 69 | +} |
| 70 | + |
| 71 | +/// A chain the walk composes in full: both must report every link, which also |
| 72 | +/// proves the fixture measures what it claims before the truncation cases use it. |
| 73 | +#[test] |
| 74 | +fn both_walks_compose_a_chain_inside_the_cap_identically() { |
| 75 | + let links = MAX_PLACEMENT_DEPTH / 2; |
| 76 | + let (router_x, extractor_x) = both_walks(links); |
| 77 | + let expected = (links + 1) as f64; |
| 78 | + assert_eq!( |
| 79 | + router_x, expected, |
| 80 | + "the router must compose all {} placements of an in-cap chain", |
| 81 | + links + 1 |
| 82 | + ); |
| 83 | + assert_eq!( |
| 84 | + extractor_x, expected, |
| 85 | + "the extractor must compose all {} placements of an in-cap chain", |
| 86 | + links + 1 |
| 87 | + ); |
| 88 | +} |
| 89 | + |
| 90 | +/// THE divergence. A chain longer than the shared cap is truncated by both |
| 91 | +/// walks, and truncation is silent on both — so the only thing that can make |
| 92 | +/// the mesh path and the 2D path put an element in the same place is the two |
| 93 | +/// caps being equal. With the router at 32 and the extractor at 100 this |
| 94 | +/// returned 33.0 and 41.0 for a 40-link chain: same file, same element, two |
| 95 | +/// positions, no error. |
| 96 | +#[test] |
| 97 | +fn both_walks_truncate_a_chain_beyond_the_cap_at_the_same_link() { |
| 98 | + let links = MAX_PLACEMENT_DEPTH + 8; |
| 99 | + let (router_x, extractor_x) = both_walks(links); |
| 100 | + assert_eq!( |
| 101 | + router_x, extractor_x, |
| 102 | + "the mesh path and the 2D drawing path must place a {}-link chain \ |
| 103 | + identically; they differ by {} links of translation, and neither \ |
| 104 | + reports an error", |
| 105 | + links + 1, |
| 106 | + (router_x - extractor_x).abs() |
| 107 | + ); |
| 108 | + assert_eq!( |
| 109 | + router_x, |
| 110 | + (MAX_PLACEMENT_DEPTH + 1) as f64, |
| 111 | + "the cap admits depths 0..=MAX_PLACEMENT_DEPTH, i.e. MAX_PLACEMENT_DEPTH + 1 placements" |
| 112 | + ); |
| 113 | +} |
| 114 | + |
| 115 | +/// Site 1 of 2. Sharing the constant removes today's drift; this stops a |
| 116 | +/// private copy from reintroducing it. #2955 proved the need by mutation: a |
| 117 | +/// local `const` shadowed the import and 800 tests stayed green. |
| 118 | +#[test] |
| 119 | +fn the_router_cap_is_the_shared_cap() { |
| 120 | + assert_eq!( |
| 121 | + GeometryRouter::MAX_PLACEMENT_DEPTH, |
| 122 | + MAX_PLACEMENT_DEPTH, |
| 123 | + "router::transforms must bound PlacementRelTo with ifc_lite_core::limits::MAX_PLACEMENT_DEPTH" |
| 124 | + ); |
| 125 | +} |
| 126 | + |
| 127 | +/// Site 2 of 2. See above. |
| 128 | +#[test] |
| 129 | +fn the_profile_extractor_cap_is_the_shared_cap() { |
| 130 | + assert_eq!( |
| 131 | + crate::profile_extractor::MAX_PLACEMENT_DEPTH, |
| 132 | + MAX_PLACEMENT_DEPTH, |
| 133 | + "profile_extractor must bound PlacementRelTo with ifc_lite_core::limits::MAX_PLACEMENT_DEPTH" |
| 134 | + ); |
| 135 | +} |
| 136 | + |
0 commit comments