axioval_rules/
free_floor_rectangle.rs

1//! Exact source-neutral free-floor-rectangle capability.
2
3use axioval_engine::{
4    BoxClearance, CapabilityEvaluation, ClearanceShape, CompiledRule, FreeSpaceError,
5    FreeSpaceServiceHandle, NotEvaluatedReason, ParameterDescriptor, ParameterType,
6    PlacementDomain, PlacementOutcome, PlacementRequest, RuleCapability, RuleContext,
7    SupportedPlacement,
8};
9use axioval_ir::contract::ParameterValue;
10use axioval_ir::{Finding, Object, Severity};
11
12use crate::selection::select_objects;
13
14/// Exact free-floor rectangle placement using a trusted free-space service.
15pub struct FreeFloorRectangle;
16
17impl RuleCapability for FreeFloorRectangle {
18    fn id(&self) -> &'static str {
19        "axioval:capability.free-floor-rectangle"
20    }
21    fn parameters(&self) -> Vec<ParameterDescriptor> {
22        vec![
23            ParameterDescriptor::required("width_metres", ParameterType::Number),
24            ParameterDescriptor::required("length_metres", ParameterType::Number),
25            ParameterDescriptor::required("height_metres", ParameterType::Number),
26        ]
27    }
28    fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation {
29        let (selected, mut evaluation) = select_objects(context, &rule.selector);
30        let Some((width, length, height)) = dimensions(rule) else {
31            return invalid_parameters(
32                selected,
33                "free-floor rectangle dimensions are invalid",
34                evaluation,
35            );
36        };
37        let Ok(shape) = BoxClearance::try_new(width, length, height) else {
38            return invalid_parameters(
39                selected,
40                "free-floor rectangle dimensions must be positive and finite",
41                evaluation,
42            );
43        };
44        let Some(service) = context.services.get::<FreeSpaceServiceHandle>() else {
45            return unavailable(
46                selected,
47                &NotEvaluatedReason::MissingService,
48                "free-space service is not registered",
49                evaluation,
50            );
51        };
52        let all_objects: Vec<_> = context
53            .project
54            .objects()
55            .map(|object| object.id.clone())
56            .collect();
57        for space in selected {
58            let obstacles = all_objects
59                .iter()
60                .filter(|id| *id != &space.id)
61                .cloned()
62                .collect();
63            let support = match SupportedPlacement::try_new(space.id.clone(), 0.0) {
64                Ok(support) => support,
65                Err(error) => {
66                    evaluation.push_object_not_evaluated(
67                        space.id.clone(),
68                        NotEvaluatedReason::InvalidEvidence,
69                        error.to_string(),
70                    );
71                    continue;
72                }
73            };
74            let request = match PlacementRequest::new_in_domain(
75                space.id.clone(),
76                ClearanceShape::Box(shape),
77                obstacles,
78                PlacementDomain::Supported(support),
79            ) {
80                Ok(request) => request,
81                Err(error) => {
82                    evaluation.push_object_not_evaluated(
83                        space.id.clone(),
84                        NotEvaluatedReason::InvalidEvidence,
85                        error.to_string(),
86                    );
87                    continue;
88                }
89            };
90            match service.find_placement(&request) {
91                Ok(PlacementOutcome::Found(_)) => {}
92                Ok(PlacementOutcome::NoPlacement(proof)) => evaluation.push_finding(Finding {
93                    rule_id: rule.id.clone(),
94                    object_id: space.id.clone(),
95                    severity: severity(rule),
96                    message: "NO_FREE_FLOOR_SPACE_FOR_RECTANGLE".into(),
97                    evidence: vec![proof.evidence().clone()],
98                }),
99                Err(error) => evaluation.push_object_not_evaluated(
100                    space.id.clone(),
101                    reason(&error),
102                    error.to_string(),
103                ),
104            }
105        }
106        evaluation
107    }
108}
109
110fn dimensions(rule: &CompiledRule) -> Option<(f64, f64, f64)> {
111    let number = |name: &str| match rule.parameters.get(name)? {
112        ParameterValue::Number { value } => Some(*value),
113        _ => None,
114    };
115    Some((
116        number("width_metres")?,
117        number("length_metres")?,
118        number("height_metres")?,
119    ))
120}
121fn severity(rule: &CompiledRule) -> Severity {
122    match rule.severity {
123        axioval_ir::contract::Severity::Error => Severity::Error,
124        axioval_ir::contract::Severity::Warning => Severity::Warning,
125        axioval_ir::contract::Severity::Info => Severity::Info,
126    }
127}
128fn invalid_parameters(
129    selected: Vec<&Object>,
130    message: &str,
131    evaluation: CapabilityEvaluation,
132) -> CapabilityEvaluation {
133    unavailable(
134        selected,
135        &NotEvaluatedReason::InvalidDeclaration,
136        message,
137        evaluation,
138    )
139}
140fn unavailable(
141    selected: Vec<&Object>,
142    reason: &NotEvaluatedReason,
143    message: &str,
144    mut evaluation: CapabilityEvaluation,
145) -> CapabilityEvaluation {
146    for object in selected {
147        evaluation.push_object_not_evaluated(object.id.clone(), reason.clone(), message);
148    }
149    evaluation
150}
151fn reason(error: &FreeSpaceError) -> NotEvaluatedReason {
152    match error {
153        FreeSpaceError::MissingGeometry(_) | FreeSpaceError::Unavailable(_) => {
154            NotEvaluatedReason::BackendUnavailable
155        }
156        FreeSpaceError::IncompleteClearanceEvidence => NotEvaluatedReason::IncompleteEvidence,
157        _ => NotEvaluatedReason::InvalidEvidence,
158    }
159}