Skip to main content

geop_ops_surface/
network.rs

1//! [`NetworkSurface`]: a face standing on its own through a network of
2//! curves — curves in one direction, `u`, crossing curves in the other,
3//! `v`, each `u` curve every `v` curve once — interpolating all of them
4//! (see [`NurbSurface3D::gordon`]).
5//!
6//! The curves are edges, of solids, sheets or wires — a 3-D sketch's
7//! lines, arcs and splines — or a planar sketch's curves, picked in any
8//! order and running either way. They are put in order along the other
9//! direction, by where they cross the first curve of it, and turned to run
10//! the way the curves they cross are ordered.
11//!
12//! Where two curves cross is found by the kernel's curve–curve intersection,
13//! refined by Newton (see [`refine_curve_curve_crossing`]). Two curves
14//! sharing an end — an edge's vertex, a sketch's point — are declared to
15//! meet there, and are taken to, without a search. A pair that does not
16//! cross, crosses more than once, or runs along each other is refused,
17//! naming both curves, and a pair that misses says by how much. So are two
18//! curves of one direction that meet within the network: they make no
19//! grid.
20//!
21//! The network's outer curves bound the face: each curve is cut to the
22//! stretch between its first and last crossing, so curves may run on past
23//! the network's border. Closed curves are refused, as are tangency
24//! conditions: the face meets the faces along its border as the curves
25//! make it.
26
27use geop_core_editor::{
28    edit_form::{EditForm, FormState},
29    editable::{Context, OperationGroup, OperationInfo, OperationTier},
30    part_operation,
31    part_operation::PartOperation,
32};
33use geop_core_geometry::{
34    intersection::{Intersections, curve_curve_intersect, refine_curve_curve_crossing},
35    nurb_curve::NurbCurve3D,
36    nurb_surface::NurbSurface3D,
37    search::{MAX_NODES, min_subdivision_size},
38};
39use geop_core_math::{
40    geop_error::{GeopError, GeopResult},
41    scalars::Scalar,
42};
43use geop_core_program::{part::StepContext, reader::Reader};
44use geop_core_topology::{
45    Sense,
46    build::{BodySpec, CoedgeOn, EdgeSpec, FaceSpec},
47    distance::{Feature, closest_points},
48};
49use geop_ops_model::sketch3d::CurveSource;
50use geop_ops_model::{BodyNames, Chain, ModelOperation, NamedModel, Namer, build_model};
51use serde::{Deserialize, Serialize};
52
53use crate::{NamedCurve, SheetSpec};
54
55/// Spans a face standing on its own through a network of curves, `u_curves`
56/// crossing `v_curves`, for the operation `N` — see the module docs. The
57/// curves are copied, the originals left as they are.
58///
59/// The face is named `network(N)`. The copy of each outer curve `X` along
60/// its border is `network(N,X)` — for a sketch's curve `X` is `K,c3` (see
61/// [`NamedCurve`]) — and the corner where the `u` curve `X`
62/// crosses the `v` curve `Y` is `network(N,X,Y)`.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
64pub struct NetworkSurface;
65
66#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
67pub struct NetworkSurfaceArgs {
68    /// The curves in one direction: two at least, in any order.
69    pub u_curves: Vec<CurveSource>,
70    /// The curves crossing them: two at least, in any order.
71    pub v_curves: Vec<CurveSource>,
72}
73
74part_operation!(NetworkSurface(NetworkSurfaceArgs));
75
76impl PartOperation for NetworkSurface {
77    const KIND: &'static str = "network_surface";
78    const INFO: OperationInfo = OperationInfo::new(
79        "UV surface",
80        "Span a face standing on its own through a network of curves — u curves crossing v curves — running along every one of them.",
81        OperationGroup::Surface,
82        OperationTier::Small,
83    )
84    .experimental()
85    .icon(crate::icons::NETWORK_SURFACE);
86    type Args = NetworkSurfaceArgs;
87    type Session = ();
88
89    /// Nothing picked yet.
90    fn new_args(_: &Context) -> NetworkSurfaceArgs {
91        NetworkSurfaceArgs::default()
92    }
93
94    fn run(ctx: &mut StepContext, args: &NetworkSurfaceArgs) -> GeopResult<()> {
95        let _step = ctx.step();
96        build_model::<Self>(ctx, args)
97    }
98
99    /// The curves of each direction, picked.
100    fn form<'a>(
101        context: &'a Context<'a>,
102        args: &NetworkSurfaceArgs,
103        _: &FormState<()>,
104    ) -> EditForm<'a, NetworkSurfaceArgs> {
105        let before = &context.before;
106        let mut f = EditForm::<NetworkSurfaceArgs>::default();
107        f.pick(
108            "u_curves",
109            "u curves",
110            before,
111            &args.u_curves,
112            true,
113            |edit, picked| edit.args.u_curves = picked,
114        );
115        f.pick(
116            "v_curves",
117            "v curves",
118            before,
119            &args.v_curves,
120            true,
121            |edit, picked| edit.args.v_curves = picked,
122        );
123        f
124    }
125}
126
127impl ModelOperation for NetworkSurface {
128    fn build(
129        model: &mut NamedModel,
130        reader: &Reader,
131        step: &str,
132        args: &NetworkSurfaceArgs,
133    ) -> GeopResult<()> {
134        let namer = Namer::new("network", step)?;
135        let resolve = |curves: &[CurveSource]| {
136            (curves.iter())
137                .map(|c| c.resolve(reader))
138                .collect::<GeopResult<Vec<_>>>()
139        };
140        let (u, v) = (resolve(&args.u_curves)?, resolve(&args.v_curves)?);
141        let (spec, names) = network_surface(&namer, &u, &v)?;
142        model.build_body(spec, names)?;
143        Ok(())
144    }
145}
146
147/// A curve of the network, as picked: its name, where it runs, and the
148/// names of its ends.
149#[derive(Clone, Debug)]
150struct Strand {
151    /// `u curve X` or `v curve X`, as errors name it.
152    label: String,
153    name: String,
154    curve: NurbCurve3D,
155    start: String,
156    end: String,
157}
158
159impl Strand {
160    fn of(chain: &Chain<NurbCurve3D>, direction: &str) -> GeopResult<Self> {
161        let named = NamedCurve::of(chain)?;
162        let label = format!("{direction} curve {}", named.name);
163        if named.start.0 == named.end.0 {
164            return Err(GeopError::new(format!(
165                "{label} is closed: a network surface spans curves with two ends"
166            )));
167        }
168        Ok(Self {
169            label,
170            name: named.name,
171            curve: named.curve,
172            start: named.start.0,
173            end: named.end.0,
174        })
175    }
176
177    /// The same curve run the other way, and where a parameter of it is
178    /// then.
179    fn turned(&self) -> (Self, impl Fn(Scalar) -> Scalar) {
180        let (t0, t1) = self.curve.domain();
181        let turned = Self {
182            curve: self.curve.reverse(),
183            start: self.end.clone(),
184            end: self.start.clone(),
185            ..self.clone()
186        };
187        (turned, move |t: Scalar| t0.add(t1).sub(t))
188    }
189}
190
191/// Where `a` crosses `b`: their parameters there. At an end the two share,
192/// that end; otherwise the one crossing the intersection search finds,
193/// refined — an error naming both if there is none, or more than one.
194fn crossing(a: &Strand, b: &Strand) -> GeopResult<(Scalar, Scalar)> {
195    let ends = |s: &Strand| {
196        let (t0, t1) = s.curve.domain();
197        [(s.start.clone(), t0), (s.end.clone(), t1)]
198    };
199    let shared: Vec<(Scalar, Scalar)> = ends(a)
200        .into_iter()
201        .flat_map(|(na, ta)| {
202            ends(b)
203                .into_iter()
204                .filter(move |(nb, _)| *nb == na)
205                .map(move |(_, tb)| (ta, tb))
206        })
207        .collect();
208    let (la, lb) = (&a.label, &b.label);
209    match shared.as_slice() {
210        [one] => return Ok(*one),
211        [] => {}
212        _ => {
213            return Err(GeopError::new(format!(
214                "{la} and {lb} meet at both their ends: each u curve must cross each v curve once"
215            )));
216        }
217    }
218    let found = curve_curve_intersect(&a.curve, &b.curve, 3, MAX_NODES, min_subdivision_size())?;
219    match found {
220        Intersections::Coincident(_) => Err(GeopError::new(format!(
221            "{la} and {lb} run along each other: each u curve must cross each v curve once"
222        ))),
223        Intersections::Found(found) => match found.as_slice() {
224            [] => Err(GeopError::new(format!(
225                "{la} and {lb} do not cross: they pass {:.3e} apart",
226                closest_points(
227                    &Feature::Curve(a.curve.clone()),
228                    &Feature::Curve(b.curve.clone())
229                )?
230                .distance
231                .to_f64()
232            ))),
233            [(ta, tb)] => Ok(refine_curve_curve_crossing(&a.curve, &b.curve, *ta, *tb)),
234            more => Err(GeopError::new(format!(
235                "{la} and {lb} cross {} times: each u curve must cross each v curve once",
236                more.len()
237            ))),
238        },
239    }
240}
241
242/// The order `strands` go in along a curve they all cross at `params`: by
243/// where they cross it. Which way round is a free choice.
244fn order(params: &[Scalar]) -> Vec<usize> {
245    let mut order: Vec<usize> = (0..params.len()).collect();
246    order.sort_by(|&i, &j| params[i].to_f64().total_cmp(&params[j].to_f64()));
247    order
248}
249
250/// Strands, and where each crosses the curves of the other direction.
251type Aligned = (Vec<Strand>, Vec<Vec<Scalar>>);
252
253/// `strands`, each crossing the curves of the other direction at
254/// `params[k]` in the order `across`, turned where those run backwards, and
255/// the crossings in that order — or an error naming a strand along which
256/// they do not run in that order, either way.
257fn aligned(
258    strands: Vec<Strand>,
259    params: Vec<Vec<Scalar>>,
260    across: &[usize],
261    first: &str,
262) -> GeopResult<Aligned> {
263    let mut out = (Vec::new(), Vec::new());
264    for (strand, params) in strands.into_iter().zip(params) {
265        let along: Vec<Scalar> = across.iter().map(|&i| params[i]).collect();
266        let rising = along.windows(2).all(|w| w[0].definitely_less(w[1]));
267        let falling = along.windows(2).all(|w| w[0].definitely_greater(w[1]));
268        let (strand, along) = if rising {
269            (strand, along)
270        } else if falling {
271            let (turned, at) = strand.turned();
272            (turned, along.into_iter().map(at).collect())
273        } else {
274            return Err(GeopError::new(format!(
275                "the curves crossing {} cross it in another order than they cross {first}: the curves must make a grid",
276                strand.label
277            )));
278        };
279        out.0.push(strand);
280        out.1.push(along);
281    }
282    Ok(out)
283}
284
285/// The sheet spanned through the network of `u_curves` and `v_curves`
286/// (see the module docs), each a curve picked: what to build, named after
287/// `namer`, as [`NetworkSurface`] says.
288pub fn network_surface(
289    namer: &Namer,
290    u_curves: &[Chain<NurbCurve3D>],
291    v_curves: &[Chain<NurbCurve3D>],
292) -> GeopResult<SheetSpec> {
293    if u_curves.len() < 2 || v_curves.len() < 2 {
294        return Err(GeopError::new(format!(
295            "a network surface needs two curves at least in each direction, not {} u curves and {} v curves",
296            u_curves.len(),
297            v_curves.len()
298        )));
299    }
300    let picked: Vec<&str> = (u_curves.iter().chain(v_curves))
301        .map(|c| c.name.as_str())
302        .collect();
303    if let Some((_, twice)) = picked
304        .iter()
305        .enumerate()
306        .find(|(k, e)| picked[..*k].contains(e))
307    {
308        return Err(GeopError::new(format!(
309            "{twice} is picked twice: each curve of a network is in one direction, once"
310        )));
311    }
312    let u = u_curves
313        .iter()
314        .map(|c| Strand::of(c, "u"))
315        .collect::<GeopResult<Vec<_>>>()?;
316    let v = v_curves
317        .iter()
318        .map(|c| Strand::of(c, "v"))
319        .collect::<GeopResult<Vec<_>>>()?;
320    let (m, n) = (u.len(), v.len());
321    // `on_u[j][i]` and `on_v[i][j]`: where `u[j]` and `v[i]` cross, on each.
322    let mut on_u = vec![Vec::with_capacity(n); m];
323    let mut on_v = vec![Vec::with_capacity(m); n];
324    for (j, uj) in u.iter().enumerate() {
325        for (i, vi) in v.iter().enumerate() {
326            let (tu, tv) = crossing(uj, vi)?;
327            on_u[j].push(tu);
328            on_v[i].push(tv);
329        }
330    }
331    // The `v` curves in order along the first `u` curve, the `u` curves
332    // along the first `v` curve; every curve turned to run that way.
333    let v_order = order(&on_u[0]);
334    let u_order = order(&on_v[0]);
335    let (u_label, v_label) = (u[0].label.clone(), v[0].label.clone());
336    let (u, on_u) = aligned(u, on_u, &v_order, &u_label)?;
337    let (v, on_v) = aligned(v, on_v, &u_order, &v_label)?;
338    let pick = |items: &[Strand], params: &[Vec<Scalar>], order: &[usize]| {
339        order
340            .iter()
341            .map(|&k| (items[k].clone(), params[k].clone()))
342            .unzip::<_, _, Vec<_>, Vec<_>>()
343    };
344    let (u, on_u): (Vec<Strand>, Vec<Vec<Scalar>>) = pick(&u, &on_u, &u_order);
345    let (v, on_v): (Vec<Strand>, Vec<Vec<Scalar>>) = pick(&v, &on_v, &v_order);
346
347    // Within the network, curves of one direction must not meet: two that
348    // cross each other between the curves they both cross make no grid,
349    // whichever way round they are taken.
350    let cut =
351        |s: &Strand, params: &[Scalar]| s.curve.sub_curve(params[0], params[params.len() - 1]);
352    for (strands, params) in [(&u, &on_u), (&v, &on_v)] {
353        let pieces = strands
354            .iter()
355            .zip(params.iter())
356            .map(|(s, p)| cut(s, p))
357            .collect::<GeopResult<Vec<_>>>()?;
358        for a in 0..pieces.len() {
359            for b in a + 1..pieces.len() {
360                let met = curve_curve_intersect(
361                    &pieces[a],
362                    &pieces[b],
363                    1,
364                    MAX_NODES,
365                    min_subdivision_size(),
366                )?;
367                if !met.as_slice().is_empty() {
368                    return Err(GeopError::new(format!(
369                        "{} and {} meet between the curves they cross: the curves of one direction must not meet, so that they make a grid with the others",
370                        strands[a].label, strands[b].label
371                    )));
372                }
373            }
374        }
375    }
376
377    let curves = |s: &[Strand]| s.iter().map(|s| s.curve.clone()).collect::<Vec<_>>();
378    let surface = NurbSurface3D::gordon(&curves(&u), &curves(&v), &on_u, &on_v)?;
379
380    // Corners `(u, v)` counter-clockwise from `(0, 0)`, as `(j, i)`: where
381    // the `j`-th `u` curve crosses the `i`-th `v` curve.
382    let corners = [(0, 0), (0, n - 1), (m - 1, n - 1), (m - 1, 0)];
383    let vertices = corners
384        .iter()
385        .map(|&(j, i)| {
386            let a = u[j].curve.evaluate(on_u[j][i])?;
387            let b = v[i].curve.evaluate(on_v[i][j])?;
388            Ok(a.union(&b))
389        })
390        .collect::<GeopResult<Vec<_>>>()?;
391    let edges = vec![
392        EdgeSpec {
393            curve: cut(&u[0], &on_u[0])?,
394            start: 0,
395            end: 1,
396        },
397        EdgeSpec {
398            curve: cut(&v[n - 1], &on_v[n - 1])?,
399            start: 1,
400            end: 2,
401        },
402        EdgeSpec {
403            curve: cut(&u[m - 1], &on_u[m - 1])?,
404            start: 3,
405            end: 2,
406        },
407        EdgeSpec {
408            curve: cut(&v[0], &on_v[0])?,
409            start: 0,
410            end: 3,
411        },
412    ];
413    let face = FaceSpec::on_unit_square(
414        surface,
415        [
416            CoedgeOn::Edge(0, Sense::Forward),
417            CoedgeOn::Edge(1, Sense::Forward),
418            CoedgeOn::Edge(2, Sense::Reversed),
419            CoedgeOn::Edge(3, Sense::Reversed),
420        ],
421    )?;
422    let names = BodyNames {
423        vertices: corners
424            .iter()
425            .map(|&(j, i)| namer.name(&[&u[j].name, &v[i].name]))
426            .collect(),
427        edges: [&u[0], &v[n - 1], &u[m - 1], &v[0]]
428            .iter()
429            .map(|s| namer.name(&[&s.name]))
430            .collect(),
431        faces: vec![namer.root()],
432        solid: None,
433    };
434    let spec = BodySpec {
435        vertices,
436        edges,
437        faces: vec![face],
438        shells: vec![vec![0]],
439        solid: false,
440    };
441    Ok((spec, names))
442}
443
444#[cfg(test)]
445mod tests;