brep_kernel/edit/direct_edit/
face_offset_torus.rs1use super::*;
2use crate::{KernelRefusal, KernelStage, OrRefuse};
3
4pub fn offset_torus_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_torus_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 fit_tolerance = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
25 let (shell_index, face_index) =
26 find_face(solid, face_id).ok_or_else(|| {
27 KernelRefusal::input(
28 KernelStage::Collect,
29 "face_id",
30 format!("offset_torus_face: no face {face_id}"),
31 )
32 })?;
33 let face = &solid.shells[shell_index].faces[face_index];
34 let Some(AnalyticSurface::Torus {
35 frame,
36 major_radius,
37 minor_radius,
38 }) = face.surface.analytic()
39 else {
40 return Err(KernelRefusal::input(
41 KernelStage::Collect,
42 "not_torus",
43 "offset_torus_face: the pushed face is not a torus",
44 ));
45 };
46
47 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Classify, "domain_u")?;
51 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Classify, "domain_v")?;
52 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
53 let point = face.surface.evaluate(um, vm).or_refuse(KernelStage::Classify, "evaluate")?;
54 let mut normal = face.surface.normal(um, vm).or_refuse(KernelStage::Classify, "normal")?;
55 if !face.same_sense {
56 normal = normal.scale(-1.0);
57 }
58 let relative = point.sub(frame.origin);
59 let axial = relative.dot(frame.axis);
60 let radial_direction = relative.sub(frame.axis.scale(axial)).normalized().or_refuse(KernelStage::Classify, "normalized")?;
61 let tube_center = frame
62 .origin
63 .add(frame.axis.scale(axial))
64 .add(radial_direction.scale(*major_radius));
65 let tube_radial = point.sub(tube_center);
66 let outward_sign = if normal.dot(tube_radial) >= 0.0 {
67 1.0
68 } else {
69 -1.0
70 };
71 let minor_new = *minor_radius + distance * outward_sign;
72 if minor_new <= tolerance {
73 return Err(KernelRefusal::input(
74 KernelStage::Classify,
75 "tube_collapse",
76 "offset_torus_face: the push collapses the tube radius — refusing",
77 ));
78 }
79 if minor_new >= *major_radius - tolerance {
80 return Err(KernelRefusal::unsupported(
81 KernelStage::Classify,
82 "spindle_torus",
83 "offset_torus_face: the push creates a horn/spindle torus — refusing",
84 ));
85 }
86
87 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
90 for shell in &solid.shells {
91 for candidate in &shell.faces {
92 for loop_record in &candidate.loops {
93 for coedge in &loop_record.coedges {
94 faces_of_edge
95 .entry(coedge.edge_id)
96 .or_default()
97 .push(candidate.id);
98 }
99 }
100 }
101 }
102 for loop_record in &face.loops {
103 for coedge in &loop_record.coedges {
104 let incident = faces_of_edge
105 .get(&coedge.edge_id)
106 .cloned()
107 .unwrap_or_default();
108 if incident.iter().any(|candidate| *candidate != face_id) {
109 return Err(KernelRefusal::unsupported(
110 KernelStage::Classify,
111 "neighbour_trimmed",
112 "offset_torus_face: a neighbour-trimmed torus is deferred; only the \
113 full torus is supported in this slice",
114 ));
115 }
116 }
117 }
118
119 let offset = crate::offset_surface(face, -distance, 0.0)?;
123 match offset.analytic() {
124 Some(AnalyticSurface::Torus {
125 frame: out_frame,
126 major_radius: out_major,
127 minor_radius: out_minor,
128 }) if out_frame.origin.sub(frame.origin).length() <= tolerance
129 && out_frame.axis.dot(frame.axis).abs() >= 1.0 - 1e-9
130 && (*out_major - *major_radius).abs() <= tolerance
131 && (*out_minor - minor_new).abs() <= tolerance => {}
132 other => {
133 return Err(KernelRefusal::internal(
134 KernelStage::Refine,
135 "offset_carrier",
136 format!(
137 "offset_torus_face: offset carrier was not the expected exact torus: {other:?}"
138 ),
139 ))
140 }
141 }
142
143 let edge_by_id: HashMap<u64, &EdgeRecord> =
144 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
145 let mut rebuilt_edges: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
146 let mut rebuilt_vertices: HashMap<u64, Vec3> = HashMap::default();
147 for loop_record in &face.loops {
148 for coedge in &loop_record.coedges {
149 if rebuilt_edges.contains_key(&coedge.edge_id) {
150 continue;
151 }
152 let edge = *edge_by_id
153 .get(&coedge.edge_id)
154 .ok_or_else(|| {
155 KernelRefusal::internal(
156 KernelStage::Fragment,
157 "missing_edge",
158 format!("offset_torus_face: missing edge {}", coedge.edge_id),
159 )
160 })?;
161 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
162 let a = coedge.pcurve.evaluate(q0).or_refuse(KernelStage::Fragment, "evaluate")?;
163 let b = coedge.pcurve.evaluate(q1).or_refuse(KernelStage::Fragment, "evaluate")?;
164 let m = coedge.pcurve.evaluate(0.5 * (q0 + q1)).or_refuse(KernelStage::Fragment, "evaluate")?;
165 let uv_tolerance = 1e-8;
166 let iso = if (a.x - b.x).abs() <= uv_tolerance && (a.x - m.x).abs() <= uv_tolerance {
170 Some((offset.iso_curve_u(a.x).or_refuse(KernelStage::Fragment, "iso_curve_u")?, a.y, b.y))
171 } else if (a.y - b.y).abs() <= uv_tolerance && (a.y - m.y).abs() <= uv_tolerance {
172 Some((offset.iso_curve_v(a.y).or_refuse(KernelStage::Fragment, "iso_curve_v")?, a.x, b.x))
173 } else {
174 None
175 };
176 let (base, mut start, mut end) = match iso {
180 Some(triple) => triple,
181 None => {
182 let image = crate::image_curve(
183 &offset,
184 &coedge.pcurve,
185 fit_tolerance,
186 "offset_torus_face",
187 ).or_refuse(KernelStage::Fragment, "image_curve")?;
188 (image.curve, image.t0, image.t1)
189 }
190 };
191 if !coedge.forward {
192 std::mem::swap(&mut start, &mut end);
193 }
194 let [d0, d1] = base.domain().or_refuse(KernelStage::Fragment, "domain")?;
195 let (curve, t0, t1) = if start <= end {
196 (base, start, end)
197 } else {
198 (base.reversed().or_refuse(KernelStage::Fragment, "reversed")?, d0 + d1 - start, d0 + d1 - end)
199 };
200 let start_point = curve.evaluate(t0).or_refuse(KernelStage::Fragment, "evaluate")?;
201 let end_point = curve.evaluate(t1).or_refuse(KernelStage::Fragment, "evaluate")?;
202 for (vertex_id, vertex_point) in [
203 (edge.start_vertex_id, start_point),
204 (edge.end_vertex_id, end_point),
205 ] {
206 if let Some(known) = rebuilt_vertices.get(&vertex_id) {
207 if known.sub(vertex_point).length() > tolerance {
208 return Err(KernelRefusal::internal(
209 KernelStage::Fragment,
210 "seam_agreement",
211 format!(
212 "offset_torus_face: periodic seams disagree at vertex {vertex_id}"
213 ),
214 ));
215 }
216 } else {
217 rebuilt_vertices.insert(vertex_id, vertex_point);
218 }
219 }
220 rebuilt_edges.insert(edge.id, (curve, t0, t1));
221 }
222 }
223
224 finish_intrinsic_face_offset(
225 solid,
226 (shell_index, face_index),
227 offset,
228 &rebuilt_edges,
229 &rebuilt_vertices,
230 "offset_torus_face",
231 )
232
233}
234