use ifc_lite_core::IfcType;
use ifc_lite_processing::default_color_for_type;
const NEUTRAL_GRAY: [f32; 4] = [0.8, 0.8, 0.8, 1.0];
fn wasm_default(t: IfcType) -> [f32; 4] {
match t {
IfcType::IfcWall | IfcType::IfcWallStandardCase => [0.85, 0.85, 0.85, 1.0],
IfcType::IfcSlab => [0.7, 0.7, 0.7, 1.0],
IfcType::IfcRoof => [0.6, 0.5, 0.4, 1.0],
IfcType::IfcColumn | IfcType::IfcBeam | IfcType::IfcMember => [0.6, 0.65, 0.7, 1.0],
IfcType::IfcWindow => [0.6, 0.8, 1.0, 0.4],
IfcType::IfcDoor => [0.6, 0.45, 0.3, 1.0],
IfcType::IfcStair => [0.75, 0.75, 0.75, 1.0],
IfcType::IfcRailing => [0.4, 0.4, 0.45, 1.0],
IfcType::IfcPlate | IfcType::IfcCovering => [0.8, 0.8, 0.8, 1.0],
IfcType::IfcCurtainWall => [0.5, 0.7, 0.9, 0.5],
IfcType::IfcFurnishingElement => [0.7, 0.55, 0.4, 1.0],
IfcType::IfcSpace => [0.2, 0.85, 1.0, 0.3],
IfcType::IfcOpeningElement => [1.0, 0.42, 0.29, 0.4],
IfcType::IfcSite => [0.4, 0.8, 0.3, 1.0],
_ => NEUTRAL_GRAY,
}
}
fn processing_default(t: IfcType) -> [f32; 4] {
match t {
IfcType::IfcWall | IfcType::IfcWallStandardCase => [0.85, 0.85, 0.85, 1.0],
IfcType::IfcSlab => [0.7, 0.7, 0.7, 1.0],
IfcType::IfcRoof => [0.6, 0.5, 0.4, 1.0],
IfcType::IfcColumn | IfcType::IfcBeam | IfcType::IfcMember => [0.6, 0.65, 0.7, 1.0],
IfcType::IfcWindow => [0.6, 0.8, 1.0, 0.4],
IfcType::IfcDoor => [0.6, 0.45, 0.3, 1.0],
IfcType::IfcStair | IfcType::IfcStairFlight => [0.75, 0.75, 0.75, 1.0],
IfcType::IfcRailing => [0.4, 0.4, 0.45, 1.0],
IfcType::IfcPlate | IfcType::IfcCovering => [0.8, 0.8, 0.8, 1.0],
IfcType::IfcFurnishingElement => [0.5, 0.35, 0.2, 1.0],
IfcType::IfcSpace => [0.2, 0.85, 1.0, 0.3],
IfcType::IfcOpeningElement => [1.0, 0.42, 0.29, 0.4],
IfcType::IfcSite => [0.4, 0.8, 0.3, 1.0],
IfcType::IfcBuildingElementProxy => [0.6, 0.6, 0.6, 1.0],
_ => NEUTRAL_GRAY,
}
}
const MAPPED_TYPES: &[IfcType] = &[
IfcType::IfcWall,
IfcType::IfcWallStandardCase,
IfcType::IfcSlab,
IfcType::IfcRoof,
IfcType::IfcColumn,
IfcType::IfcBeam,
IfcType::IfcMember,
IfcType::IfcWindow,
IfcType::IfcDoor,
IfcType::IfcStair,
IfcType::IfcStairFlight,
IfcType::IfcRailing,
IfcType::IfcPlate,
IfcType::IfcCovering,
IfcType::IfcCurtainWall,
IfcType::IfcFurnishingElement,
IfcType::IfcSpace,
IfcType::IfcOpeningElement,
IfcType::IfcSite,
IfcType::IfcBuildingElementProxy,
];
const CONTESTED: &[IfcType] = &[
IfcType::IfcStairFlight,
IfcType::IfcCurtainWall,
IfcType::IfcFurnishingElement,
IfcType::IfcBuildingElementProxy,
];
fn is_contested(t: IfcType) -> bool {
CONTESTED.contains(&t)
}
#[test]
fn union_agrees_with_both_tables_on_uncontested_types() {
for &t in MAPPED_TYPES {
if is_contested(t) {
continue;
}
let canonical = default_color_for_type(t).to_array();
assert_eq!(
canonical,
wasm_default(t),
"{t:?}: canonical must match the wasm table on uncontested types"
);
assert_eq!(
canonical,
processing_default(t),
"{t:?}: canonical must match the processing table on uncontested types"
);
}
}
#[test]
fn union_picks_the_documented_winner_for_contested_types() {
let cases = [
(IfcType::IfcStairFlight, [0.75, 0.75, 0.75, 1.0], false), (IfcType::IfcCurtainWall, [0.5, 0.7, 0.9, 0.5], true), (IfcType::IfcFurnishingElement, [0.7, 0.55, 0.4, 1.0], true), (IfcType::IfcBuildingElementProxy, [0.6, 0.6, 0.6, 1.0], false), ];
for (t, expected, from_wasm) in cases {
let canonical = default_color_for_type(t).to_array();
assert_eq!(canonical, expected, "{t:?}: unexpected canonical value");
let winner = if from_wasm {
wasm_default(t)
} else {
processing_default(t)
};
assert_eq!(canonical, winner, "{t:?}: canonical must equal the chosen source table");
}
assert_ne!(
default_color_for_type(IfcType::IfcFurnishingElement).to_array(),
processing_default(IfcType::IfcFurnishingElement),
"furnishing must change away from processing's [0.5,0.35,0.2,1]"
);
}
#[test]
fn exactly_four_types_change_per_table() {
let wasm_deltas: Vec<IfcType> = MAPPED_TYPES
.iter()
.copied()
.filter(|&t| default_color_for_type(t).to_array() != wasm_default(t))
.collect();
let processing_deltas: Vec<IfcType> = MAPPED_TYPES
.iter()
.copied()
.filter(|&t| default_color_for_type(t).to_array() != processing_default(t))
.collect();
assert_eq!(
wasm_deltas,
vec![IfcType::IfcStairFlight, IfcType::IfcBuildingElementProxy],
"unexpected changes relative to the wasm table"
);
assert_eq!(
processing_deltas,
vec![IfcType::IfcCurtainWall, IfcType::IfcFurnishingElement],
"unexpected changes relative to the processing table"
);
}
fn repo_root() -> Option<std::path::PathBuf> {
let mut dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
loop {
if dir.join("rust").is_dir() && dir.join("apps").is_dir() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
fn collect_rs_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let skip = matches!(
path.file_name().and_then(|n| n.to_str()),
Some("target" | "node_modules" | ".git" | "dist" | "build")
);
if !skip {
collect_rs_files(&path, out);
}
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
#[test]
fn no_duplicate_default_color_tables() {
let Some(root) = repo_root() else {
eprintln!("repo root not found (packaged context) — skipping guard");
return;
};
let allow = |rel: &str| rel.ends_with("rust/processing/tests/styling_parity.rs");
let mut files = Vec::new();
collect_rs_files(&root.join("rust"), &mut files);
collect_rs_files(&root.join("apps"), &mut files);
let mut offenders = Vec::new();
for path in files {
let rel = path
.strip_prefix(&root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
if allow(&rel) {
continue;
}
let Ok(src) = std::fs::read_to_string(&path) else {
continue;
};
let declares_color_table = src.lines().any(|line| {
let line = line.trim_start();
line.starts_with("fn get_default_color")
|| line.starts_with("pub fn get_default_color")
|| line.starts_with("pub(crate) fn get_default_color")
});
if declares_color_table {
offenders.push(rel);
}
}
assert!(
offenders.is_empty(),
"found per-consumer default-color table(s) outside the canonical \
`processing::style` — use `default_color_for_type` instead (issue #913): {offenders:?}"
);
}
#[test]
fn no_duplicate_surface_style_color_extraction() {
let Some(root) = repo_root() else {
eprintln!("repo root not found (packaged context) — skipping guard");
return;
};
let allow = |rel: &str| {
rel.ends_with("rust/processing/tests/styling_parity.rs")
|| rel.starts_with("rust/geometry/examples/")
};
let mut files = Vec::new();
collect_rs_files(&root.join("rust"), &mut files);
collect_rs_files(&root.join("apps"), &mut files);
let mut offenders = Vec::new();
for path in files {
let rel = path
.strip_prefix(&root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
if allow(&rel) {
continue;
}
let Ok(src) = std::fs::read_to_string(&path) else {
continue;
};
let declares = src.lines().any(|line| {
let line = line.trim_start();
["fn ", "pub fn ", "pub(crate) fn "].iter().any(|p| {
line.starts_with(&format!("{p}extract_color_from_rendering"))
|| line.starts_with(&format!("{p}extract_color_rgb"))
})
});
if declares {
offenders.push(rel);
}
}
assert!(
offenders.is_empty(),
"surface-style colour extraction must live only in \
`ifc_lite_processing::style::extract_surface_style_colors`; found a per-pipeline \
copy in: {offenders:?}"
);
}