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 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 offset = crate::offset_surface(face, -distance, 0.0)?;
107 let offsets = OffsetEvaluator::new(
110 "push_freeform",
111 &face.surface,
112 OffsetNormal::Face {
113 same_sense: face.same_sense,
114 },
115 );
116 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
117 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
118 let target = distance.abs();
119 for iu in 0..15 {
120 for iv in 0..15 {
121 let u = u0 + (u1 - u0) * (iu as f64 + 0.31) / 15.0;
122 let v = v0 + (v1 - v0) * (iv as f64 + 0.43) / 15.0;
123 let source_point = face.surface.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?;
124 let offset_point = offset.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?;
125 let delta = offset_point.sub(source_point);
126 let normal = offsets.normal(u, v).or_refuse(KernelStage::Refine, "normal")?;
127 let distance_error = (delta.length() - target).abs();
128 let normal_error = if delta.length() > tolerance {
129 1.0 - delta.normalized().or_refuse(KernelStage::Refine, "normalized")?.dot(normal).abs()
130 } else {
131 0.0
132 };
133 if distance_error > residual_tolerance || normal_error > 2e-3 {
134 return Err(KernelRefusal::non_convergence(
135 KernelStage::Refine,
136 "offset_fit",
137 format!(
138 "offset_freeform_face: offset fit exceeds tolerance \
139 (distance error {distance_error:.3e}, normal error {normal_error:.3e})"
140 ),
141 ));
142 }
143 }
144 }
145
146 let edge_by_id: HashMap<u64, &EdgeRecord> =
147 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
148 let mut rebuilt_edges: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
149 let mut rebuilt_vertices: HashMap<u64, Vec3> = HashMap::default();
150 for loop_record in &face.loops {
151 for coedge in &loop_record.coedges {
152 if rebuilt_edges.contains_key(&coedge.edge_id) {
153 continue;
154 }
155 let edge = *edge_by_id
156 .get(&coedge.edge_id)
157 .ok_or_else(|| {
158 KernelRefusal::internal(
159 KernelStage::Fragment,
160 "missing_edge",
161 format!("offset_freeform_face: missing edge {}", coedge.edge_id),
162 )
163 })?;
164 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
165 let a = coedge.pcurve.evaluate(q0).or_refuse(KernelStage::Fragment, "evaluate")?;
166 let b = coedge.pcurve.evaluate(q1).or_refuse(KernelStage::Fragment, "evaluate")?;
167 let m = coedge.pcurve.evaluate(0.5 * (q0 + q1)).or_refuse(KernelStage::Fragment, "evaluate")?;
168 let uv_tolerance = 1e-7;
169 let iso = if (a.x - b.x).abs() <= uv_tolerance && (a.x - m.x).abs() <= uv_tolerance {
173 Some((offset.iso_curve_u(a.x).or_refuse(KernelStage::Fragment, "iso_curve_u")?, a.y, b.y))
174 } else if (a.y - b.y).abs() <= uv_tolerance && (a.y - m.y).abs() <= uv_tolerance {
175 Some((offset.iso_curve_v(a.y).or_refuse(KernelStage::Fragment, "iso_curve_v")?, a.x, b.x))
176 } else {
177 None
178 };
179 let (base, mut start, mut end) = match iso {
187 Some(triple) => triple,
188 None => {
189 let image = crate::image_curve(
190 &offset,
191 &coedge.pcurve,
192 fit_tolerance,
193 "offset_freeform_face",
194 ).or_refuse(KernelStage::Fragment, "image_curve")?;
195 (image.curve, image.t0, image.t1)
196 }
197 };
198 if !coedge.forward {
199 std::mem::swap(&mut start, &mut end);
200 }
201 let [d0, d1] = base.domain().or_refuse(KernelStage::Fragment, "domain")?;
202 let (curve, t0, t1) = if start <= end {
203 (base, start, end)
204 } else {
205 (base.reversed().or_refuse(KernelStage::Fragment, "reversed")?, d0 + d1 - start, d0 + d1 - end)
206 };
207 let start_point = curve.evaluate(t0).or_refuse(KernelStage::Fragment, "evaluate")?;
208 let end_point = curve.evaluate(t1).or_refuse(KernelStage::Fragment, "evaluate")?;
209 for (vertex_id, point) in [
210 (edge.start_vertex_id, start_point),
211 (edge.end_vertex_id, end_point),
212 ] {
213 if let Some(known) = rebuilt_vertices.get(&vertex_id) {
214 if known.sub(point).length() > residual_tolerance {
215 return Err(KernelRefusal::internal(
216 KernelStage::Fragment,
217 "vertex_agreement",
218 format!(
219 "offset_freeform_face: intrinsic boundaries disagree at vertex {vertex_id}"
220 ),
221 ));
222 }
223 } else {
224 rebuilt_vertices.insert(vertex_id, point);
225 }
226 }
227 rebuilt_edges.insert(edge.id, (curve, t0, t1));
228 }
229 }
230
231 finish_intrinsic_face_offset(
232 solid,
233 (shell_index, face_index),
234 offset,
235 &rebuilt_edges,
236 &rebuilt_vertices,
237 "offset_freeform_face",
238 )
239
240}
241