Skip to main content

brep_kernel/edit/direct_edit/
face_offset_torus.rs

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