brep_kernel/edit/direct_edit/
face_offset_torus.rs1use super::*;
2
3pub fn offset_torus_face(
9 solid: &BrepSolid,
10 face_id: u64,
11 distance: f64,
12) -> Result<BrepSolid, String> {
13 if !distance.is_finite() {
14 return Err("offset_torus_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 fit_tolerance = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
20 let (shell_index, face_index) =
21 find_face(solid, face_id).ok_or_else(|| format!("offset_torus_face: no face {face_id}"))?;
22 let face = &solid.shells[shell_index].faces[face_index];
23 let Some(AnalyticSurface::Torus {
24 frame,
25 major_radius,
26 minor_radius,
27 }) = face.surface.analytic()
28 else {
29 return Err("offset_torus_face: the pushed face is not a torus".into());
30 };
31
32 let [u0, u1] = face.surface.domain_u()?;
36 let [v0, v1] = face.surface.domain_v()?;
37 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
38 let point = face.surface.evaluate(um, vm)?;
39 let mut normal = face.surface.normal(um, vm)?;
40 if !face.same_sense {
41 normal = normal.scale(-1.0);
42 }
43 let relative = point.sub(frame.origin);
44 let axial = relative.dot(frame.axis);
45 let radial_direction = relative.sub(frame.axis.scale(axial)).normalized()?;
46 let tube_center = frame
47 .origin
48 .add(frame.axis.scale(axial))
49 .add(radial_direction.scale(*major_radius));
50 let tube_radial = point.sub(tube_center);
51 let outward_sign = if normal.dot(tube_radial) >= 0.0 {
52 1.0
53 } else {
54 -1.0
55 };
56 let minor_new = *minor_radius + distance * outward_sign;
57 if minor_new <= tolerance {
58 return Err("offset_torus_face: the push collapses the tube radius — refusing".into());
59 }
60 if minor_new >= *major_radius - tolerance {
61 return Err("offset_torus_face: the push creates a horn/spindle torus — refusing".into());
62 }
63
64 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
67 for shell in &solid.shells {
68 for candidate in &shell.faces {
69 for loop_record in &candidate.loops {
70 for coedge in &loop_record.coedges {
71 faces_of_edge
72 .entry(coedge.edge_id)
73 .or_default()
74 .push(candidate.id);
75 }
76 }
77 }
78 }
79 for loop_record in &face.loops {
80 for coedge in &loop_record.coedges {
81 let incident = faces_of_edge
82 .get(&coedge.edge_id)
83 .cloned()
84 .unwrap_or_default();
85 if incident.iter().any(|candidate| *candidate != face_id) {
86 return Err(
87 "offset_torus_face: a neighbour-trimmed torus is deferred; only the \
88 full torus is supported in this slice"
89 .into(),
90 );
91 }
92 }
93 }
94
95 let offset = crate::offset_surface(face, -distance, 0.0)?;
99 match offset.analytic() {
100 Some(AnalyticSurface::Torus {
101 frame: out_frame,
102 major_radius: out_major,
103 minor_radius: out_minor,
104 }) if out_frame.origin.sub(frame.origin).length() <= tolerance
105 && out_frame.axis.dot(frame.axis).abs() >= 1.0 - 1e-9
106 && (*out_major - *major_radius).abs() <= tolerance
107 && (*out_minor - minor_new).abs() <= tolerance => {}
108 other => {
109 return Err(format!(
110 "offset_torus_face: offset carrier was not the expected exact torus: {other:?}"
111 ))
112 }
113 }
114
115 let edge_by_id: HashMap<u64, &EdgeRecord> =
116 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
117 let mut rebuilt_edges: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
118 let mut rebuilt_vertices: HashMap<u64, Vec3> = HashMap::default();
119 for loop_record in &face.loops {
120 for coedge in &loop_record.coedges {
121 if rebuilt_edges.contains_key(&coedge.edge_id) {
122 continue;
123 }
124 let edge = *edge_by_id
125 .get(&coedge.edge_id)
126 .ok_or_else(|| format!("offset_torus_face: missing edge {}", coedge.edge_id))?;
127 let [q0, q1] = coedge.pcurve.domain()?;
128 let a = coedge.pcurve.evaluate(q0)?;
129 let b = coedge.pcurve.evaluate(q1)?;
130 let m = coedge.pcurve.evaluate(0.5 * (q0 + q1))?;
131 let uv_tolerance = 1e-8;
132 let iso = if (a.x - b.x).abs() <= uv_tolerance && (a.x - m.x).abs() <= uv_tolerance {
136 Some((offset.iso_curve_u(a.x)?, a.y, b.y))
137 } else if (a.y - b.y).abs() <= uv_tolerance && (a.y - m.y).abs() <= uv_tolerance {
138 Some((offset.iso_curve_v(a.y)?, a.x, b.x))
139 } else {
140 None
141 };
142 let (base, mut start, mut end) = match iso {
146 Some(triple) => triple,
147 None => {
148 let image = crate::image_curve(
149 &offset,
150 &coedge.pcurve,
151 fit_tolerance,
152 "offset_torus_face",
153 )?;
154 (image.curve, image.t0, image.t1)
155 }
156 };
157 if !coedge.forward {
158 std::mem::swap(&mut start, &mut end);
159 }
160 let [d0, d1] = base.domain()?;
161 let (curve, t0, t1) = if start <= end {
162 (base, start, end)
163 } else {
164 (base.reversed()?, d0 + d1 - start, d0 + d1 - end)
165 };
166 let start_point = curve.evaluate(t0)?;
167 let end_point = curve.evaluate(t1)?;
168 for (vertex_id, vertex_point) in [
169 (edge.start_vertex_id, start_point),
170 (edge.end_vertex_id, end_point),
171 ] {
172 if let Some(known) = rebuilt_vertices.get(&vertex_id) {
173 if known.sub(vertex_point).length() > tolerance {
174 return Err(format!(
175 "offset_torus_face: periodic seams disagree at vertex {vertex_id}"
176 ));
177 }
178 } else {
179 rebuilt_vertices.insert(vertex_id, vertex_point);
180 }
181 }
182 rebuilt_edges.insert(edge.id, (curve, t0, t1));
183 }
184 }
185
186 finish_intrinsic_face_offset(
187 solid,
188 (shell_index, face_index),
189 offset,
190 &rebuilt_edges,
191 &rebuilt_vertices,
192 "offset_torus_face",
193 )
194}
195
196