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