Skip to main content

geop_core_topology/edit/
split_edge_at_vertex.rs

1/// Newton iterations for the `(u, v)` foot-point refinement. Like
2/// `NurbCurve::refine_parameter_at_point`'s own count, this only affects how
3/// tightly an already-isolated answer is pinned down.
4const NEWTON_ITERATIONS: usize = 20;
5
6use crate::{Coedge, CoedgeGeometry, Edge, EdgeId, Model, Sense, VertexId};
7use geop_core_geometry::contains::{curve::curve_could_contain, surface::surface_could_contain};
8use geop_core_math::{
9    geop_error::{GeopError, GeopResult, WithContext},
10    scalars::Scalar,
11    vector::Vector2,
12};
13
14impl<S: Scalar> Model<S> {
15    /// Splits `edge_id` at `edge_t` into two edges joined at the pre-existing
16    /// `vertex_id` (which must already coincide with `edge.curve.evaluate(edge_t)`),
17    /// and splits every coedge tracing `edge_id` to match. `edge_id` itself
18    /// keeps the first (start -> vertex) segment; the new edge holding the
19    /// second (vertex -> end) segment, in the same direction, is returned.
20    ///
21    /// `max_nodes`/`min_subdivision_size` bound the BFS search used to locate
22    /// each coedge's own pcurve parameter for the split (see
23    /// `curve_could_contain`).
24    pub fn split_edge_at_vertex(
25        &mut self,
26        edge_id: EdgeId,
27        edge_t: S,
28        vertex_id: VertexId,
29        max_nodes: usize,
30        min_subdivision_size: S,
31    ) -> GeopResult<EdgeId> {
32        let ctx = |e: GeopError| {
33            e.with_context(format!(
34                "Model::split_edge_at_vertex(edge_id={edge_id}, edge_t={edge_t}, vertex_id={vertex_id}, max_nodes={max_nodes}, min_subdivision_size={min_subdivision_size})"
35            ))
36        };
37
38        let edge = self.get_edge(edge_id).with_context(&ctx)?.clone();
39        let vertex_point = self.get_vertex(vertex_id).with_context(&ctx)?.point;
40
41        let curve_point = edge.curve.evaluate(edge_t).with_context(&ctx)?;
42        if !curve_point.could_be_equal(&vertex_point) {
43            return Err(ctx(GeopError::new(format!(
44                "edge {edge_id} at t={edge_t} evaluates to {curve_point}, which does not coincide with vertex {vertex_id} at {vertex_point}"
45            ))));
46        }
47
48        // A split parameter that isn't *definitely* strictly inside its own
49        // curve's domain could be that domain's bound — meaning the vertex is
50        // already an endpoint of that curve, so there is no interior split to
51        // make. That is the caller's job to rule out before asking (see
52        // `find_piercing_crossing`, which skips exactly these), so reaching
53        // here with one is a caller bug and is reported rather than quietly
54        // ignored: silently doing nothing would leave the caller believing an
55        // edge had been split when it had not.
56        //
57        // Checked for the 3-D curve *and* every coedge's pcurve before any
58        // mutation — splitting the edge and only then finding a pcurve that
59        // cannot be split would leave the model half-updated.
60        //
61        // Both parameters are Newton-refined against the point they are meant
62        // to name before anything else looks at them (see
63        // `NurbCurve::refine_parameter_at_point`). A subdivision search only
64        // pins a parameter down to its own tolerance, and `split` cannot take
65        // a parameter that wide — Boehm insertion amplifies it without bound.
66        // The refinement is what makes the parameter narrow enough to split
67        // at *and* still an honest enclosure, replacing the `sharpen` that
68        // used to stand here and silently moved the split to the interval's
69        // midpoint instead of the located point. Validation below therefore
70        // tests exactly the value the split will use.
71        let (curve_lo, curve_hi) = edge.curve.domain();
72        let edge_t = edge
73            .curve
74            .refine_parameter_at_point(edge_t, &vertex_point)
75            .with_context(&ctx)?;
76        if !edge_t.definitely_greater(curve_lo) || !edge_t.definitely_less(curve_hi) {
77            return Err(ctx(GeopError::new(format!(
78                "edge_t={edge_t:?} is not strictly inside the curve domain ({curve_lo:?}, {curve_hi:?}), so vertex {vertex_id} is an endpoint of edge {edge_id} rather than interior to it — there is nothing to split"
79            ))));
80        }
81        let mut pcurve_splits = Vec::new();
82        for coedge_id in self.coedges_of_edge(edge_id) {
83            let coedge = self.get_coedge(coedge_id).with_context(&ctx)?.clone();
84            let coedge_ctx = |e: GeopError| {
85                let (t0, t1) = coedge.pcurve.domain();
86                e.with_context(format!(
87                    "coedge_id={coedge_id}, face={}, sense={:?}, pcurve_domain=({t0}, {t1})",
88                    coedge.face, coedge.sense,
89                ))
90            };
91            let surface = &self.get_face(coedge.face).with_context(&ctx)?.surface;
92            // The pcurve's own parameter range doesn't share the edge curve's
93            // `t` values, so the split point is relocated geometrically:
94            // locate the vertex in the face's `(u, v)` space, then project
95            // that `(u, v)` back onto the coedge's own pcurve.
96            let (u, v) = surface_could_contain(
97                surface,
98                &vertex_point,
99                max_nodes,
100                min_subdivision_size,
101            )
102            .with_context(&ctx)
103            .with_context(&coedge_ctx)?
104            .ok_or_else(|| {
105                ctx(coedge_ctx(GeopError::new(format!(
106                    "could not locate vertex {vertex_id} (at {vertex_point}) on face {}'s surface",
107                    coedge.face
108                ))))
109            })?;
110            // `surface_could_contain` isolates the `(u, v)` to its own
111            // subdivision tolerance, which is far too wide to serve as the
112            // *target* of the pcurve refinement below: a wide target makes a
113            // wide residual, which makes a wide step, and the refined pcurve
114            // parameter comes back no narrower than it started. So refine the
115            // target first, by the same subdivide-to-isolate-then-Newton
116            // split of labour — `project` polishes the foot point and keeps
117            // its last iterate unsharpened, so the result is narrow and still
118            // honest. Intersected with the box the search proved it lies in,
119            // and falling back to that box if Newton left it.
120            let refined = surface
121                .project(vertex_point, u.sharpen(), v.sharpen(), NEWTON_ITERATIONS)
122                .with_context(&ctx)
123                .with_context(&coedge_ctx)?;
124            let (u, v) = if refined.0.could_be_equal(u) && refined.1.could_be_equal(v) {
125                (u.intersect(refined.0), v.intersect(refined.1))
126            } else {
127                (u, v)
128            };
129            let uv = Vector2::from_array([u, v]);
130            let uv_ctx = |e: GeopError| e.with_context(format!("found uv=({u}, {v})"));
131            let pcurve_t =
132                curve_could_contain(&coedge.pcurve, &uv, max_nodes, min_subdivision_size)
133                    .with_context(&ctx)
134                    .with_context(&coedge_ctx)
135                    .with_context(&uv_ctx)?
136                    .ok_or_else(|| {
137                        ctx(coedge_ctx(uv_ctx(GeopError::new(format!(
138                            "could not locate vertex {vertex_id} on coedge {coedge_id}'s pcurve"
139                        )))))
140                    })?;
141            let (pcurve_lo, pcurve_hi) = coedge.pcurve.domain();
142            let pcurve_t = coedge
143                .pcurve
144                .refine_parameter_at_point(pcurve_t, &uv)
145                .with_context(&ctx)
146                .with_context(&coedge_ctx)?;
147            if !pcurve_t.definitely_greater(pcurve_lo) || !pcurve_t.definitely_less(pcurve_hi) {
148                return Err(ctx(coedge_ctx(GeopError::new(format!(
149                    "pcurve_t={pcurve_t:?} is not strictly inside coedge {coedge_id}'s pcurve domain ({pcurve_lo:?}, {pcurve_hi:?}), so vertex {vertex_id} is an endpoint of that pcurve rather than interior to it — there is nothing to split"
150                )))));
151            }
152            pcurve_splits.push((coedge_id, pcurve_t));
153        }
154
155        let (new_curve_1, new_curve_2) = edge.curve.split(edge_t).with_context(&ctx)?;
156        // Reuse `edge_id` for the first (start -> vertex) segment, shortened
157        // in place, and only allocate a new edge for the second segment.
158        let existing_edge = self.get_edge_mut(edge_id).with_context(&ctx)?;
159        existing_edge.curve = new_curve_1;
160        existing_edge.end_vertex = vertex_id;
161        let edge_1 = edge_id;
162        let edge_2 = self.insert_edge(Edge {
163            curve: new_curve_2,
164            start_vertex: vertex_id,
165            end_vertex: edge.end_vertex,
166        });
167
168        for (coedge_id, pcurve_t) in pcurve_splits {
169            let coedge = self.get_coedge(coedge_id).with_context(&ctx)?.clone();
170
171            let coedge_ctx = |e: GeopError| {
172                let (t0, t1) = coedge.pcurve.domain();
173                e.with_context(format!(
174                    "coedge_id={coedge_id}, face={}, sense={:?}, pcurve_domain=({t0}, {t1})",
175                    coedge.face, coedge.sense,
176                ))
177            };
178
179            let (pcurve_left, pcurve_right) = coedge
180                .pcurve
181                .split(pcurve_t)
182                .with_context(&ctx)
183                .with_context(&coedge_ctx)?;
184            // `pcurve_left`/`pcurve_right` are in the coedge's own traversal
185            // order (its domain's low end is its own start), which for a
186            // `Reversed` coedge runs from `edge_2` back to `edge_1`.
187            let (first_edge, first_pcurve, second_edge, second_pcurve) = match coedge.sense {
188                Sense::Forward => (edge_1, pcurve_left, edge_2, pcurve_right),
189                Sense::Reversed => (edge_2, pcurve_left, edge_1, pcurve_right),
190            };
191
192            let new_coedge_id = self.insert_coedge(Coedge {
193                geometry: CoedgeGeometry::Edge(second_edge),
194                sense: coedge.sense,
195                pcurve: second_pcurve,
196                next: coedge.next,
197                prev: coedge_id,
198                face: coedge.face,
199            });
200
201            let existing = self.get_coedge_mut(coedge_id).with_context(&ctx)?;
202            existing.geometry = CoedgeGeometry::Edge(first_edge);
203            existing.pcurve = first_pcurve;
204            existing.next = new_coedge_id;
205
206            self.get_coedge_mut(coedge.next).with_context(&ctx)?.prev = new_coedge_id;
207        }
208
209        Ok(edge_2)
210    }
211}