use ifc_model::{Entity, EntityId, Model, Value};
use super::violation::{RuleViolation, ViolationKind};
pub fn check(model: &Model, id: EntityId, entity: &Entity, out: &mut Vec<RuleViolation>) {
let name = entity.type_name.to_ascii_uppercase();
if name == "IFCRECTANGULARTRIMMEDSURFACE" {
let (u1, v1, u2, v2) = (
real_at(entity, 1),
real_at(entity, 2),
real_at(entity, 3),
real_at(entity, 4),
);
distinct(id, &name, "U1AndU2Different", "U1", "U2", u1, u2, out);
distinct(id, &name, "V1AndV2Different", "V1", "V2", v1, v2, out);
if let (Some(a), Some(b), Some(sense)) = (v1, v2, bool_at(entity, 6)) {
if sense != (b > a) {
out.push(RuleViolation::new(
id,
name.clone(),
"VsenseCompatible",
ViolationKind::Disagreement,
format!("Vsense is {sense} but V2 > V1 is {}", b > a),
));
}
}
}
directrix_bounded(model, id, entity, &name, out);
trim_values_consistent(id, entity, &name, out);
usense_compatible(model, id, entity, &name, out);
applicable_mapped_repr(model, id, entity, &name, out);
if name == "IFCTOROIDALSURFACE" {
if let (Some(major), Some(minor)) = (real_at(entity, 1), real_at(entity, 2)) {
if minor >= major {
out.push(RuleViolation::new(
id,
name.clone(),
"MajorLargerMinor",
ViolationKind::Degenerate,
format!("MinorRadius {minor} is not less than MajorRadius {major}"),
));
}
}
}
if name == "IFCSWEPTDISKSOLID" || name == "IFCSWEPTDISKSOLIDPOLYGONAL" {
if let (Some(radius), Some(inner)) = (real_at(entity, 1), real_at(entity, 2)) {
if radius <= inner {
out.push(RuleViolation::new(
id,
name.clone(),
"InnerRadiusSize",
ViolationKind::Degenerate,
format!("InnerRadius {inner} is not smaller than Radius {radius}"),
));
}
}
}
if name == "IFCSWEPTDISKSOLIDPOLYGONAL" {
if let (Some(radius), Some(fillet)) = (real_at(entity, 1), real_at(entity, 5)) {
if fillet < radius {
out.push(RuleViolation::new(
id,
name.clone(),
"CorrectRadii",
ViolationKind::Degenerate,
format!("FilletRadius {fillet} is smaller than Radius {radius}"),
));
}
}
}
if name == "IFCCARTESIANPOINT" {
if let Some(Value::List(c)) = entity.attribute(0).map(|v| v.unwrap_typed()) {
if c.len() < 2 {
out.push(RuleViolation::new(
id,
name.clone(),
"CP2Dor3D",
ViolationKind::Dimensionality,
format!("Coordinates holds {}, must hold at least 2", c.len()),
));
}
}
}
if crate::select::is_a(&name, "IFCSWEPTAREASOLID") {
profile_type(
model,
id,
entity,
&name,
0,
"AREA",
"SweptAreaType",
"SweptArea",
out,
);
}
if name == "IFCEXTRUDEDAREASOLIDTAPERED" || name == "IFCREVOLVEDAREASOLIDTAPERED" {
tapered_profiles(model, id, entity, &name, out);
}
if crate::select::is_a(&name, "IFCSWEPTSURFACE") {
profile_type(
model,
id,
entity,
&name,
0,
"CURVE",
"SweptCurveType",
"SweptCurve",
out,
);
}
}
#[allow(clippy::too_many_arguments)]
fn profile_type(
model: &Model,
id: EntityId,
entity: &Entity,
type_name: &str,
slot: usize,
want: &str,
rule: &'static str,
label: &str,
out: &mut Vec<RuleViolation>,
) {
let Some(Value::Ref(target)) = entity.attribute(slot).map(|v| v.unwrap_typed()) else {
return;
};
let Some(profile) = model.get(*target) else {
return;
};
let Some(Value::Enum(kind)) = profile.attribute(0).map(|v| v.unwrap_typed()) else {
return;
};
if kind.eq_ignore_ascii_case(want) {
return;
}
out.push(RuleViolation::new(
id,
type_name.to_string(),
rule,
ViolationKind::WrongType,
format!("{label} {target} is a {kind} profile, must be {want}"),
));
}
fn tapered_profiles(
model: &Model,
id: EntityId,
entity: &Entity,
type_name: &str,
out: &mut Vec<RuleViolation>,
) {
let (Some(Value::Ref(start)), Some(Value::Ref(end))) = (
entity.attribute(0).map(|v| v.unwrap_typed()),
entity.attribute(4).map(|v| v.unwrap_typed()),
) else {
return;
};
let (Some(s), Some(e)) = (model.get(*start), model.get(*end)) else {
return;
};
let (sn, en) = (
s.type_name.to_ascii_uppercase(),
e.type_name.to_ascii_uppercase(),
);
let ok = if en == "IFCDERIVEDPROFILEDEF" {
matches!(e.attribute(2).map(|v| v.unwrap_typed()), Some(Value::Ref(p)) if *p == *start)
} else if crate::select::is_a(&sn, "IFCPARAMETERIZEDPROFILEDEF") {
sn == en
} else {
false
};
if !ok {
out.push(RuleViolation::new(
id,
type_name.to_string(),
"CorrectProfileAssignment",
ViolationKind::Disagreement,
format!("SweptArea {sn} and EndSweptArea {en} do not correspond"),
));
}
}
#[allow(clippy::too_many_arguments)]
fn distinct(
id: EntityId,
type_name: &str,
rule: &'static str,
label_a: &str,
label_b: &str,
a: Option<f64>,
b: Option<f64>,
out: &mut Vec<RuleViolation>,
) {
let (Some(a), Some(b)) = (a, b) else { return };
if a != b {
return;
}
out.push(RuleViolation::new(
id,
type_name.to_string(),
rule,
ViolationKind::Degenerate,
format!("{label_a} and {label_b} are both {a}, so the trim is empty"),
));
}
fn real_at(entity: &Entity, slot: usize) -> Option<f64> {
match entity.attribute(slot).map(|v| v.unwrap_typed()) {
Some(Value::Real(v)) => Some(*v),
Some(Value::Integer(v)) => Some(*v as f64),
_ => None,
}
}
fn bool_at(entity: &Entity, slot: usize) -> Option<bool> {
match entity.attribute(slot).map(|v| v.unwrap_typed()) {
Some(Value::Bool(v)) => Some(*v),
_ => None,
}
}
fn directrix_bounded(
model: &Model,
id: EntityId,
entity: &Entity,
name: &str,
out: &mut Vec<RuleViolation>,
) {
let (directrix_slot, start_slot, end_slot) = match name {
"IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => (0usize, 3usize, 4usize),
"IFCSURFACECURVESWEPTAREASOLID" | "IFCFIXEDREFERENCESWEPTAREASOLID" => (2, 3, 4),
_ => return,
};
let written = |slot: usize| {
entity
.attribute(slot)
.is_some_and(|v| !matches!(v.unwrap_typed(), Value::Null))
};
let has_params = written(start_slot) && written(end_slot);
if has_params {
return;
}
let Some(Value::Ref(directrix)) = entity.attribute(directrix_slot).map(|v| v.unwrap_typed())
else {
return;
};
let Some(curve) = model.get(*directrix) else {
return;
};
let curve_name = curve.type_name.to_ascii_uppercase();
let bounded_or_conic = crate::select::is_a(&curve_name, "IFCCONIC")
|| crate::select::is_a(&curve_name, "IFCBOUNDEDCURVE");
if !bounded_or_conic {
out.push(RuleViolation::new(
id,
name.to_string(),
"DirectrixBounded",
ViolationKind::Disagreement,
format!(
"the directrix {directrix} is neither a conic nor a bounded \
curve, so StartParam and EndParam are required to bound the \
sweep"
),
));
}
}
fn trim_values_consistent(id: EntityId, entity: &Entity, name: &str, out: &mut Vec<RuleViolation>) {
if !crate::select::is_a(name, "IFCTRIMMEDCURVE") {
return;
}
for (slot, rule) in [
(1usize, "Trim1ValuesConsistent"),
(2, "Trim2ValuesConsistent"),
] {
let Some(Value::List(items)) = entity.attribute(slot).map(|v| v.unwrap_typed()) else {
continue;
};
if items.len() < 2 {
continue;
}
let kind = |v: &Value| match v.unwrap_typed() {
Value::Ref(_) => Some("a cartesian point"),
Value::Real(_) | Value::Integer(_) => Some("a parameter value"),
_ => None,
};
let (Some(first), Some(second)) = (kind(&items[0]), kind(&items[1])) else {
continue;
};
if first == second {
out.push(RuleViolation::new(
id,
name.to_string(),
rule,
ViolationKind::Disagreement,
format!("both trim values are {first}; the two must differ in kind"),
));
}
}
}
fn usense_compatible(
model: &Model,
id: EntityId,
entity: &Entity,
name: &str,
out: &mut Vec<RuleViolation>,
) {
if name != "IFCRECTANGULARTRIMMEDSURFACE" {
return;
}
let Some(Value::Ref(basis)) = entity.attribute(0).map(|v| v.unwrap_typed()) else {
return;
};
let Some(surface) = model.get(*basis) else {
return;
};
let surface_name = surface.type_name.to_ascii_uppercase();
let wraps_in_u = (crate::select::is_a(&surface_name, "IFCELEMENTARYSURFACE")
&& !crate::select::is_a(&surface_name, "IFCPLANE"))
|| crate::select::is_a(&surface_name, "IFCSURFACEOFREVOLUTION");
if wraps_in_u {
return;
}
let (Some(u1), Some(u2)) = (real_at(entity, 1), real_at(entity, 3)) else {
return;
};
let Some(Value::Bool(usense)) = entity.attribute(5).map(|v| v.unwrap_typed()) else {
return;
};
if *usense != (u2 > u1) {
out.push(RuleViolation::new(
id,
name.to_string(),
"UsenseCompatible",
ViolationKind::Disagreement,
format!("Usense is {usense} but U1 = {u1} and U2 = {u2}"),
));
}
}
fn applicable_mapped_repr(
model: &Model,
id: EntityId,
entity: &Entity,
name: &str,
out: &mut Vec<RuleViolation>,
) {
if name != "IFCREPRESENTATIONMAP" {
return;
}
let Some(Value::Ref(mapped)) = entity.attribute(1).map(|v| v.unwrap_typed()) else {
return;
};
let Some(target) = model.get(*mapped) else {
return;
};
let target_name = target.type_name.to_ascii_uppercase();
let is_shape_model = matches!(
target_name.as_str(),
"IFCSHAPEMODEL" | "IFCSHAPEREPRESENTATION" | "IFCTOPOLOGYREPRESENTATION"
);
if !is_shape_model {
out.push(RuleViolation::new(
id,
name.to_string(),
"ApplicableMappedRepr",
ViolationKind::WrongType,
format!("the mapped representation {mapped} is {target_name}, not an IfcShapeModel"),
));
}
}