1use 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
14pub 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}