1use crate::fit::solve_dense;
2use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
3use crate::{interpolate_curve, KnotVector, NurbsCurve, NurbsSurface, Vec3, Vec4};
4use rustc_hash::FxHashMap as HashMap;
5use serde::Serialize;
6
7fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
8 Ok((
9 KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
10 KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
11 ))
12}
13
14fn stable_face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, String> {
15 let normal_at = |u, v| face.surface.normal(u, v).ok();
16 let mut normal = normal_at(u, v);
17 let ([u0, u1], [v0, v1]) = domains(&face.surface)?;
18 if normal.is_none() {
19 let du = (u1 - u0) * 1e-5;
20 let dv = (v1 - v0) * 1e-5;
21 for (candidate_u, candidate_v) in [
22 ((u + du).clamp(u0, u1), v),
23 ((u - du).clamp(u0, u1), v),
24 (u, (v + dv).clamp(v0, v1)),
25 (u, (v - dv).clamp(v0, v1)),
26 ] {
27 normal = normal_at(candidate_u, candidate_v);
28 if normal.is_some() {
29 break;
30 }
31 }
32 }
33 let singular_here = {
44 let du = (u1 - u0) * 1e-4;
45 let dv = (v1 - v0) * 1e-4;
46 let here = face.surface.evaluate(u, v)?;
47 let along_u = face
48 .surface
49 .evaluate((u + du).clamp(u0, u1), v)?
50 .sub(here)
51 .length()
52 .max(
53 face.surface
54 .evaluate((u - du).clamp(u0, u1), v)?
55 .sub(here)
56 .length(),
57 );
58 let along_v = face
59 .surface
60 .evaluate(u, (v + dv).clamp(v0, v1))?
61 .sub(here)
62 .length()
63 .max(
64 face.surface
65 .evaluate(u, (v - dv).clamp(v0, v1))?
66 .sub(here)
67 .length(),
68 );
69 let scale = along_u.max(along_v);
70 scale > 0.0 && along_u.min(along_v) < scale * 1e-6
71 };
72 if singular_here {
73 let v_mid = (v0 + v1) * 0.5;
74 let u_mid = (u0 + u1) * 0.5;
75 let mut interior = None;
76 for fraction in [1e-3, 1e-2, 5e-2, 0.25] {
77 let candidate_v = v + (v_mid - v) * fraction;
78 let candidate_u = u + (u_mid - u) * fraction;
79 for (cu, cv) in [(u, candidate_v), (candidate_u, v), (candidate_u, candidate_v)] {
80 if let Some(candidate) = normal_at(cu, cv) {
81 interior = Some(candidate);
82 break;
83 }
84 }
85 if interior.is_some() {
86 break;
87 }
88 }
89 normal = match (normal, interior) {
90 (Some(at_point), Some(interior)) if at_point.dot(interior) > 1.0 - 1e-6 => {
91 Some(at_point)
92 }
93 (_, Some(interior)) => Some(interior),
94 (at_point, None) => at_point,
95 };
96 }
97 let normal =
98 normal.ok_or_else(|| "offset_surface: cannot determine surface normal".to_string())?;
99 Ok(if face.same_sense {
100 normal
101 } else {
102 normal.scale(-1.0)
103 })
104}
105
106fn greville_parameters(knots: &KnotVector) -> Vec<f64> {
107 let mut parameters = (0..knots.control_point_count())
108 .map(|index| {
109 knots.knots[index + 1..=index + knots.degree]
110 .iter()
111 .sum::<f64>()
112 / knots.degree as f64
113 })
114 .collect::<Vec<_>>();
115 let domain = knots.domain();
116 parameters[0] = domain[0];
117 *parameters.last_mut().unwrap() = domain[1];
118 parameters
119}
120
121fn collocation_matrix(knots: &KnotVector, parameters: &[f64], weights: &[f64]) -> Vec<Vec<f64>> {
125 parameters
126 .iter()
127 .map(|parameter| {
128 let mut row = vec![0.0; knots.control_point_count()];
129 let span = knots.find_span(*parameter);
130 for (offset, value) in knots
131 .basis_functions(span, *parameter)
132 .into_iter()
133 .enumerate()
134 {
135 let index = span - knots.degree + offset;
136 row[index] = value * weights[index];
137 }
138 let denominator: f64 = row.iter().sum();
139 if denominator.abs() > 0.0 {
140 for value in &mut row {
141 *value /= denominator;
142 }
143 }
144 row
145 })
146 .collect()
147}
148
149fn separable_weights(weights: &[Vec<f64>]) -> Option<(Vec<f64>, Vec<f64>)> {
153 let first_row = weights.first()?;
154 let anchor = *first_row.first()?;
155 if anchor.abs() <= 1e-12 {
156 return None;
157 }
158 let a: Vec<f64> = weights.iter().map(|row| row[0]).collect();
159 let b: Vec<f64> = first_row.iter().map(|w| w / anchor).collect();
160 for (i, row) in weights.iter().enumerate() {
161 for (j, &w) in row.iter().enumerate() {
162 if (w - a[i] * b[j]).abs() > 1e-10 * (1.0 + w.abs()) {
163 return None;
164 }
165 }
166 }
167 Some((a, b))
168}
169
170fn interpolate_tensor(
176 knot_u: &KnotVector,
177 knot_v: &KnotVector,
178 parameters_u: &[f64],
179 parameters_v: &[f64],
180 samples: &[Vec<Vec3>],
181 weights: &[Vec<f64>],
182) -> Result<Vec<Vec<Vec4>>, String> {
183 let count_u = parameters_u.len();
184 let count_v = parameters_v.len();
185 if let Some((weights_u, weights_v)) = separable_weights(weights) {
186 let matrix_u = collocation_matrix(knot_u, parameters_u, &weights_u);
187 let matrix_v = collocation_matrix(knot_v, parameters_v, &weights_v);
188 let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
189 for column in 0..count_v {
190 let solve_axis = |axis: fn(Vec3) -> f64| {
191 solve_dense(
192 matrix_u.clone(),
193 samples.iter().map(|row| axis(row[column])).collect(),
194 )
195 };
196 let x = solve_axis(|point| point.x)?;
197 let y = solve_axis(|point| point.y)?;
198 let z = solve_axis(|point| point.z)?;
199 for row in 0..count_u {
200 intermediate[row][column] = Vec3::new(x[row], y[row], z[row]);
201 }
202 }
203 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
204 for row in 0..count_u {
205 let solve_axis = |axis: fn(Vec3) -> f64| {
206 solve_dense(
207 matrix_v.clone(),
208 intermediate[row].iter().copied().map(axis).collect(),
209 )
210 };
211 let x = solve_axis(|point| point.x)?;
212 let y = solve_axis(|point| point.y)?;
213 let z = solve_axis(|point| point.z)?;
214 for column in 0..count_v {
215 controls[row][column] = Vec4::from_point(
216 Vec3::new(x[column], y[column], z[column]),
217 weights[row][column],
218 );
219 }
220 }
221 return Ok(controls);
222 }
223
224 let unknowns = count_u * count_v;
227 let mut matrix = vec![vec![0.0; unknowns]; unknowns];
228 for (k, &u) in parameters_u.iter().enumerate() {
229 let span_u = knot_u.find_span(u);
230 let basis_u = knot_u.basis_functions(span_u, u);
231 for (l, &v) in parameters_v.iter().enumerate() {
232 let span_v = knot_v.find_span(v);
233 let basis_v = knot_v.basis_functions(span_v, v);
234 let row = &mut matrix[k * count_v + l];
235 let mut denominator = 0.0;
236 for (du, value_u) in basis_u.iter().enumerate() {
237 let i = span_u - knot_u.degree + du;
238 for (dv, value_v) in basis_v.iter().enumerate() {
239 let j = span_v - knot_v.degree + dv;
240 let entry = value_u * value_v * weights[i][j];
241 row[i * count_v + j] = entry;
242 denominator += entry;
243 }
244 }
245 if denominator.abs() > 0.0 {
246 for value in row.iter_mut() {
247 *value /= denominator;
248 }
249 }
250 }
251 }
252 let solve_axis = |axis: fn(Vec3) -> f64| {
253 solve_dense(
254 matrix.clone(),
255 samples
256 .iter()
257 .flat_map(|row| row.iter().copied().map(axis))
258 .collect(),
259 )
260 };
261 let x = solve_axis(|point| point.x)?;
262 let y = solve_axis(|point| point.y)?;
263 let z = solve_axis(|point| point.z)?;
264 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
265 for row in 0..count_u {
266 for column in 0..count_v {
267 let index = row * count_v + column;
268 controls[row][column] = Vec4::from_point(
269 Vec3::new(x[index], y[index], z[index]),
270 weights[row][column],
271 );
272 }
273 }
274 Ok(controls)
275}
276
277pub fn offset_surface(
281 face: &FaceRecord,
282 distance: f64,
283 planar_extension: f64,
284) -> Result<NurbsSurface, String> {
285 let source = &face.surface;
286 if source.is_affine()? {
287 let ([u0, u1], [v0, v1]) = domains(source)?;
288 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
289 let shift = normal.scale(-distance);
290 let mut points = source
291 .control_points
292 .iter()
293 .map(|row| {
294 row.iter()
295 .map(|control| Ok(control.point()?.add(shift)))
296 .collect::<Result<Vec<_>, String>>()
297 })
298 .collect::<Result<Vec<_>, String>>()?;
299 if planar_extension > 0.0 {
300 let p00 = points[0][0];
301 let p01 = points[0][1];
302 let p10 = points[1][0];
303 let direction_u = p10.sub(p00).normalized()?;
304 let direction_v = p01.sub(p00).normalized()?;
305 points[0][0] = p00
306 .sub(direction_u.scale(planar_extension))
307 .sub(direction_v.scale(planar_extension));
308 points[0][1] = p01
309 .sub(direction_u.scale(planar_extension))
310 .add(direction_v.scale(planar_extension));
311 points[1][0] = p10
312 .add(direction_u.scale(planar_extension))
313 .sub(direction_v.scale(planar_extension));
314 points[1][1] = points[1][1]
315 .add(direction_u.scale(planar_extension))
316 .add(direction_v.scale(planar_extension));
317 }
318 let controls = points
319 .into_iter()
320 .enumerate()
321 .map(|(row, points)| {
322 points
323 .into_iter()
324 .enumerate()
325 .map(|(column, point)| {
326 Vec4::from_point(point, source.control_points[row][column].w)
327 })
328 .collect()
329 })
330 .collect();
331 return NurbsSurface::new(
332 source.degree_u,
333 source.degree_v,
334 source.knots_u.clone(),
335 source.knots_v.clone(),
336 controls,
337 );
338 }
339
340 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u)?;
341 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v)?;
342 let parameters_u = greville_parameters(&knot_u);
343 let parameters_v = greville_parameters(&knot_v);
344 let mut samples = Vec::new();
345 for &u in ¶meters_u {
346 let mut row = Vec::new();
347 for &v in ¶meters_v {
348 row.push(
349 source
350 .evaluate(u, v)?
351 .add(stable_face_normal(face, u, v)?.scale(-distance)),
352 );
353 }
354 samples.push(row);
355 }
356 if parameters_v.len() == 2 && parameters_u.len() >= 3 {
368 let centroid = |column: usize| {
369 let mut sum = Vec3::default();
370 for row in &samples {
371 sum = sum.add(row[column]);
372 }
373 sum.scale(1.0 / samples.len() as f64)
374 };
375 let near_centroid = centroid(0);
376 let far_centroid = centroid(1);
377 let mut inverted = true;
378 let mut pinch_fraction = 0.0f64;
379 let mut near_mean = 0.0f64;
380 let mut far_mean = 0.0f64;
381 for row in &samples {
382 let near_radial = row[0].sub(near_centroid);
383 let far_radial = row[1].sub(far_centroid);
384 let near_len = near_radial.length();
385 let far_len = far_radial.length();
386 if near_len <= 1e-9 || far_len <= 1e-9 {
387 inverted = false;
388 break;
389 }
390 if near_radial.dot(far_radial) >= 0.0 {
391 inverted = false;
392 break;
393 }
394 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
395 near_mean += near_len / samples.len() as f64;
396 far_mean += far_len / samples.len() as f64;
397 }
398 if inverted {
399 let retrim_far = far_mean <= near_mean;
402 for row in &mut samples {
403 let near = row[0];
404 let far = row[1];
405 let pinch = near.add(far.sub(near).scale(pinch_fraction));
406 if retrim_far {
407 row[1] = pinch;
408 } else {
409 row[0] = pinch;
410 }
411 }
412 }
413 if planar_extension > 0.0 && !inverted {
422 let mut min_ruling = f64::MAX;
423 let mut back_allowance = f64::MAX;
424 let mut forward_allowance = f64::MAX;
425 let mut extendable = true;
426 for row in &samples {
427 let ruling = row[1].sub(row[0]);
428 let length = ruling.length();
429 min_ruling = min_ruling.min(length);
430 let near_radial = row[0].sub(near_centroid).length();
434 let far_radial = row[1].sub(far_centroid).length();
435 if (far_radial - near_radial).abs() > 1e-9 {
436 let apex_at = near_radial / (near_radial - far_radial);
437 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
438 extendable = false;
440 break;
441 }
442 if apex_at < 0.0 {
443 back_allowance = back_allowance.min(0.9 * -apex_at);
444 } else {
445 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
446 }
447 }
448 }
449 if extendable && min_ruling > 1e-9 {
450 let stretch = planar_extension / min_ruling;
451 let back = stretch.min(back_allowance);
452 let forward = stretch.min(forward_allowance);
453 for row in &mut samples {
454 let ruling = row[1].sub(row[0]);
455 row[0] = row[0].sub(ruling.scale(back));
456 row[1] = row[1].add(ruling.scale(forward));
457 }
458 }
459 }
460 }
461 let weights = source
462 .control_points
463 .iter()
464 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
465 .collect::<Vec<_>>();
466 NurbsSurface::new(
467 source.degree_u,
468 source.degree_v,
469 source.knots_u.clone(),
470 source.knots_v.clone(),
471 interpolate_tensor(
472 &knot_u,
473 &knot_v,
474 ¶meters_u,
475 ¶meters_v,
476 &samples,
477 &weights,
478 )?,
479 )
480}
481
482fn mapped_pcurve_polyline(
483 surface: &NurbsSurface,
484 pcurve: &NurbsCurve,
485 degenerate: bool,
486) -> Result<(Vec<Vec3>, Vec<f64>), String> {
487 let [start, end] = pcurve.domain()?;
488 let evaluate = |fraction: f64| {
489 let uv = pcurve.evaluate(start + (end - start) * fraction)?;
490 surface.evaluate(uv.x, uv.y)
491 };
492 let first = evaluate(0.0)?;
493 let last = evaluate(1.0)?;
494 if degenerate {
495 return Ok((vec![first, last], vec![0.0, 1.0]));
496 }
497 fn append(
498 evaluate: &impl Fn(f64) -> Result<Vec3, String>,
499 a_fraction: f64,
500 a: Vec3,
501 b_fraction: f64,
502 b: Vec3,
503 depth: usize,
504 parameters: &mut Vec<f64>,
505 points: &mut Vec<Vec3>,
506 ) -> Result<(), String> {
507 let fractions =
508 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
509 let samples = fractions
510 .map(evaluate)
511 .into_iter()
512 .collect::<Result<Vec<_>, String>>()?;
513 let deviation = samples
514 .iter()
515 .enumerate()
516 .map(|(index, point)| {
517 point
518 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
519 .length()
520 })
521 .fold(0.0, f64::max);
522 if deviation <= 5e-4 || depth >= 10 {
523 parameters.push(b_fraction);
524 points.push(b);
525 return Ok(());
526 }
527 append(
528 evaluate,
529 a_fraction,
530 a,
531 fractions[1],
532 samples[1],
533 depth + 1,
534 parameters,
535 points,
536 )?;
537 append(
538 evaluate,
539 fractions[1],
540 samples[1],
541 b_fraction,
542 b,
543 depth + 1,
544 parameters,
545 points,
546 )
547 }
548 let mut parameters = vec![0.0];
549 let mut points = vec![first];
550 append(
551 &evaluate,
552 0.0,
553 first,
554 1.0,
555 last,
556 0,
557 &mut parameters,
558 &mut points,
559 )?;
560 Ok((points, parameters))
561}
562
563#[derive(Clone, Debug, Serialize)]
564pub struct OffsetFaceCarrier {
565 pub vertices: Vec<VertexRecord>,
566 pub edges: Vec<EdgeRecord>,
567 pub face: FaceRecord,
568}
569
570fn claim_vertex_image(
571 source_id: u64,
572 point: Vec3,
573 vertex_images: &mut HashMap<u64, u64>,
574 vertices: &mut Vec<VertexRecord>,
575 next_id: &mut u64,
576) -> u64 {
577 if let Some(id) = vertex_images.get(&source_id) {
578 return *id;
579 }
580 let id = *next_id;
581 *next_id += 1;
582 vertices.push(VertexRecord { id, point });
583 vertex_images.insert(source_id, id);
584 id
585}
586
587pub fn offset_face_carrier(
588 solid: &BrepSolid,
589 face_id: u64,
590 distance: f64,
591 planar_extension: f64,
592) -> Result<OffsetFaceCarrier, String> {
593 let source = solid
594 .shells
595 .iter()
596 .flat_map(|shell| &shell.faces)
597 .find(|face| face.id == face_id)
598 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}"))?;
599 let surface = offset_surface(source, distance, planar_extension)?;
600 let source_edges = solid
601 .edges
602 .iter()
603 .map(|edge| (edge.id, edge))
604 .collect::<HashMap<_, _>>();
605 let source_vertices = solid
606 .vertices
607 .iter()
608 .map(|vertex| (vertex.id, vertex))
609 .collect::<HashMap<_, _>>();
610 let mut vertices = Vec::new();
611 let mut vertex_images = HashMap::default();
612 let mut edges = Vec::new();
613 let mut edge_images = HashMap::default();
614 let mut loops = Vec::new();
615 let mut next_id = 1u64;
616
617 for source_loop in &source.loops {
618 let mut coedges = Vec::new();
619 for source_coedge in &source_loop.coedges {
620 let source_edge = source_edges
621 .get(&source_coedge.edge_id)
622 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string())?;
623 let (source_start, source_end) = if source_coedge.forward {
624 (source_edge.start_vertex_id, source_edge.end_vertex_id)
625 } else {
626 (source_edge.end_vertex_id, source_edge.start_vertex_id)
627 };
628 if !source_vertices.contains_key(&source_start)
629 || !source_vertices.contains_key(&source_end)
630 {
631 return Err("offset_face_carrier: missing source vertex".into());
632 }
633 let (points, parameters) =
651 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
652 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
653 let image_collapsed = points
654 .iter()
655 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
656 let (edge_id, forward) =
657 if let Some((edge_id, edge_start_vertex_id)) = edge_images.get(&source_edge.id) {
658 (
659 *edge_id,
660 vertex_images.get(&source_start) == Some(edge_start_vertex_id),
661 )
662 } else {
663 let curve = if image_collapsed {
664 NurbsCurve::new(
665 1,
666 vec![0.0, 0.0, 1.0, 1.0],
667 vec![
668 Vec4::from_point(points[0], 1.0),
669 Vec4::from_point(points[0], 1.0),
670 ],
671 )?
672 } else {
673 interpolate_curve(&points, 1, ¶meters)?
674 };
675 let start_vertex_id = claim_vertex_image(
676 source_start,
677 points[0],
678 &mut vertex_images,
679 &mut vertices,
680 &mut next_id,
681 );
682 let end_vertex_id = claim_vertex_image(
683 source_end,
684 points[points.len() - 1],
685 &mut vertex_images,
686 &mut vertices,
687 &mut next_id,
688 );
689 let id = next_id;
690 next_id += 1;
691 let domain = curve.domain()?;
692 edges.push(EdgeRecord {
693 id,
694 curve,
695 t0: domain[0],
696 t1: domain[1],
697 start_vertex_id,
698 end_vertex_id,
699 degenerate: source_edge.degenerate && image_collapsed,
703 name: source_edge
707 .name
708 .as_ref()
709 .map(|name| format!("{name}_Offset")),
710 });
711 edge_images.insert(source_edge.id, (id, start_vertex_id));
712 (id, true)
713 };
714 let id = next_id;
715 next_id += 1;
716 coedges.push(CoedgeRecord {
717 id,
718 edge_id,
719 forward,
720 pcurve: source_coedge.pcurve.clone(),
721 });
722 }
723 let id = next_id;
724 next_id += 1;
725 loops.push(LoopRecord { id, coedges });
726 }
727 Ok(OffsetFaceCarrier {
728 vertices,
729 edges,
730 face: FaceRecord {
731 id: next_id,
732 surface,
733 same_sense: source.same_sense,
734 loops,
735 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
736 },
737 })
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use crate::{make_box_brep, make_cylinder_brep};
744
745 #[test]
746 fn affine_offset_is_exact_and_preserves_weights() {
747 let solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
748 let face = &solid.shells[0].faces[0];
749 let offset = offset_surface(face, 0.75, 0.0).unwrap();
750 let domain_u = KnotVector::new(face.surface.knots_u.clone(), 1)
751 .unwrap()
752 .domain();
753 let domain_v = KnotVector::new(face.surface.knots_v.clone(), 1)
754 .unwrap()
755 .domain();
756 let u = (domain_u[0] + domain_u[1]) / 2.0;
757 let v = (domain_v[0] + domain_v[1]) / 2.0;
758 let displacement = offset
759 .evaluate(u, v)
760 .unwrap()
761 .sub(face.surface.evaluate(u, v).unwrap());
762 assert!((displacement.length() - 0.75).abs() < 1e-12);
763 }
764
765 #[test]
766 fn curved_offset_carrier_maps_every_trim_to_new_surface() {
767 let solid =
768 make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 4.0).unwrap();
769 let side = &solid.shells[0].faces[0];
770 let carrier = offset_face_carrier(&solid, side.id, 0.5, 0.0).unwrap();
771 for coedge in carrier
772 .face
773 .loops
774 .iter()
775 .flat_map(|loop_record| &loop_record.coedges)
776 {
777 let edge = carrier
778 .edges
779 .iter()
780 .find(|edge| edge.id == coedge.edge_id)
781 .unwrap();
782 for fraction in [0.0, 0.3, 0.8, 1.0] {
783 let uv = coedge.pcurve.evaluate(fraction).unwrap();
784 let on_surface = carrier.face.surface.evaluate(uv.x, uv.y).unwrap();
785 let parameter = if coedge.forward {
786 edge.t0 + (edge.t1 - edge.t0) * fraction
787 } else {
788 edge.t1 - (edge.t1 - edge.t0) * fraction
789 };
790 assert!(
791 on_surface
792 .sub(edge.curve.evaluate(parameter).unwrap())
793 .length()
794 < 7e-4
795 );
796 }
797 }
798 }
799}