brep_kernel/edit/direct_edit/
face_offset_freeform.rs1use super::*;
2use crate::{KernelRefusal, KernelStage, OrRefuse};
3
4pub fn offset_freeform_face(
10 solid: &BrepSolid,
11 face_id: u64,
12 distance: f64,
13) -> Result<BrepSolid, KernelRefusal> {
14 if !distance.is_finite() {
15 return Err(KernelRefusal::input(
16 KernelStage::Collect,
17 "distance",
18 "offset_freeform_face: distance must be finite",
19 ));
20 }
21 let scale = solid_model_scale(solid);
22 let tolerance = (scale * 1e-7).max(1e-9);
23 let residual_tolerance = (scale * 5e-4).max(5e-6);
26 let fit_tolerance = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
28 let (shell_index, face_index) = find_face(solid, face_id)
29 .ok_or_else(|| {
30 KernelRefusal::input(
31 KernelStage::Collect,
32 "face_id",
33 format!("offset_freeform_face: no face {face_id}"),
34 )
35 })?;
36 let face = &solid.shells[shell_index].faces[face_index];
37 if face.surface.analytic().is_some() {
38 return Err(KernelRefusal::input(
39 KernelStage::Collect,
40 "analytic_carrier",
41 "offset_freeform_face: the pushed carrier is analytic",
42 ));
43 }
44
45 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
46 for shell in &solid.shells {
47 for candidate in &shell.faces {
48 for loop_record in &candidate.loops {
49 for coedge in &loop_record.coedges {
50 faces_of_edge
51 .entry(coedge.edge_id)
52 .or_default()
53 .push(candidate.id);
54 }
55 }
56 }
57 }
58 for loop_record in &face.loops {
59 for coedge in &loop_record.coedges {
60 let incident = faces_of_edge
61 .get(&coedge.edge_id)
62 .cloned()
63 .unwrap_or_default();
64 if incident.iter().any(|candidate| *candidate != face_id) {
65 return Err(KernelRefusal::unsupported(
66 KernelStage::Classify,
67 "neighbour_trimmed",
68 "offset_freeform_face: neighbour-trimmed NURBS faces require general \
69 surface intersection and are deferred — refusing",
70 ));
71 }
72 }
73 }
74
75 let displacement = if face.same_sense { distance } else { -distance };
83 let region = crate::offset_regularity::TrimRegion::from_face(face)?;
84 let mut scan = crate::offset_regularity::scan_offset_regularity(
85 &face.surface,
86 ®ion,
87 &[displacement],
88 crate::thicken::FOLD_FACTOR,
89 crate::offset_regularity::ScanBudget::SHEET,
90 )?;
91 if let Some(worst) = scan.worst.filter(|worst| worst.factor <= crate::thicken::FOLD_FACTOR) {
92 return Err(KernelRefusal::unsupported(
93 KernelStage::Classify,
94 "fold",
95 format!(
96 "offset_freeform_face: a push by {distance} folds the offset through the face's evolute \
97 — inside its trim the curvature radius {:.6} at (u={:.6}, v={:.6}) is not larger \
98 than the push (fold factor {:.3e}), and a push cannot carve the fold away",
99 worst.radius(),
100 worst.u,
101 worst.v,
102 worst.factor
103 ),
104 ));
105 }
106 let certainty = scan.certify(&face.surface, ®ion, &[displacement], crate::thicken::FOLD_FACTOR);
110 if let crate::offset_regularity::Certainty::Folded(worst) = certainty {
111 return Err(KernelRefusal::unsupported(
114 KernelStage::Classify,
115 "fold",
116 format!(
117 "offset_freeform_face: a push by {distance} folds the offset through the face's evolute \
118 — inside its trim the curvature radius {:.6} at (u={:.6}, v={:.6}) is not larger \
119 than the push (fold factor {:.3e}, found between the scan's samples), and a push \
120 cannot carve the fold away",
121 worst.radius(),
122 worst.u,
123 worst.v,
124 worst.factor
125 ),
126 ));
127 }
128 if let crate::offset_regularity::Certainty::Uncertain {
129 unresolved,
130 budget_exhausted,
131 least,
132 } = certainty
133 {
134 return Err(KernelRefusal::unsupported(
135 KernelStage::Classify,
136 "fold_uncertain",
137 format!(
138 "offset_freeform_face: a push by {distance} cannot be certified regular over the \
139 face's trim — none of {} samples collapsed, but {} cell(s) were still risky at \
140 the scan's finest spacing{}{}; a push cannot carve a fold it cannot place",
141 scan.sampled,
142 unresolved,
143 if budget_exhausted { " and the refinement budget ran out" } else { "" },
144 least
145 .map(|least| format!(
146 " (least fold factor {:.3e} at (u={:.6}, v={:.6}))",
147 least.factor, least.u, least.v
148 ))
149 .unwrap_or_default()
150 ),
151 ));
152 }
153 let offset = crate::offset_surface(face, -distance, 0.0)?;
154 let offsets = OffsetEvaluator::new(
157 "push_freeform",
158 &face.surface,
159 OffsetNormal::Face {
160 same_sense: face.same_sense,
161 },
162 );
163 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
164 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
165 let target = distance.abs();
166 for iu in 0..15 {
167 for iv in 0..15 {
168 let u = u0 + (u1 - u0) * (iu as f64 + 0.31) / 15.0;
169 let v = v0 + (v1 - v0) * (iv as f64 + 0.43) / 15.0;
170 let source_point = face.surface.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?;
171 let offset_point = offset.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?;
172 let delta = offset_point.sub(source_point);
173 let normal = offsets.normal(u, v).or_refuse(KernelStage::Refine, "normal")?;
174 let distance_error = (delta.length() - target).abs();
175 let normal_error = if delta.length() > tolerance {
176 1.0 - delta.normalized().or_refuse(KernelStage::Refine, "normalized")?.dot(normal).abs()
177 } else {
178 0.0
179 };
180 if distance_error > residual_tolerance || normal_error > 2e-3 {
181 return Err(KernelRefusal::non_convergence(
182 KernelStage::Refine,
183 "offset_fit",
184 format!(
185 "offset_freeform_face: offset fit exceeds tolerance \
186 (distance error {distance_error:.3e}, normal error {normal_error:.3e})"
187 ),
188 ));
189 }
190 }
191 }
192
193 let edge_by_id: HashMap<u64, &EdgeRecord> =
194 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
195 let mut rebuilt_edges: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
196 let mut rebuilt_vertices: HashMap<u64, Vec3> = HashMap::default();
197 for loop_record in &face.loops {
198 for coedge in &loop_record.coedges {
199 if rebuilt_edges.contains_key(&coedge.edge_id) {
200 continue;
201 }
202 let edge = *edge_by_id
203 .get(&coedge.edge_id)
204 .ok_or_else(|| {
205 KernelRefusal::internal(
206 KernelStage::Fragment,
207 "missing_edge",
208 format!("offset_freeform_face: missing edge {}", coedge.edge_id),
209 )
210 })?;
211 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
212 let a = coedge.pcurve.evaluate(q0).or_refuse(KernelStage::Fragment, "evaluate")?;
213 let b = coedge.pcurve.evaluate(q1).or_refuse(KernelStage::Fragment, "evaluate")?;
214 let m = coedge.pcurve.evaluate(0.5 * (q0 + q1)).or_refuse(KernelStage::Fragment, "evaluate")?;
215 let uv_tolerance = 1e-7;
216 let iso = if (a.x - b.x).abs() <= uv_tolerance && (a.x - m.x).abs() <= uv_tolerance {
220 Some((offset.iso_curve_u(a.x).or_refuse(KernelStage::Fragment, "iso_curve_u")?, a.y, b.y))
221 } else if (a.y - b.y).abs() <= uv_tolerance && (a.y - m.y).abs() <= uv_tolerance {
222 Some((offset.iso_curve_v(a.y).or_refuse(KernelStage::Fragment, "iso_curve_v")?, a.x, b.x))
223 } else {
224 None
225 };
226 let (base, mut start, mut end) = match iso {
234 Some(triple) => triple,
235 None => {
236 let image = crate::image_curve(
237 &offset,
238 &coedge.pcurve,
239 fit_tolerance,
240 "offset_freeform_face",
241 ).or_refuse(KernelStage::Fragment, "image_curve")?;
242 (image.curve, image.t0, image.t1)
243 }
244 };
245 if !coedge.forward {
246 std::mem::swap(&mut start, &mut end);
247 }
248 let [d0, d1] = base.domain().or_refuse(KernelStage::Fragment, "domain")?;
249 let (curve, t0, t1) = if start <= end {
250 (base, start, end)
251 } else {
252 (base.reversed().or_refuse(KernelStage::Fragment, "reversed")?, d0 + d1 - start, d0 + d1 - end)
253 };
254 let start_point = curve.evaluate(t0).or_refuse(KernelStage::Fragment, "evaluate")?;
255 let end_point = curve.evaluate(t1).or_refuse(KernelStage::Fragment, "evaluate")?;
256 for (vertex_id, point) in [
257 (edge.start_vertex_id, start_point),
258 (edge.end_vertex_id, end_point),
259 ] {
260 if let Some(known) = rebuilt_vertices.get(&vertex_id) {
261 if known.sub(point).length() > residual_tolerance {
262 return Err(KernelRefusal::internal(
263 KernelStage::Fragment,
264 "vertex_agreement",
265 format!(
266 "offset_freeform_face: intrinsic boundaries disagree at vertex {vertex_id}"
267 ),
268 ));
269 }
270 } else {
271 rebuilt_vertices.insert(vertex_id, point);
272 }
273 }
274 rebuilt_edges.insert(edge.id, (curve, t0, t1));
275 }
276 }
277
278 finish_intrinsic_face_offset(
279 solid,
280 (shell_index, face_index),
281 offset,
282 &rebuilt_edges,
283 &rebuilt_vertices,
284 "offset_freeform_face",
285 )
286
287}
288