brep_kernel/edit/direct_edit/
face_offset_freeform.rs1use super::*;
2
3pub fn offset_freeform_face(
9 solid: &BrepSolid,
10 face_id: u64,
11 distance: f64,
12) -> Result<BrepSolid, String> {
13 if !distance.is_finite() {
14 return Err("offset_freeform_face: distance must be finite".into());
15 }
16 let scale = solid_model_scale(solid);
17 let tolerance = (scale * 1e-7).max(1e-9);
18 let residual_tolerance = (scale * 5e-4).max(5e-6);
21 let fit_tolerance = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
23 let (shell_index, face_index) = find_face(solid, face_id)
24 .ok_or_else(|| format!("offset_freeform_face: no face {face_id}"))?;
25 let face = &solid.shells[shell_index].faces[face_index];
26 if face.surface.analytic().is_some() {
27 return Err("offset_freeform_face: the pushed carrier is analytic".into());
28 }
29
30 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
31 for shell in &solid.shells {
32 for candidate in &shell.faces {
33 for loop_record in &candidate.loops {
34 for coedge in &loop_record.coedges {
35 faces_of_edge
36 .entry(coedge.edge_id)
37 .or_default()
38 .push(candidate.id);
39 }
40 }
41 }
42 }
43 for loop_record in &face.loops {
44 for coedge in &loop_record.coedges {
45 let incident = faces_of_edge
46 .get(&coedge.edge_id)
47 .cloned()
48 .unwrap_or_default();
49 if incident.iter().any(|candidate| *candidate != face_id) {
50 return Err(
51 "offset_freeform_face: neighbour-trimmed NURBS faces require general \
52 surface intersection and are deferred — refusing"
53 .into(),
54 );
55 }
56 }
57 }
58
59 let displacement = if face.same_sense { distance } else { -distance };
67 let region = crate::offset_regularity::TrimRegion::from_face(face)?;
68 let scan = crate::offset_regularity::scan_offset_regularity(
69 &face.surface,
70 ®ion,
71 &[displacement],
72 crate::thicken::FOLD_FACTOR,
73 crate::offset_regularity::ScanBudget::SHEET,
74 )?;
75 if let Some(worst) = scan.worst.filter(|worst| worst.factor <= crate::thicken::FOLD_FACTOR) {
76 return Err(format!(
77 "offset_freeform_face: a push by {distance} folds the offset through the face's evolute \
78 — inside its trim the curvature radius {:.6} at (u={:.6}, v={:.6}) is not larger \
79 than the push (fold factor {:.3e}), and a push cannot carve the fold away",
80 worst.radius(),
81 worst.u,
82 worst.v,
83 worst.factor
84 ));
85 }
86 let offset = crate::offset_surface(face, -distance, 0.0)?;
87 let offsets = OffsetEvaluator::new(
90 "push_freeform",
91 &face.surface,
92 OffsetNormal::Face {
93 same_sense: face.same_sense,
94 },
95 );
96 let [u0, u1] = face.surface.domain_u()?;
97 let [v0, v1] = face.surface.domain_v()?;
98 let target = distance.abs();
99 for iu in 0..15 {
100 for iv in 0..15 {
101 let u = u0 + (u1 - u0) * (iu as f64 + 0.31) / 15.0;
102 let v = v0 + (v1 - v0) * (iv as f64 + 0.43) / 15.0;
103 let source_point = face.surface.evaluate(u, v)?;
104 let offset_point = offset.evaluate(u, v)?;
105 let delta = offset_point.sub(source_point);
106 let normal = offsets.normal(u, v)?;
107 let distance_error = (delta.length() - target).abs();
108 let normal_error = if delta.length() > tolerance {
109 1.0 - delta.normalized()?.dot(normal).abs()
110 } else {
111 0.0
112 };
113 if distance_error > residual_tolerance || normal_error > 2e-3 {
114 return Err(format!(
115 "offset_freeform_face: offset fit exceeds tolerance \
116 (distance error {distance_error:.3e}, normal error {normal_error:.3e})"
117 ));
118 }
119 }
120 }
121
122 let edge_by_id: HashMap<u64, &EdgeRecord> =
123 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
124 let mut rebuilt_edges: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
125 let mut rebuilt_vertices: HashMap<u64, Vec3> = HashMap::default();
126 for loop_record in &face.loops {
127 for coedge in &loop_record.coedges {
128 if rebuilt_edges.contains_key(&coedge.edge_id) {
129 continue;
130 }
131 let edge = *edge_by_id
132 .get(&coedge.edge_id)
133 .ok_or_else(|| format!("offset_freeform_face: missing edge {}", coedge.edge_id))?;
134 let [q0, q1] = coedge.pcurve.domain()?;
135 let a = coedge.pcurve.evaluate(q0)?;
136 let b = coedge.pcurve.evaluate(q1)?;
137 let m = coedge.pcurve.evaluate(0.5 * (q0 + q1))?;
138 let uv_tolerance = 1e-7;
139 let iso = if (a.x - b.x).abs() <= uv_tolerance && (a.x - m.x).abs() <= uv_tolerance {
143 Some((offset.iso_curve_u(a.x)?, a.y, b.y))
144 } else if (a.y - b.y).abs() <= uv_tolerance && (a.y - m.y).abs() <= uv_tolerance {
145 Some((offset.iso_curve_v(a.y)?, a.x, b.x))
146 } else {
147 None
148 };
149 let (base, mut start, mut end) = match iso {
157 Some(triple) => triple,
158 None => {
159 let image = crate::image_curve(
160 &offset,
161 &coedge.pcurve,
162 fit_tolerance,
163 "offset_freeform_face",
164 )?;
165 (image.curve, image.t0, image.t1)
166 }
167 };
168 if !coedge.forward {
169 std::mem::swap(&mut start, &mut end);
170 }
171 let [d0, d1] = base.domain()?;
172 let (curve, t0, t1) = if start <= end {
173 (base, start, end)
174 } else {
175 (base.reversed()?, d0 + d1 - start, d0 + d1 - end)
176 };
177 let start_point = curve.evaluate(t0)?;
178 let end_point = curve.evaluate(t1)?;
179 for (vertex_id, point) in [
180 (edge.start_vertex_id, start_point),
181 (edge.end_vertex_id, end_point),
182 ] {
183 if let Some(known) = rebuilt_vertices.get(&vertex_id) {
184 if known.sub(point).length() > residual_tolerance {
185 return Err(format!(
186 "offset_freeform_face: intrinsic boundaries disagree at vertex {vertex_id}"
187 ));
188 }
189 } else {
190 rebuilt_vertices.insert(vertex_id, point);
191 }
192 }
193 rebuilt_edges.insert(edge.id, (curve, t0, t1));
194 }
195 }
196
197 finish_intrinsic_face_offset(
198 solid,
199 (shell_index, face_index),
200 offset,
201 &rebuilt_edges,
202 &rebuilt_vertices,
203 "offset_freeform_face",
204 )
205}
206