use ifc_model::{Entity, EntityId, Model, Value};
use super::dimension::{dim_of, first_dim_disagreement, list_refs};
use super::violation::{RuleViolation, ViolationKind};
#[derive(Clone, Copy)]
struct Subject<'a> {
id: EntityId,
entity: &'a Entity,
type_name: &'a str,
}
pub fn check(model: &Model, id: EntityId, entity: &Entity, out: &mut Vec<RuleViolation>) {
let name = entity.type_name.to_ascii_uppercase();
let subject = Subject {
id,
entity,
type_name: &name,
};
match name.as_str() {
"IFCPOLYLINE" => same_dim_list(model, subject, 0, "SameDim", "Points", out),
"IFCGEOMETRICSET" | "IFCGEOMETRICCURVESET" => {
same_dim_list(model, subject, 0, "ConsistentDim", "Elements", out)
}
"IFCLINE" => line_same_dim(model, subject, out),
"IFCPCURVE" => fixed_dim_ref(model, subject, 1, 2, "DimIs2D", out),
"IFCOFFSETCURVE2D" => fixed_dim_ref(model, subject, 0, 2, "DimIs2D", out),
"IFCOFFSETCURVE3D" => fixed_dim_ref(model, subject, 0, 3, "DimIs2D", out),
"IFCSURFACECURVE" | "IFCINTERSECTIONCURVE" | "IFCSEAMCURVE" => {
fixed_dim_ref(model, subject, 0, 3, "CurveIs3D", out)
}
"IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => {
fixed_dim_ref(model, subject, 0, 3, "DirectrixDim", out)
}
"IFCSECTIONEDSPINE" => fixed_dim_ref(model, subject, 0, 3, "SpineCurveDim", out),
"IFCPOLYGONALBOUNDEDHALFSPACE" => fixed_dim_ref(model, subject, 3, 2, "BoundaryDim", out),
_ => {}
}
if crate::select::is_a(&name, "IFCBSPLINECURVE") {
same_dim_list(model, subject, 1, "SameDim", "ControlPointsList", out);
}
if crate::select::is_a(&name, "IFCCOMPOSITECURVE") {
same_dim_list(model, subject, 0, "SameDim", "Segments", out);
}
composite_curve_continuity(model, subject, out);
consistent_profile_types(model, subject, out);
if name.starts_with("IFCCARTESIANTRANSFORMATIONOPERATOR") {
transformation_operator(model, subject, out);
}
}
fn same_dim_list(
model: &Model,
subject: Subject<'_>,
slot: usize,
rule: &'static str,
label: &str,
out: &mut Vec<RuleViolation>,
) {
let Subject {
id,
entity,
type_name,
} = subject;
let ids = list_refs(entity, slot);
if let Some((expected, found, offender)) = first_dim_disagreement(model, &ids) {
out.push(RuleViolation::new(
id,
type_name.to_string(),
rule,
ViolationKind::Disagreement,
format!("{label} starts {expected}D but {offender} is {found}D"),
));
}
}
fn fixed_dim_ref(
model: &Model,
subject: Subject<'_>,
slot: usize,
want: usize,
rule: &'static str,
out: &mut Vec<RuleViolation>,
) {
let Subject {
id,
entity,
type_name,
} = subject;
let Some(Value::Ref(target)) = entity.attribute(slot).map(|v| v.unwrap_typed()) else {
return;
};
let Some(found) = dim_of(model, *target) else {
return;
};
if found != want {
out.push(RuleViolation::new(
id,
type_name.to_string(),
rule,
ViolationKind::Dimensionality,
format!("{target} is {found}D, must be {want}D"),
));
}
}
fn line_same_dim(model: &Model, subject: Subject<'_>, out: &mut Vec<RuleViolation>) {
let Subject {
id,
entity,
type_name,
} = subject;
let (Some(Value::Ref(pnt)), Some(Value::Ref(dir))) = (
entity.attribute(0).map(|v| v.unwrap_typed()),
entity.attribute(1).map(|v| v.unwrap_typed()),
) else {
return;
};
let (Some(pd), Some(dd)) = (dim_of(model, *pnt), dim_of(model, *dir)) else {
return;
};
if pd != dd {
out.push(RuleViolation::new(
id,
type_name.to_string(),
"SameDim",
ViolationKind::Disagreement,
format!("Pnt {pnt} is {pd}D but Dir {dir} is {dd}D"),
));
}
}
fn transformation_operator(model: &Model, subject: Subject<'_>, out: &mut Vec<RuleViolation>) {
let Subject {
id,
entity,
type_name,
} = subject;
let subject = Subject {
id,
entity,
type_name,
};
let (want, dim_rule) = if type_name.starts_with("IFCCARTESIANTRANSFORMATIONOPERATOR3D") {
(3usize, "DimIs3D")
} else if type_name.starts_with("IFCCARTESIANTRANSFORMATIONOPERATOR2D") {
(2usize, "DimEqual2")
} else {
return;
};
fixed_dim_ref(model, subject, 2, want, dim_rule, out);
let axes: &[(usize, &'static str)] = if want == 3 {
&[(0, "Axis1Is3D"), (1, "Axis2Is3D"), (4, "Axis3Is3D")]
} else {
&[(0, "Axis1Is2D"), (1, "Axis2Is2D")]
};
for (slot, rule) in axes {
fixed_dim_ref(model, subject, *slot, want, rule, out);
}
}
fn composite_curve_continuity(model: &Model, subject: Subject<'_>, out: &mut Vec<RuleViolation>) {
let (id, entity, name) = (subject.id, subject.entity, subject.type_name);
if !crate::select::is_a(name, "IFCCOMPOSITECURVE") {
return;
}
let segments = super::dimension::list_refs(entity, 0);
if segments.is_empty() {
return;
}
let transition_of = |seg: EntityId| -> Option<String> {
match model.get(seg)?.attribute(0).map(|v| v.unwrap_typed()) {
Some(Value::Enum(e)) => Some(e.to_ascii_uppercase()),
_ => None,
}
};
let Some(last) = segments.last().and_then(|s| transition_of(*s)) else {
return;
};
let closed = last != "DISCONTINUOUS";
let discontinuous = segments
.iter()
.filter(|seg| transition_of(**seg).as_deref() == Some("DISCONTINUOUS"))
.count();
let want = if closed { 0 } else { 1 };
if discontinuous != want {
out.push(RuleViolation::new(
id,
name.to_string(),
"CurveContinuous",
ViolationKind::Disagreement,
format!(
"{discontinuous} segments are DISCONTINUOUS; a closed curve \
admits 0 and an open curve exactly 1"
),
));
}
if crate::select::is_a(name, "IFCBOUNDARYCURVE") && !closed {
out.push(RuleViolation::new(
id,
name.to_string(),
"IsClosed",
ViolationKind::Disagreement,
"a boundary curve must be closed, but its segments end in a \
discontinuity"
.to_string(),
));
}
}
fn consistent_profile_types(model: &Model, subject: Subject<'_>, out: &mut Vec<RuleViolation>) {
let (id, entity, name) = (subject.id, subject.entity, subject.type_name);
if name != "IFCSECTIONEDSPINE" {
return;
}
let sections = super::dimension::list_refs(entity, 1);
let kind = |id: EntityId| -> Option<String> {
match model.get(id)?.attribute(0).map(|v| v.unwrap_typed()) {
Some(Value::Enum(e)) => Some(e.to_ascii_uppercase()),
_ => None,
}
};
let Some(first) = sections.first().and_then(|s| kind(*s)) else {
return;
};
for section in sections.iter().skip(1) {
let Some(other) = kind(*section) else {
continue;
};
if other != first {
out.push(RuleViolation::new(
id,
name.to_string(),
"ConsistentProfileTypes",
ViolationKind::Disagreement,
format!("cross-section {section} is {other}, but the first is {first}"),
));
return;
}
}
}