1use std::collections::{HashMap, HashSet};
11
12use axiolid_brep::ExactBRep;
13use axiolid_construct::boolean_exact::{boolean_prisms_exact, Prism};
14use axiolid_construct::extrude::extrude_profile_exact;
15use axiolid_construct::revolve_exact::revolve_profile_exact;
16use axiolid_contracts::{
17 Backend, BackendDescriptor, BackendId, ExecutionOptions, ExecutionTarget, GeomError,
18 GeomResult, Operation,
19};
20use axiolid_exact_compile_contract::ExactCompiler;
21use axiolid_model::{GeometryGraph, GeometryNode, NodeId, SolidOperation};
22
23#[derive(Debug, Clone, Copy, Default)]
25pub struct ReferenceExactCompiler;
26
27impl ReferenceExactCompiler {
28 pub const ID: BackendId = BackendId::new("scalar-exact-compile");
30
31 pub const fn new() -> Self {
33 Self
34 }
35}
36
37impl Backend for ReferenceExactCompiler {
38 fn descriptor(&self) -> BackendDescriptor {
39 BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
40 }
41}
42
43impl ExactCompiler for ReferenceExactCompiler {
44 fn compile_exact(
45 &self,
46 graph: &GeometryGraph,
47 root: NodeId,
48 options: &ExecutionOptions,
49 ) -> GeomResult<ExactBRep> {
50 ExactCompilation::new(graph, options).compile(root)
51 }
52
53 fn compile_exact_batch_into(
54 &self,
55 graph: &GeometryGraph,
56 roots: &[NodeId],
57 options: &ExecutionOptions,
58 destination: &mut Vec<ExactBRep>,
59 ) -> GeomResult<()> {
60 destination.reserve(roots.len());
61 let mut compilation = ExactCompilation::new(graph, options);
62 for &root in roots {
63 destination.push(compilation.compile(root)?);
64 }
65 Ok(())
66 }
67}
68
69struct ExactCompilation<'a> {
70 graph: &'a GeometryGraph,
71 options: &'a ExecutionOptions,
72 cache: HashMap<NodeId, ExactBRep>,
73 active: HashSet<NodeId>,
74 cache_hits: usize,
75 evaluated_nodes: usize,
76}
77
78impl<'a> ExactCompilation<'a> {
79 fn new(graph: &'a GeometryGraph, options: &'a ExecutionOptions) -> Self {
80 Self {
81 graph,
82 options,
83 cache: HashMap::new(),
84 active: HashSet::new(),
85 cache_hits: 0,
86 evaluated_nodes: 0,
87 }
88 }
89
90 fn compile(&mut self, root: NodeId) -> GeomResult<ExactBRep> {
91 if let Some(cached) = self.cache.get(&root) {
92 self.cache_hits += 1;
93 return Ok(cached.clone());
94 }
95 if !self.active.insert(root) {
96 return Err(GeomError::InvalidInput(format!(
97 "exact compilation cycle reached at node {root:?}"
98 )));
99 }
100
101 self.evaluated_nodes += 1;
102 let result = self.compile_uncached(root);
103 self.active.remove(&root);
104 let exact = result?;
105 self.cache.insert(root, exact.clone());
106 Ok(exact)
107 }
108
109 fn compile_uncached(&self, root: NodeId) -> GeomResult<ExactBRep> {
110 let node = self.graph.get(root).ok_or_else(|| {
111 GeomError::InvalidInput(format!("node {root:?} does not belong to this graph"))
112 })?;
113
114 match node {
115 GeometryNode::SolidOperation(SolidOperation::Extrusion {
116 profile,
117 direction,
118 depth,
119 }) => {
120 let profile_node = self.graph.get(*profile).ok_or_else(|| {
121 GeomError::InvalidInput(format!(
122 "extrusion profile {profile:?} does not belong to this graph"
123 ))
124 })?;
125 let GeometryNode::Profile(profile) = profile_node else {
126 return Err(unsupported("extrusion profile reference"));
127 };
128 extrude_profile_exact(profile, *direction, *depth, self.options.tolerance())
129 .map_err(remap_construction_error)
130 }
131 GeometryNode::SolidOperation(SolidOperation::Revolution {
132 profile,
133 axis_origin,
134 axis_direction,
135 angle,
136 }) => {
137 let profile_node = self.graph.get(*profile).ok_or_else(|| {
138 GeomError::InvalidInput(format!(
139 "revolution profile {profile:?} does not belong to this graph"
140 ))
141 })?;
142 let GeometryNode::Profile(profile) = profile_node else {
143 return Err(unsupported("revolution profile reference"));
144 };
145 revolve_profile_exact(
146 profile,
147 *axis_origin,
148 *axis_direction,
149 *angle,
150 self.options.tolerance(),
151 )
152 .map_err(remap_construction_error)
153 }
154 GeometryNode::SolidOperation(SolidOperation::Boolean {
155 left,
156 right,
157 operator,
158 }) => {
159 let subject = self.prism_operand(*left, "boolean subject")?;
160 let tool = self.prism_operand(*right, "boolean tool")?;
161 boolean_prisms_exact(&subject, &tool, *operator, self.options.tolerance())
162 .map_err(remap_construction_error)
163 }
164 _ => Err(unsupported(exact_input_family(node))),
165 }
166 }
167}
168
169impl ExactCompilation<'_> {
170 fn prism_operand(&self, id: NodeId, role: &'static str) -> GeomResult<Prism> {
177 let _ = role;
178 let node = self.graph.get(id).ok_or_else(|| {
179 GeomError::InvalidInput(format!("operand {id:?} does not belong to this graph"))
180 })?;
181 let GeometryNode::SolidOperation(SolidOperation::Extrusion {
182 profile,
183 direction,
184 depth,
185 }) = *node
186 else {
187 return Err(unsupported(
188 "exact boolean operand that is not an extrusion",
189 ));
190 };
191 if direction.normalize_or_zero().dot(axiolid_core::Vec3::Z)
192 < 1.0 - self.options.tolerance().linear()
193 {
194 return Err(unsupported(
195 "exact boolean operand with an oblique extrusion",
196 ));
197 }
198 let profile_node = self.graph.get(profile).ok_or_else(|| {
199 GeomError::InvalidInput(format!("profile {profile:?} does not belong to this graph"))
200 })?;
201 let GeometryNode::Profile(axiolid_profile::Profile::Rectangle(rectangle)) = profile_node
202 else {
203 return Err(unsupported(
204 "exact boolean operand with a non-rectangle profile",
205 ));
206 };
207 if rectangle.thickness.is_some()
208 || rectangle.outer_radius.is_some()
209 || rectangle.inner_radius.is_some()
210 {
211 return Err(unsupported(
212 "exact boolean operand with a non-sharp profile",
213 ));
214 }
215 let (hx, hy) = (rectangle.x / 2.0, rectangle.y / 2.0);
216 Ok(Prism {
217 rings: vec![vec![
218 axiolid_core::Point2::new(-hx, -hy),
219 axiolid_core::Point2::new(hx, -hy),
220 axiolid_core::Point2::new(hx, hy),
221 axiolid_core::Point2::new(-hx, hy),
222 ]],
223 bottom: 0.0,
224 top: depth,
225 })
226 }
227}
228
229fn remap_construction_error(error: GeomError) -> GeomError {
230 match error {
231 GeomError::UnsupportedInput { input, .. } => unsupported(input),
232 error => error,
233 }
234}
235
236fn unsupported(input: &'static str) -> GeomError {
237 GeomError::UnsupportedInput {
238 backend: ReferenceExactCompiler::ID,
239 operation: Operation::GraphCompilation,
240 input,
241 }
242}
243
244fn exact_input_family(node: &GeometryNode) -> &'static str {
245 match node {
246 GeometryNode::Point2(_) => "2D point",
247 GeometryNode::Point3(_) => "3D point",
248 GeometryNode::Vector2(_) => "2D vector",
249 GeometryNode::Vector3(_) => "3D vector",
250 GeometryNode::Frame2(_) => "2D frame",
251 GeometryNode::Frame3(_) => "3D frame",
252 GeometryNode::Transform(_) => "transform",
253 GeometryNode::PointList2(_) => "2D point list",
254 GeometryNode::PointList3(_) => "3D point list",
255 GeometryNode::Primitive(_) => "primitive",
256 GeometryNode::Curve2(_) => "2D curve",
257 GeometryNode::Curve3(_) => "3D curve",
258 GeometryNode::CurveRelation(_) => "curve relation",
259 GeometryNode::PointOnCurve(_) => "point on curve",
260 GeometryNode::Surface(_) => "surface",
261 GeometryNode::SurfaceRelation(_) => "surface relation",
262 GeometryNode::PointOnSurface(_) => "point on surface",
263 GeometryNode::Profile(_) => "profile",
264 GeometryNode::OpenProfile(_) => "open profile",
265 GeometryNode::HalfSpace(_) => "half-space",
266 GeometryNode::BRep(_) => "B-rep",
267 GeometryNode::PolygonMesh(_) => "polygon mesh",
268 GeometryNode::TriMesh(_) => "triangle mesh",
269 GeometryNode::BoundingBox(_) => "bounding box",
270 GeometryNode::SolidOperation(operation) => solid_operation_family(operation),
271 GeometryNode::Instance(_) => "instance",
272 GeometryNode::Collection(_) => "collection",
273 _ => "unknown geometry node",
274 }
275}
276
277pub(crate) fn solid_operation_family(operation: &SolidOperation) -> &'static str {
278 match operation {
279 SolidOperation::Extrusion { .. } => "extrusion",
280 SolidOperation::TaperedExtrusion { .. } => "tapered extrusion",
281 SolidOperation::Revolution { .. } => "revolution",
282 SolidOperation::TaperedRevolution { .. } => "tapered revolution",
283 SolidOperation::SweptDisk { .. } => "swept disk",
284 SolidOperation::FixedReferenceSweep { .. } => "fixed-reference sweep",
285 SolidOperation::SurfaceCurveSweep { .. } => "surface-curve sweep",
286 SolidOperation::SectionedSpine { .. } => "sectioned spine",
287 SolidOperation::Boolean { .. } => "boolean",
288 SolidOperation::BoundedHalfSpace { .. } => "bounded half-space",
289 _ => "unknown solid operation",
290 }
291}
292
293#[cfg(test)]
294mod family_name_tests {
295 use super::{solid_operation_family, SOLID_FAMILY_NAMES};
296 use axiolid_core::Vec3;
297 use axiolid_model::{GeometryGraphBuilder, GeometryNode, SolidOperation};
298 use axiolid_profile::{Profile, RectangleProfile};
299
300 #[test]
307 fn the_exported_table_matches_the_naming_function() {
308 assert_eq!(
309 SOLID_FAMILY_NAMES.len(),
310 10,
311 "the declared family count changed; update the table"
312 );
313 for name in SOLID_FAMILY_NAMES {
314 assert!(!name.is_empty());
315 assert_ne!(*name, "unknown solid operation");
316 }
317
318 let mut builder = GeometryGraphBuilder::new();
320 let profile = builder
321 .push(GeometryNode::Profile(Profile::Rectangle(
322 RectangleProfile {
323 x: 1.0,
324 y: 1.0,
325 thickness: None,
326 outer_radius: None,
327 inner_radius: None,
328 },
329 )))
330 .expect("a rectangle profile is a valid node");
331
332 let extrusion = SolidOperation::Extrusion {
333 profile,
334 direction: Vec3::Z,
335 depth: 1.0,
336 };
337 assert!(
338 SOLID_FAMILY_NAMES.contains(&solid_operation_family(&extrusion)),
339 "the function returned a name the table does not carry"
340 );
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use axiolid_core::{Tolerance, Vec3};
347 use axiolid_model::GeometryGraphBuilder;
348 use axiolid_profile::{Profile, RectangleProfile};
349
350 use super::*;
351
352 #[test]
353 fn duplicate_roots_hit_the_exact_result_cache() {
354 let mut builder = GeometryGraphBuilder::new();
355 let profile = builder
356 .push(GeometryNode::Profile(Profile::Rectangle(
357 RectangleProfile {
358 x: 2.0,
359 y: 1.0,
360 thickness: None,
361 outer_radius: None,
362 inner_radius: None,
363 },
364 )))
365 .unwrap();
366 let root = builder
367 .push(GeometryNode::SolidOperation(SolidOperation::Extrusion {
368 profile,
369 direction: Vec3::Z,
370 depth: 1.0,
371 }))
372 .unwrap();
373 let graph = builder.finish(vec![root]).unwrap();
374 let options = ExecutionOptions::new(Tolerance::METRE);
375 let mut compilation = ExactCompilation::new(&graph, &options);
376
377 let first = compilation.compile(root).unwrap();
378 let second = compilation.compile(root).unwrap();
379
380 assert_eq!(first, second);
381 assert_eq!(compilation.evaluated_nodes, 1);
382 assert_eq!(compilation.cache_hits, 1);
383 assert_eq!(compilation.cache.len(), 1);
384 }
385}
386
387pub const SOLID_FAMILY_NAMES: &[&str] = &[
393 "extrusion",
394 "tapered extrusion",
395 "revolution",
396 "tapered revolution",
397 "swept disk",
398 "fixed-reference sweep",
399 "surface-curve sweep",
400 "sectioned spine",
401 "boolean",
402 "bounded half-space",
403];