pub(super) type IndexColumns<'a> = (&'a [u32], &'a [u32], &'a [u32], &'a [u8]);
pub(super) struct ColumnsDiscovery {
pub buffered_jobs: Vec<(u32, usize, usize, ifc_lite_core::IfcType)>,
pub total_jobs: u32,
pub project_id: Option<u32>,
pub site_position: Option<(u32, usize, usize)>,
pub prepass_spans: ifc_lite_processing::prepass::PrepassSpans,
pub mapped_item_spans: Vec<(u32, usize, usize)>,
pub rel_defines_by_type_spans: Vec<(u32, usize, usize)>,
pub type_candidate_spans: Vec<(u32, usize, usize, ifc_lite_core::IfcType)>,
pub has_layer_set: bool,
}
fn keyword_at(content: &[u8], start: usize, end: usize) -> &str {
let span = &content[start..end.min(content.len())];
let eq = span.iter().position(|&b| b == b'=').map(|p| p + 1).unwrap_or(0);
let kw_end = span[eq..]
.iter()
.position(|&b| b == b'(')
.map(|p| eq + p)
.unwrap_or(span.len());
std::str::from_utf8(&span[eq..kw_end]).unwrap_or("").trim()
}
pub(super) fn discover_from_columns(
content: &[u8],
ids: &[u32],
starts: &[u32],
lengths: &[u32],
classes: &[u8],
disabled_types: &rustc_hash::FxHashSet<String>,
) -> ColumnsDiscovery {
use ifc_lite_processing as p;
let mut d = ColumnsDiscovery {
buffered_jobs: Vec::new(),
total_jobs: 0,
project_id: None,
site_position: None,
prepass_spans: p::prepass::PrepassSpans::default(),
mapped_item_spans: Vec::new(),
rel_defines_by_type_spans: Vec::new(),
type_candidate_spans: Vec::new(),
has_layer_set: false,
};
for i in 0..ids.len() {
let class = classes[i];
if class == p::PREPASS_CLASS_NONE {
continue;
}
let id = ids[i];
let start = starts[i] as usize;
let end = start + lengths[i] as usize;
match class & p::PREPASS_CLASS_CODE_MASK {
c if c == p::PREPASS_CLASS_PROJECT => {
if d.project_id.is_none() {
d.project_id = Some(id);
}
continue;
}
c if c == p::PREPASS_CLASS_SITE => {
if d.site_position.is_none() {
d.site_position = Some((id, start, end));
}
d.buffered_jobs.push((id, start, end, ifc_lite_core::IfcType::IfcSite));
d.total_jobs += 1;
continue;
}
c if c == p::PREPASS_CLASS_STYLED_ITEM => {
d.prepass_spans.styled_items.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_INDEXED_COLOUR_MAP => {
d.prepass_spans.indexed_colour_maps.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_MATERIAL_DEF_REPR => {
d.prepass_spans.material_def_reprs.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_REL_ASSOCIATES_MATERIAL => {
d.prepass_spans.rel_associates_material.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_REL_VOIDS => {
d.prepass_spans.void_rels.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_REL_FILLS => {
d.prepass_spans.fills_rels.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_REL_AGGREGATES => {
d.prepass_spans.aggregate_rels.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_MATERIAL_LAYER_SET => {
d.has_layer_set = true;
continue;
}
c if c == p::PREPASS_CLASS_MAPPED_ITEM => {
d.mapped_item_spans.push((id, start, end));
continue;
}
c if c == p::PREPASS_CLASS_REL_DEFINES_BY_TYPE => {
d.rel_defines_by_type_spans.push((id, start, end));
continue;
}
_ => {}
}
if class & p::PREPASS_CLASS_FLAG_TYPE_CANDIDATE != 0 {
let kw = keyword_at(content, start, end);
d.type_candidate_spans
.push((id, start, end, ifc_lite_core::IfcType::from_str(kw)));
}
if class & p::PREPASS_CLASS_FLAG_GEOMETRY_JOB != 0 {
let kw = keyword_at(content, start, end);
if disabled_types.is_empty() || !disabled_types.contains(kw) {
d.buffered_jobs
.push((id, start, end, ifc_lite_core::IfcType::from_str(kw)));
d.total_jobs += 1;
}
}
}
d
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../geometry/tests/fixtures/issue_1910_storey_shell_geometry.ifc"
);
fn read_fixture() -> String {
std::fs::read_to_string(FIXTURE).expect("issue_1910 storey fixture must be present")
}
#[test]
fn sharded_column_discovery_schedules_storey_geometry_job() {
let mut content = read_fixture();
let injected =
"#42=IFCBUILDINGSTOREY('7777777777777777770108',$,'Level 2',$,$,#18,$,$,.ELEMENT.,0.);\n";
let endsec_pos = content.rfind("ENDSEC;").expect("fixture must have an ENDSEC;");
content.insert_str(endsec_pos, injected);
let bytes = content.as_bytes();
assert!(
!ifc_lite_core::has_geometry_by_name("IFCBUILDINGSTOREY"),
"sanity: IFCBUILDINGSTOREY must stay excluded from has_geometry_by_name -- \
otherwise this test would pass via the ordinary by-name classification and \
stop proving the instance-level exception fires"
);
let (records, classes, handoff) =
ifc_lite_processing::scan_shard_classified(bytes, 0, bytes.len());
assert!(handoff.is_none(), "single shard must cover the whole fixture");
let ids: Vec<u32> = records.iter().map(|&(id, _, _)| id).collect();
let starts: Vec<u32> = records.iter().map(|&(_, s, _)| s as u32).collect();
let lengths: Vec<u32> = records.iter().map(|&(_, s, e)| (e - s) as u32).collect();
let find_storey_idx = |global_id: &str| {
records
.iter()
.position(|&(_, s, e)| {
keyword_at(bytes, s, e) == "IFCBUILDINGSTOREY"
&& bytes[s..e].windows(global_id.len()).any(|w| w == global_id.as_bytes())
})
.unwrap_or_else(|| panic!("fixture must contain a storey with GlobalId {global_id}"))
};
let with_repr_idx = find_storey_idx("7777777777777777770103");
let without_repr_idx = find_storey_idx("7777777777777777770108");
assert!(
classes[with_repr_idx] & ifc_lite_processing::PREPASS_CLASS_FLAG_GEOMETRY_JOB != 0,
"IFCBUILDINGSTOREY's shard class byte must carry the geometry-job flag \
when its Representation is non-null (#1910)"
);
assert!(
classes[without_repr_idx] & ifc_lite_processing::PREPASS_CLASS_FLAG_GEOMETRY_JOB == 0,
"an IFCBUILDINGSTOREY with a null Representation must NOT carry the \
geometry-job flag (#1910 negative case)"
);
let disabled = rustc_hash::FxHashSet::default();
let discovery = discover_from_columns(bytes, &ids, &starts, &lengths, &classes, &disabled);
let with_repr_id = records[with_repr_idx].0;
let without_repr_id = records[without_repr_idx].0;
assert!(
discovery
.buffered_jobs
.iter()
.any(|&(id, _, _, _)| id == with_repr_id),
"sharded column discovery must emit a geometry job for the \
storey whose only geometry hangs off IFCBUILDINGSTOREY (#1910); \
buffered_jobs = {:?}",
discovery.buffered_jobs
);
assert!(
discovery
.buffered_jobs
.iter()
.all(|&(id, _, _, _)| id != without_repr_id),
"sharded column discovery must NOT emit a geometry job for a storey \
with a null Representation (#1910 negative case); buffered_jobs = {:?}",
discovery.buffered_jobs
);
}
}