Skip to main content

brep_kernel/edit/direct_edit/
face_offset_torus.rs

1use super::*;
2use crate::{KernelRefusal, KernelStage, OrRefuse};
3
4/// Push a toroidal face by changing its tube radius. Boundary edges must be
5/// intrinsic to this face; neighbour-trimmed rims require re-intersection and
6/// are refused. The carrier retains its knot vectors, weights, and UV basis,
7/// keeping pcurves valid. Iso boundaries rebuild exactly; other self-incident
8/// trims use [`crate::image_curve`].
9pub 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    // Curve-fit accuracy is separate from the surface and identity tolerances.
24    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    // Positive push follows the face's outward normal. For a cavity torus the
48    // topological normal opposes the tube radial, so positive distance shrinks
49    // the carrier instead of growing it.
50    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    // A trimmed rim has another incident face. Keep that harder neighbour-heal
88    // slice out of this exact full-torus path.
89    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    // offset_surface uses the shell convention positive=opposite outward, so
120    // negate the direct-edit distance. For an analytic torus the Greville fit
121    // reproduces the exact rational torus with unchanged parameterization.
122    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            // The two iso lanes are untouched: a periodic seam is an exact iso
167            // of the same-basis offset torus, so every solid this path already
168            // builds stays BIT-IDENTICAL.
169            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            // A general intrinsic trim used to be refused here. This path
177            // admits only edges with no OTHER incident face, so the boundary is
178            // definitional and its image on the offset torus IS the new edge.
179            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