ogeom_algo/network.rs
1//! A network of B-spline patches fitted to a scan at once.
2//!
3//! The caller lays out the network: four-sided patches, each bounded by
4//! curves drawn on the scan (a polyline, or a curve), a curve between two
5//! patches shared by both. [`fit_network`] fits every patch to the points
6//! in one least-squares system, in which:
7//!
8//! - **a shared curve is one set of poles.** Both patches' boundary rows
9//! are the same unknowns, and the corners where curves meet are one
10//! unknown each, so the patches meet exactly, with no gap to report but
11//! rounding.
12//! - **a curve that is not a crease is held tangent, pole by pole.** Each
13//! pole of the curve and the poles beside it in the two patches are
14//! held on one line, at a ratio fixed along the curve:
15//! `B − L = λ (R − B)`. The cross derivatives of the two patches along
16//! the curve are then parallel everywhere, the same tangent plane on
17//! either side. The ratio is read off a fit without the constraints, as
18//! the ratio of the pole spacings either side, and is shared by every
19//! curve that runs straight on through a corner of four patches, which
20//! is what lets the constraints of the curves crossing there agree.
21//! - **a corner of other than four patches is held tangent where it
22//! can be.** The triples hold the parameter lines that cross a curve to
23//! one direction on both sides of it. At a corner of three patches (or
24//! five, or six, or the point a three-sided patch's missing side shrinks
25//! to) the lines crossing a curve there lean one way on one side and the
26//! other way on the other, and no triple holds; tangency there needs the
27//! cross derivatives to differ by a multiple of the curve's own tangent,
28//! `D₁ + λ D₂ + γ T = 0`. At the corner that puts the curves' first poles
29//! in one plane, at ratios read off the fit without the constraints. A
30//! corner of an odd number of patches whose curves all run on to the
31//! network's border is first held with `λ` constant and `γ` linear along
32//! each whole curve: a cubic spline on the curve's knots, nought where it
33//! is nought at as many points as it has coefficients, an exact linear
34//! condition though not a pole-by-pole one. It binds each curve to a
35//! single cubic where `γ` is not nought at its knots. Otherwise, or
36//! where that misses the points, a corner is held by its spans: the knot
37//! past each curve's span next to the corner is doubled, that span is a
38//! quadratic, and `γ` is a quadratic over it with a double root at its
39//! far end and nought past it. Past that span the curve is held by the
40//! pole triples at its corner's ratio, so its far end may be on the
41//! border, a corner of four patches (an edge's fillet attached to a
42//! rounded box corner) or a junction, and spans raised there shrink the
43//! quadratic span. A junction pins the curve to its patch's knot
44//! spacings; the line through the junction is then held at the corner's
45//! ratio instead, tangent but not `C¹`. A curve may also run on to
46//! another corner held by its spans, which pins it to a second ratio:
47//! the first poles round a corner may be scaled to give its curves other
48//! ratios, so both corners are scaled to the geometric mean of the two,
49//! moved as little as closes every cycle of a corner's scales all of
50//! whose curves are asked a ratio. Where neither way meets the points, a
51//! corner of an odd number of patches, or an even number above four,
52//! whose curves each run on to the border or to another such corner has
53//! its gluing spread along its curves: `λ` constant and `γ` linear along
54//! each whole curve, from the corner's `g` to a value read off the fit
55//! at the border or to the other corner's, every knot inside the curve's
56//! strip doubled and the curves along it held `C²` across each. The
57//! curve is then as free to refine as any, its patches only `C¹` across
58//! those knots. At an even corner the twists round it close in a cycle
59//! that is always singular, which leaves one condition on the curves'
60//! second poles, held with the rest. This is the gluing
61//! a Coons patch's parameters carry, `γ` falling along the whole curve,
62//! which the points at their seed's parameters admit where `γ` nought
63//! past one span does not. So a corner of three patches or more,
64//! other than four, whose curves each run on to the border, a corner of
65//! four patches, a junction or another such corner is held, every curve
66//! into it tangent to rounding, as long as the fit still comes within
67//! the tolerance: the one ratio along each curve these conditions allow
68//! is a restriction the points at their seed's parameters need not
69//! admit. Where no way of holding it does, and at any other corner of
70//! other than four patches (an apex, or a junction), a curve with such a
71//! corner at an end is fitted without the triples: near tangent as the
72//! points are smooth, its angle measured into [`Network::worst_tangent`].
73//! Every such corner's widest angle between the patches' normals is
74//! reported in [`Network::corners`].
75//! - **a fan is held tangent where it can be.** At a point every patch
76//! there shrinks its missing side to, both patches beside each curve
77//! into it shrink, so the parameter lines crossing the curve turn
78//! neither way and its pole triples hold it from end to end. Each
79//! patch's normal at the point is the limit `S_d × S_dw`, `d` across
80//! its missing side and `w` along it: from the point to the row of poles
81//! one in from that side, crossed with the row's own derivative. The
82//! patches share one tangent plane there where every patch's such row
83//! lies in one plane through the point; the rows are held to the layout
84//! they take in the fit without the constraints, projected on a plane
85//! through the point, its orientation, scale and shear left free. A fit
86//! holding a fan is kept only where every patch's normal at the point is
87//! defined all along its missing side and points one way, which the
88//! Bernstein coefficients of `(R − V) × R'` bound, `R` the row and `V`
89//! the point. Each way of holding the corners is first tried with the
90//! fans held, and only where none of them comes within the tolerance are
91//! the fans left near tangent. A point where a three-sided patch meets
92//! four-sided ones is left near tangent, its angle measured into
93//! [`Network::worst_tangent`]: exact tangency is not offered there. The
94//! three-sided patch's cross derivative vanishes at the point and the
95//! other's does not, so no ratio fixed along the curve serves, and the
96//! gluing that does, `a D₃ + b D₄ + c T = 0` with polynomial `a`, `b`,
97//! `c`, is not the one the seed's parameters give the points: fitted to
98//! the fit without the constraints, the best such gluing misses its cross
99//! derivatives by 2 to 4.5 per cent, however fine the spans. Held to it,
100//! the patches shear off the points' parameters and stand further off
101//! the shape than the fit near tangent does (on a ball's cap of four
102//! wedges, two three-sided, clean at 0.001: near tangent at 1.2e-3 rad,
103//! 2.3e-4 off the ball).
104//! - **a side may be a chain of curves.** A curve may end on the middle of
105//! a patch's side (a T-junction), the side then a chain of curves meeting
106//! there ([`ChainedPatch`]). The patch takes a knot of multiplicity three
107//! at the junction, so its boundary row is the chain's poles, shared
108//! exactly, and the side opposite crosses the same junction: it is
109//! split alike, or is one curve whose knots carry the junction too. The
110//! patch is held `C¹` across the parameter line between the two, pole by
111//! pole, at the ratio of the knot spacings either side, or tangent at a
112//! corner's ratio where a curve from a corner held by its spans ends
113//! there. That line runs straight on from the curve ending at the
114//! junction, so that curve is held tangent at that same ratio, and the
115//! chain's curves either side of the junction share theirs, as at a
116//! corner of four patches.
117//! - **spans are raised per strip.** The curves opposite each other in a
118//! patch share knots, and so do all the curves of a strip of patches
119//! joined through their opposite sides. Where a patch misses the points
120//! by more than the tolerance its strips gain spans, the way
121//! [`fit_surface_scattered_over`](ogeom_geom::fit::fit_surface_scattered_over)
122//! refines a single patch.
123//! - **points may be reparameterised.** Once refinement ends,
124//! [`fit_network_chained`] can move every point's parameters to its
125//! nearest point on its patch and fit the patches again, a fit kept only
126//! where it comes nearer the points. Where every way of holding the
127//! corners misses, and so does the fit near tangent, each held fit that
128//! missed is finished that way from its spans before the near-tangent
129//! one is kept: a point whose nearest point lies on a side its patch
130//! shares is taken by the patch across that side where that one is
131//! nearer, as the seeds take each point by the nearest. A step is kept
132//! only while it holds the corners, takes off more of the squared
133//! distances than twice the noise's variance, read once off the fit at
134//! the seed's parameters, and leaves neither the worst miss that is not
135//! a stray nor the worst of all no worse; after each, the fit is refined
136//! on with that variance held, every patch a point misses, strays and
137//! all, tried at more spans, for at most eight steps. The first that
138//! comes within the tolerance is the network.
139//!
140//! The constraints are solved for exactly, by elimination, before the
141//! least squares: what is held tangent is tangent to rounding.
142
143mod build;
144mod solve;
145mod topology;
146
147use std::collections::BTreeMap;
148
149use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
150use ogeom_geom::fit::Ended;
151use ogeom_geom::{BSplineSurface, Curve, SurfaceGeometry, SurfaceSeeds, project_on_surface_from};
152use ogeom_math::{ControlGrid, KnotVector, Point, Vector};
153use ogeom_mesh::PointIndex;
154use ogeom_topo::{Model, Shape};
155
156use solve::{Basis, Lower, null_space, null_space_known, solve_spd};
157use topology::Topology;
158
159/// The degree of every patch and curve: the least that is curvature
160/// continuous within a patch and carries the thin-plate fairing.
161const DEGREE: usize = 3;
162
163/// The upper quartile of the standard normal distribution, `Φ⁻¹(3/4)`: the
164/// median of the absolute value of a normal deviate, in deviations.
165const NORMAL_QUARTILE: f64 = 0.674_489_750_196_081_7;
166
167/// How many points a worker projects at a time.
168const POINT_BATCH: usize = 2048;
169
170/// The most steps a held fit that missed is finished by. Re-taking the
171/// points fixes the few a seed took by the wrong patch within a step or
172/// two; past that, each step is a correction converging linearly, which
173/// creeps the points under the tolerance while the patches' corners and
174/// free borders, which no point beyond them holds, drift off the shape.
175const FINISH_STEPS: usize = 8;
176
177/// One side of a patch: the curve along it and which way it runs.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct PatchSide {
180 /// The curve's index in the network's curves.
181 pub curve: usize,
182 /// Whether the curve runs against the patch's walk round its sides.
183 pub reversed: bool,
184}
185
186/// A patch of the network: its four sides, walked round it.
187///
188/// Side 0 runs along `u` at `v = 0`, side 1 up `v` at `u = 1`, side 2 back
189/// along `u` at `v = 1`, side 3 back down `v` at `u = 0`, so the walk turns
190/// counter-clockwise seen from the side the patch's normal `S_u × S_v`
191/// faces. Two patches sharing a curve walk it in opposite directions, as
192/// the faces of a shell do. One side may be missing: a three-sided patch,
193/// that side shrunk to the point where its neighbours meet.
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct PatchDef {
196 /// The sides, in walking order.
197 pub sides: [Option<PatchSide>; 4],
198}
199
200impl PatchDef {
201 /// A four-sided patch from its curves in walking order, each with
202 /// whether it runs against the walk.
203 #[must_use]
204 pub fn four(sides: [(usize, bool); 4]) -> Self {
205 Self {
206 sides: sides.map(|(curve, reversed)| Some(PatchSide { curve, reversed })),
207 }
208 }
209}
210
211/// A patch of the network whose sides may each be a chain of curves.
212///
213/// The sides are walked as a [`PatchDef`]'s, each side's curves in walking
214/// order, each meeting the next at a junction, where a curve of a
215/// neighbouring patch may end (a T-junction). An empty side is missing, as
216/// a [`PatchDef`]'s `None`.
217///
218/// A side split into several curves has its patch cross the same
219/// junctions from the side opposite: that side is split too, or is one
220/// curve, which then carries the junctions to the patch beyond it. A curve
221/// so carrying junctions may itself be one of the curves of a split side
222/// (nested junctions): every side then crosses as many strips as the
223/// curves along it carry between them, and the side opposite as many, the
224/// `i`-th along one direction opposite the `i`-th along it on the other.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct ChainedPatch {
227 /// The sides, in walking order, each its curves in walking order.
228 pub sides: [Vec<PatchSide>; 4],
229}
230
231impl ChainedPatch {
232 /// A four-sided patch from its sides' curves in walking order, each
233 /// with whether it runs against the walk.
234 #[must_use]
235 pub fn four(sides: [Vec<(usize, bool)>; 4]) -> Self {
236 Self {
237 sides: sides.map(|side| {
238 side.into_iter()
239 .map(|(curve, reversed)| PatchSide { curve, reversed })
240 .collect()
241 }),
242 }
243 }
244}
245
246impl From<PatchDef> for ChainedPatch {
247 fn from(patch: PatchDef) -> Self {
248 Self {
249 sides: patch.sides.map(|side| side.into_iter().collect()),
250 }
251 }
252}
253
254/// A curve of the network, as the caller draws it on the scan.
255#[derive(Debug, Clone, PartialEq)]
256pub enum CurveDef {
257 /// Points along the curve, in order, the first and last at its ends.
258 Polyline(Vec<Point>),
259 /// A curve over a range of its parameter, read within the tolerance.
260 Curve {
261 /// The curve.
262 curve: Curve,
263 /// The range it is used over.
264 range: (f64, f64),
265 },
266}
267
268/// How [`fit_network`] fits.
269#[derive(Debug, Clone, PartialEq)]
270pub struct NetworkOptions {
271 /// The distance every point is to come within; also how far the
272 /// curves meeting at a corner may stand apart there, and the chord a
273 /// [`CurveDef::Curve`] is read to.
274 pub tolerance: f64,
275 /// The thin-plate fairing weight of each patch, over the unit square
276 /// its parameters run over, against the mean squared distance of its
277 /// points, as
278 /// [`ScatteredOptions::fairing`](ogeom_geom::fit::ScatteredOptions::fairing)
279 /// weighs a single patch. It settles the poles no point reaches: a
280 /// patch with no points needs it above nought.
281 pub smoothing: f64,
282 /// The curves left creased: their patches share the curve but not a
283 /// tangent plane across it.
284 pub creases: Vec<usize>,
285 /// The knot spans every strip starts with.
286 pub spans: usize,
287 /// The most knot spans a strip may reach.
288 pub most: usize,
289 /// The curves held to the caller's own: each one's poles are the
290 /// least-squares projection of the caller's curve on the curve's knots
291 /// at that round, its ends at the corners, and the fit moves them no
292 /// further. A [`CurveDef::Curve`] is read at its own parameter scaled
293 /// over the unit interval, so a curve the knots can carry (a cubic, a
294 /// spline on fewer knots) is the held curve exactly; a polyline is read
295 /// at its chord parameters.
296 pub held: Vec<usize>,
297 /// Held curves on the network's border whose patch leaves them as a
298 /// support asks: the poles one row in from each such curve are moved,
299 /// at every round, to the nearest point that steps off each pole of the
300 /// curve as [`Across`] says, and held there. The patch then meets the
301 /// support tangent along the curve exactly, up to the held curve's own
302 /// projection.
303 pub across: Vec<(usize, Across)>,
304}
305
306/// How a patch leaves a held border curve: the direction every step from a
307/// pole of the curve to the pole one row in may take.
308#[derive(Debug, Clone, Copy, PartialEq)]
309pub enum Across {
310 /// Within the plane square to this normal: a planar face the patch
311 /// meets tangent along the curve.
312 Plane(Vector),
313 /// Along this direction: a ruled support, a cylinder say, whose rulings
314 /// run this way across the curve.
315 Along(Vector),
316}
317
318/// How one patch fits its points.
319#[derive(Debug, Clone, Copy, PartialEq)]
320pub struct PatchFit {
321 /// The root mean square distance from its points to it; nought with
322 /// no points.
323 pub sigma: f64,
324 /// The farthest of its points from it.
325 pub worst: f64,
326 /// How many points it fits.
327 pub points: usize,
328 /// Its knot spans, `u` then `v`.
329 pub spans: (usize, usize),
330}
331
332/// A corner inside the network where other than four patches meet, every
333/// curve at it smooth (a patch whose side runs straight through it counts
334/// twice): held tangent to rounding, or near tangent, as the [module
335/// documentation](self) describes.
336#[derive(Debug, Clone, Copy, PartialEq)]
337pub struct IrregularCorner {
338 /// Where it is.
339 pub point: Point,
340 /// How many patches meet there.
341 pub valence: usize,
342 /// The widest angle between two of their normals there, in radians.
343 pub angle: f64,
344}
345
346/// What [`fit_network`] produced.
347#[derive(Debug, Clone)]
348pub struct Network {
349 /// A face per patch, in the patches' order, sharing an edge per shared
350 /// curve.
351 pub faces: Vec<Shape>,
352 /// The faces as one shell.
353 pub shell: Shape,
354 /// An edge per curve, in the curves' order.
355 pub edges: Vec<Shape>,
356 /// Each patch's surface, over the unit square.
357 pub surfaces: Vec<BSplineSurface>,
358 /// The widest angle, in radians, between the two patches' normals
359 /// across a curve between two patches that is not a crease, read at
360 /// both ends and the Gauss nodes of every span (an end at a missing
361 /// side's point left out, where a normal is not defined), and at the
362 /// irregular corners.
363 pub worst_tangent: f64,
364 /// The widest distance between a curve's edge and the boundary of a
365 /// patch it bounds, read at the same nodes.
366 pub worst_gap: f64,
367 /// How each patch fits its points.
368 pub per_patch: Vec<PatchFit>,
369 /// The corners inside the network where other than four patches meet,
370 /// no curve there a crease, and their angles.
371 pub corners: Vec<IrregularCorner>,
372 /// How many points lie past the network's border, nearest a side no
373 /// other patch shares, and were left out.
374 pub outside: usize,
375 /// Why refinement stopped; never [`Ended::Fixed`].
376 pub ended: Ended,
377}
378
379/// Fit a network of patches to a scan's points, all at once.
380///
381/// Each patch is first seeded: every curve is drawn as a B-spline with its
382/// poles on the caller's curve (at the Greville abscissae of its chord
383/// parameters), its spans doubled from [`NetworkOptions::spans`] until it
384/// passes within the tolerance of the caller's points or reaches
385/// [`NetworkOptions::most`], and each patch is the Coons patch of its four
386/// seeds. Each point is taken by the seed nearest it, at the parameters
387/// of its nearest point there; a point whose nearest point lies on a side
388/// no other patch shares lies past the network and is left out. The points
389/// are those of the region the network covers: a point of the scan beyond
390/// it is fitted by the nearest patch unless it lies past a free side.
391///
392/// The patches are then fitted as the [module documentation](self)
393/// describes, each weighing the mean squared distance of its points plus
394/// [`NetworkOptions::smoothing`] times its thin-plate energy, and every
395/// distance reported is from a point to its nearest point on its patch.
396/// While a point misses by more than the tolerance, each strip of a patch
397/// that misses is tried at twice its spans, and the try Mallows' criterion
398/// scores best is kept if it scores below the fit so far, as
399/// [`fit_surface_scattered_over`](ogeom_geom::fit::fit_surface_scattered_over)
400/// refines, the controls counted after the constraints. Every such strip is
401/// tried, not the worst patch's alone: a patch held tangent to a neighbour
402/// too coarse for the shape misses for its neighbour. A patch misses only
403/// in a cell holding as many points as the sixteen poles that bear on it,
404/// and a point farther off than the noise of its cell reaches is a stray
405/// rather than a miss. Where corners of other than four patches can be
406/// held tangent, the network is refined from the start with each way of
407/// holding them the [module documentation](self) describes in turn, until
408/// one comes within the tolerance; failing that, with those corners near
409/// tangent. A point where a three-sided patch meets four-sided ones is
410/// always left near tangent, its angle measured into
411/// [`Network::worst_tangent`]: exact tangency is not offered there, for the
412/// reason the [module documentation](self) gives. Where that misses too, each held way is finished from its
413/// spans, its points re-taken at their nearest points on it and the fit
414/// refined on, as the module documentation describes, and the first to
415/// come within the tolerance is kept. The points a patch fits ([`PatchFit::points`]) are then those it
416/// takes as finished.
417///
418/// The faces are built in `model` on the fitted patches, every curve an
419/// edge on the poles the patches share, with the parameter lines of the
420/// patches as its pcurves, and sewn by those shared edges into one shell.
421///
422/// # Errors
423///
424/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
425/// the tolerance is not a distance, the smoothing is not finite and
426/// non-negative, the spans are nought or above their bound, there is no
427/// patch, a patch has more than one side missing, names a curve that does
428/// not exist or is bounded twice by one curve or meets itself at a
429/// corner, a curve bounds no patch or more than two, two patches walk a
430/// shared curve the same way, a curve closes on itself, a crease names no
431/// curve, a curve has fewer than two points or a point that is not
432/// finite, or curves meeting at a corner stand farther apart there than
433/// the tolerance; [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric)
434/// if the points and the fairing cannot settle every pole (raise the
435/// smoothing); as [`project_on_surface`](ogeom_geom::project_on_surface)
436/// and the face builders otherwise.
437pub fn fit_network(
438 model: &mut Model,
439 patches: &[PatchDef],
440 curves: &[CurveDef],
441 points: &PointIndex,
442 options: &NetworkOptions,
443 tol: Tolerances,
444) -> OgeomResult<Network> {
445 let chained: Vec<ChainedPatch> = patches.iter().copied().map(ChainedPatch::from).collect();
446 fit_network_chained(model, &chained, curves, points, options, 0, tol)
447}
448
449/// [`fit_network`] over patches whose sides may be chains of curves, a
450/// curve ending on the middle of another's side at a T-junction, with
451/// parameter correction.
452///
453/// Every curve of a chained side is shared pole for pole as a whole side
454/// is, and a T-junction is held tangent as the [module
455/// documentation](self) describes; a curve that runs between two
456/// junctions whose ratios differ is left near tangent, its angle measured
457/// into [`Network::worst_tangent`].
458///
459/// Once refinement ends, every point's parameters move to its nearest
460/// point on its patch and the patches are fitted again at the same spans,
461/// up to `corrections` times. A fit is kept only where its root mean
462/// square distance to the points is below the one before, and, where every
463/// point was within the tolerance, every point stays within it; the first
464/// that is not ends the corrections. [`fit_network`] corrects none. A
465/// correction fits the points, not the shape they sample: where the points
466/// leave a pole unsettled (the point a three-sided patch shrinks its
467/// missing side to, which only the points round it place), it can move that
468/// pole as freely as the seed's parameters did.
469///
470/// # Errors
471///
472/// As [`fit_network`], and
473/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if two
474/// opposite sides of a patch, one of them split, cross different numbers
475/// of strips, the split sides carry their junctions round a loop back to
476/// themselves, or the curves of a split side meet one another, or the
477/// patch's corners, other than at their junctions.
478pub fn fit_network_chained(
479 model: &mut Model,
480 patches: &[ChainedPatch],
481 curves: &[CurveDef],
482 points: &PointIndex,
483 options: &NetworkOptions,
484 corrections: usize,
485 tol: Tolerances,
486) -> OgeomResult<Network> {
487 let tolerance = options.tolerance;
488 if !(tolerance > 0.0 && tolerance.is_finite()) {
489 ogeom_bail!(Construction, "a tolerance of {tolerance} is not a distance");
490 }
491 if !(options.smoothing.is_finite() && options.smoothing >= 0.0) {
492 ogeom_bail!(
493 Construction,
494 "a smoothing weight of {} is not a weight",
495 options.smoothing
496 );
497 }
498 if options.spans == 0 || options.most < options.spans {
499 ogeom_bail!(
500 Construction,
501 "{} starting spans within {} is not a span budget",
502 options.spans,
503 options.most
504 );
505 }
506 let topo = Topology::of(patches, curves.len(), &options.creases)?;
507 let mut drawn = drawn_curves(curves, tolerance, tol)?;
508 let mut held: Vec<Option<HeldCurve<'_>>> = vec![None; curves.len()];
509 for &c in &options.held {
510 let Some(def) = curves.get(c) else {
511 ogeom_bail!(Construction, "held curve {c} is not a curve of the network");
512 };
513 held[c] = Some(match def {
514 CurveDef::Curve { curve, range } => HeldCurve::Curve(curve, *range),
515 CurveDef::Polyline(points) => {
516 let mut at = vec![0.0];
517 for w in points.windows(2) {
518 at.push(at[at.len() - 1] + w[0].distance(w[1]));
519 }
520 let total = at[at.len() - 1];
521 let at = at
522 .iter()
523 .map(|t| if total > 0.0 { t / total } else { 0.0 })
524 .collect();
525 HeldCurve::Polyline(points, at)
526 }
527 });
528 }
529 let mut across: Vec<Option<Across>> = vec![None; curves.len()];
530 for &(c, how) in &options.across {
531 if held.get(c).is_none_or(Option::is_none) {
532 ogeom_bail!(
533 Construction,
534 "curve {c} is held across but not held; its poles must stand still for \
535 the step across them to be held"
536 );
537 }
538 let bounding = patches
539 .iter()
540 .flat_map(|patch| patch.sides.iter().flatten())
541 .filter(|side| side.curve == c)
542 .count();
543 if bounding != 1 {
544 ogeom_bail!(
545 Construction,
546 "curve {c} is held across but bounds {bounding} patches; only a border \
547 curve has a side to be held across"
548 );
549 }
550 let (Across::Plane(v) | Across::Along(v)) = how;
551 if !(v.magnitude() > tol.confusion()
552 && v.x.is_finite()
553 && v.y.is_finite()
554 && v.z.is_finite())
555 {
556 ogeom_bail!(
557 Construction,
558 "curve {c} is held across a direction of no length"
559 );
560 }
561 across[c] = Some(how);
562 }
563 // A held curve is seeded at the parameter it is held at, so the points
564 // take their parameters along it as the held poles place them: a
565 // curve's own, drawn at as many even steps as it has points.
566 let mut params: Vec<Option<Vec<f64>>> = vec![None; curves.len()];
567 for (c, held) in held.iter().enumerate() {
568 if let Some(curve @ HeldCurve::Curve(..)) = held {
569 let n = drawn[c].len().max(2) - 1;
570 let at: Vec<f64> = (0..=n).map(|i| precise(i) / precise(n)).collect();
571 drawn[c] = at
572 .iter()
573 .map(|&t| curve.at(t, tol))
574 .collect::<OgeomResult<_>>()?;
575 params[c] = Some(at);
576 }
577 }
578 let corners = corner_points(&topo, &drawn, tolerance)?;
579 let weights = strip_weights(&topo, &drawn);
580 let seeds = Seeds::new(
581 &topo, patches, &drawn, &corners, &weights, options, ¶ms, tol,
582 )?;
583 let cloud = points.points();
584 let taken = seeds.take(&topo, patches, &cloud, tol)?;
585
586 let mut fit = Fit {
587 topo: &topo,
588 patches,
589 taken: &taken,
590 weights: &weights,
591 smoothing: options.smoothing,
592 corners: Corners::Loose,
593 fans: false,
594 held: &held,
595 across: &across,
596 vertex_points: &corners,
597 tol,
598 };
599 // A corner held exactly tangent binds its curves' cross derivatives to
600 // a relation with one ratio along each, which the points at the seed's
601 // parameters need not admit within the tolerance. Each way of holding
602 // the corners is refined in turn until one comes within it, the corners
603 // left near tangent last. Holding a corner by its spans holds every
604 // corner held whole and more besides; where it holds no more, the
605 // corners are held whole first.
606 let mut tries = Vec::with_capacity(4);
607 let whole = !topo.odd_corners.is_empty();
608 let spanned = !topo.span_corners.is_empty();
609 let more = topo
610 .span_corners
611 .iter()
612 .any(|(v, _)| !topo.odd_corners.iter().any(|(w, _)| w == v));
613 if spanned && more {
614 tries.push(Corners::Spans);
615 }
616 if whole {
617 tries.push(Corners::Whole);
618 }
619 if spanned && !more {
620 tries.push(Corners::Spans);
621 }
622 if !topo.spread_corners.is_empty() {
623 tries.push(Corners::Spread);
624 }
625 tries.push(Corners::Loose);
626 // Where there are fans, each way is first tried with them held too,
627 // the near-tangent corners' way only kept where it comes within the
628 // tolerance with them held.
629 let mut tries: Vec<(Corners, bool)> = tries.into_iter().map(|c| (c, false)).collect();
630 if !topo.fans.is_empty() {
631 let fanned: Vec<(Corners, bool)> = tries.iter().map(|&(c, _)| (c, true)).collect();
632 tries.splice(0..0, fanned);
633 }
634 let mut settled = None;
635 let mut missed = Vec::new();
636 for (corners, fans) in tries {
637 fit.corners = corners;
638 fit.fans = fans;
639 let (spans, current, ended) = fit.refine(options)?;
640 if (ended == Ended::Met && current.held) || (corners == Corners::Loose && !fans) {
641 settled = Some((spans, current, ended));
642 break;
643 }
644 if current.held {
645 missed.push((corners, fans, spans, current));
646 }
647 }
648 let Some((mut spans, mut current, mut ended)) = settled else {
649 ogeom_bail!(Numeric, "no way of holding the corners was tried");
650 };
651 // Where the corners near tangent miss too, each held try that missed
652 // is finished at its spans, in the order they were tried: the points
653 // re-taken at their nearest points on the fit.
654 let mut retaken = None;
655 if ended != Ended::Met {
656 for (corners, fans, at, round) in missed {
657 let held = Fit {
658 corners,
659 fans,
660 ..fit
661 };
662 if let Some((at, round, again)) = held.finish(options, &at, round)? {
663 fit.corners = corners;
664 fit.fans = fans;
665 (spans, current, ended) = (at, round, Ended::Met);
666 retaken = Some(again);
667 break;
668 }
669 }
670 }
671 let fit = Fit {
672 taken: retaken.as_ref().unwrap_or(&taken),
673 ..fit
674 };
675 // Parameter correction, at the spans refinement settled on. Refinement
676 // fits every try at the seed's parameters, so that tries compare as
677 // fits of the same points at the same parameters rather than as
678 // corrections that have run further or less far.
679 for _ in 0..corrections {
680 let next = fit.round(&spans, ¤t.feet)?;
681 let kept_within = ended != Ended::Met || next.distances.iter().all(|d| *d <= tolerance);
682 if next.square() < current.square() && kept_within && next.held {
683 current = next;
684 } else {
685 break;
686 }
687 }
688
689 let built = build::faces(model, &topo, patches, ¤t, tol)?;
690 let measured = build::measure(&topo, patches, ¤t, tol)?;
691 let mut per_patch = vec![
692 PatchFit {
693 sigma: 0.0,
694 worst: 0.0,
695 points: 0,
696 spans: (0, 0),
697 };
698 patches.len()
699 ];
700 for (p, fit) in per_patch.iter_mut().enumerate() {
701 let [su, sv] = &topo.patch_strips[p];
702 fit.spans = (
703 su.iter().map(|&s| spans[s]).sum(),
704 sv.iter().map(|&s| spans[s]).sum(),
705 );
706 }
707 for (&p, &d) in fit.taken.patch.iter().zip(¤t.distances) {
708 let fit = &mut per_patch[p];
709 fit.points += 1;
710 fit.sigma += d * d;
711 fit.worst = fit.worst.max(d);
712 }
713 for fit in &mut per_patch {
714 if fit.points > 0 {
715 fit.sigma = (fit.sigma / precise(fit.points)).sqrt();
716 }
717 }
718 Ok(Network {
719 faces: built.faces,
720 shell: built.shell,
721 edges: built.edges,
722 surfaces: current.surfaces,
723 worst_tangent: measured.worst_tangent,
724 worst_gap: measured.worst_gap,
725 per_patch,
726 corners: measured.corners,
727 outside: taken.outside,
728 ended,
729 })
730}
731
732/// A count as a float.
733#[allow(clippy::cast_precision_loss, reason = "counts far below 2^52")]
734fn precise(n: usize) -> f64 {
735 n as f64
736}
737
738/// The median of non-empty `values`: the mean of the middle two of an even
739/// count.
740fn median(values: &[f64]) -> f64 {
741 let mut sorted = values.to_vec();
742 sorted.sort_by(f64::total_cmp);
743 let k = sorted.len() / 2;
744 if sorted.len().is_multiple_of(2) {
745 f64::midpoint(sorted[k - 1], sorted[k])
746 } else {
747 sorted[k]
748 }
749}
750
751/// Each curve as points along it.
752fn drawn_curves(
753 curves: &[CurveDef],
754 tolerance: f64,
755 tol: Tolerances,
756) -> OgeomResult<Vec<Vec<Point>>> {
757 let mut out = Vec::with_capacity(curves.len());
758 for (c, def) in curves.iter().enumerate() {
759 let points = match def {
760 CurveDef::Polyline(points) => points.clone(),
761 CurveDef::Curve { curve, range } => {
762 let deflection = ogeom_mesh::Deflection {
763 chord: tolerance,
764 ..ogeom_mesh::Deflection::default()
765 };
766 ogeom_mesh::discretize(curve, *range, deflection, tol)?.points
767 }
768 };
769 if points.len() < 2 {
770 ogeom_bail!(Construction, "curve {c} has fewer than two points");
771 }
772 if let Some(p) = points
773 .iter()
774 .find(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()))
775 {
776 ogeom_bail!(Construction, "curve {c}'s point {p:?} is not finite");
777 }
778 out.push(points);
779 }
780 Ok(out)
781}
782
783/// `A x = b` for a small dense `A` and three right-hand sides, by Gaussian
784/// elimination with partial pivoting; `None` where a pivot vanishes.
785fn solve_dense(mut a: Vec<Vec<f64>>, mut b: Vec<[f64; 3]>) -> Option<Vec<[f64; 3]>> {
786 let n = a.len();
787 for col in 0..n {
788 let pivot = (col..n).max_by(|&i, &j| a[i][col].abs().total_cmp(&a[j][col].abs()))?;
789 if a[pivot][col] == 0.0 || !a[pivot][col].is_finite() {
790 return None;
791 }
792 a.swap(col, pivot);
793 b.swap(col, pivot);
794 let (head, tail) = a.split_at_mut(col + 1);
795 let (b_head, b_tail) = b.split_at_mut(col + 1);
796 let (pivot_row, pivot_b) = (&head[col], b_head[col]);
797 for (row, rhs) in tail.iter_mut().zip(b_tail.iter_mut()) {
798 let f = row[col] / pivot_row[col];
799 if f == 0.0 {
800 continue;
801 }
802 for (entry, p) in row[col..].iter_mut().zip(&pivot_row[col..]) {
803 *entry -= f * p;
804 }
805 for (entry, p) in rhs.iter_mut().zip(pivot_b) {
806 *entry -= f * p;
807 }
808 }
809 }
810 let mut x = vec![[0.0; 3]; n];
811 for row in (0..n).rev() {
812 let mut sum = b[row];
813 for (k, coefficient) in a[row].iter().enumerate().skip(row + 1) {
814 for (s, known) in sum.iter_mut().zip(x[k]) {
815 *s -= coefficient * known;
816 }
817 }
818 x[row] = sum.map(|s| s / a[row][row]);
819 }
820 Some(x)
821}
822
823/// Each vertex: the mean of the curve ends that meet there, all within the
824/// tolerance of it.
825fn corner_points(topo: &Topology, drawn: &[Vec<Point>], tolerance: f64) -> OgeomResult<Vec<Point>> {
826 let mut sums = vec![(Vector::ZERO, 0usize); topo.vertices];
827 let ends = |c: usize| {
828 let d = &drawn[c];
829 (d[0], d[d.len() - 1])
830 };
831 for (c, &(a, b)) in topo.curve_ends.iter().enumerate() {
832 let (pa, pb) = ends(c);
833 sums[a].0 += pa.to_vector();
834 sums[a].1 += 1;
835 sums[b].0 += pb.to_vector();
836 sums[b].1 += 1;
837 }
838 let corners: Vec<Point> = sums
839 .iter()
840 .map(|(s, n)| Point::ORIGIN + *s / precise((*n).max(1)))
841 .collect();
842 for (c, &(a, b)) in topo.curve_ends.iter().enumerate() {
843 let (pa, pb) = ends(c);
844 for (v, p) in [(a, pa), (b, pb)] {
845 let off = p.distance(corners[v]);
846 if off > tolerance {
847 ogeom_bail!(
848 Construction,
849 "curve {c} ends {off} from the corner it meets other curves at, \
850 beyond the tolerance"
851 );
852 }
853 }
854 }
855 Ok(corners)
856}
857
858/// A knot vector of `spans` even spans over the unit interval.
859fn even_knots(spans: usize) -> OgeomResult<KnotVector> {
860 KnotVector::clamped_uniform(DEGREE, spans + DEGREE)
861}
862
863/// A knot vector's Greville abscissae: where each pole sits along it.
864fn greville(knots: &KnotVector) -> Vec<f64> {
865 let t = knots.knots();
866 (0..knots.control_point_count())
867 .map(|i| t[i + 1..=i + DEGREE].iter().sum::<f64>() / precise(DEGREE))
868 .collect()
869}
870
871/// Each strip's weight: the mean length of the curves that cross it alone,
872/// which sets its share of a run of strips joined at junctions. One for a
873/// strip no such curve crosses, which no junction then bounds.
874fn strip_weights(topo: &Topology, drawn: &[Vec<Point>]) -> Vec<f64> {
875 let mut sums = vec![(0.0, 0usize); topo.strips];
876 for (c, strips) in topo.curve_strip.iter().enumerate() {
877 if let [s] = strips.as_slice() {
878 sums[*s].0 += drawn[c]
879 .windows(2)
880 .map(|w| w[0].distance(w[1]))
881 .sum::<f64>();
882 sums[*s].1 += 1;
883 }
884 }
885 sums.iter()
886 .map(|&(sum, n)| if n > 0 { sum / precise(n) } else { 1.0 })
887 .collect()
888}
889
890/// Which knots inside a strip are doubled.
891#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
892struct Doubled {
893 /// Each end's first knot in, in the strip's own direction.
894 ends: [bool; 2],
895 /// Every knot inside it.
896 every: bool,
897}
898
899/// How many knots each strip takes beyond one per span: one for each knot
900/// inside it that is doubled.
901fn extra_knots(spans: usize, doubled: Doubled) -> usize {
902 let ends = doubled.ends;
903 match spans {
904 1 => 0,
905 _ if doubled.every => spans - 1,
906 2 => usize::from(ends[0] || ends[1]),
907 _ => usize::from(ends[0]) + usize::from(ends[1]),
908 }
909}
910
911/// The knots of a run of strips over the unit interval, and the bounds of
912/// each strip's share of it: each strip's spans even over its share, the
913/// shares in proportion to the strips' weights, and a knot of multiplicity
914/// the degree at each junction between two strips, where the run's poles
915/// pass through the junction's. `flips` says which strips the run crosses
916/// against their own direction, and `doubled` which knots inside each
917/// strip are doubled (none where it is empty).
918fn run_knots(
919 strips: &[usize],
920 flips: &[bool],
921 spans: &[usize],
922 weights: &[f64],
923 doubled: &[Doubled],
924) -> OgeomResult<(KnotVector, Vec<f64>)> {
925 let ends = |s: usize| doubled.get(s).copied().unwrap_or_default();
926 if let [one] = strips
927 && extra_knots(spans[*one], ends(*one)) == 0
928 {
929 return Ok((even_knots(spans[*one])?, vec![0.0, 1.0]));
930 }
931 let total: f64 = strips.iter().map(|&s| weights[s]).sum();
932 let mut bounds = vec![0.0];
933 let mut run = 0.0;
934 for (i, &s) in strips.iter().enumerate() {
935 run += weights[s];
936 bounds.push(if i + 1 == strips.len() {
937 1.0
938 } else {
939 run / total
940 });
941 }
942 let mut knots = vec![0.0; DEGREE + 1];
943 for (i, &s) in strips.iter().enumerate() {
944 let (a, b) = (bounds[i], bounds[i + 1]);
945 let (
946 n,
947 Doubled {
948 ends: [first, last],
949 every,
950 },
951 ) = (spans[s], ends(s));
952 let flip = flips.get(i).copied().unwrap_or(false);
953 for k in 1..n {
954 let x = if strips.len() == 1 {
955 precise(k) / precise(n)
956 } else {
957 (b - a).mul_add(precise(k) / precise(n), a)
958 };
959 // The knot's place counted in the strip's own direction.
960 let own = if flip { n - k } else { k };
961 let twice = every || (own == 1 && first) || (own == n - 1 && last);
962 knots.extend(core::iter::repeat_n(x, 1 + usize::from(twice)));
963 }
964 if i + 1 < strips.len() {
965 knots.extend(core::iter::repeat_n(b, DEGREE));
966 }
967 }
968 knots.extend(core::iter::repeat_n(1.0, DEGREE + 1));
969 Ok((KnotVector::new(knots, DEGREE)?, bounds))
970}
971
972/// The poles of a run of strips of `controls` poles each, a junction's
973/// pole shared by the strips either side.
974fn run_poles(controls: &[usize], strips: &[usize]) -> usize {
975 strips.iter().map(|&s| controls[s]).sum::<usize>() + 1 - strips.len()
976}
977
978/// Where the poles of the patches' nets stand among the unknowns: a slot
979/// per vertex, per inner pole of each curve and per inner pole of each
980/// patch.
981struct Layout {
982 /// The poles along each strip.
983 controls: Vec<usize>,
984 /// Each curve's poles.
985 curve_poles: Vec<usize>,
986 /// Each patch's net, `u` poles by `v` poles.
987 dims: Vec<(usize, usize)>,
988 /// Each curve's first inner pole's slot.
989 curve_base: Vec<usize>,
990 /// Each patch's first inner pole's slot.
991 patch_base: Vec<usize>,
992 /// How many slots there are.
993 slots: usize,
994}
995
996impl Layout {
997 /// The layout over `spans` per strip, the knots inside each strip
998 /// `doubled` names doubled.
999 fn new(topo: &Topology, spans: &[usize], doubled: &[Doubled]) -> Self {
1000 let controls: Vec<usize> = spans
1001 .iter()
1002 .zip(doubled)
1003 .map(|(&s, &ends)| s + DEGREE + extra_knots(s, ends))
1004 .collect();
1005 let curve_poles: Vec<usize> = topo
1006 .curve_strip
1007 .iter()
1008 .map(|strips| run_poles(&controls, strips))
1009 .collect();
1010 let dims: Vec<(usize, usize)> = topo
1011 .patch_strips
1012 .iter()
1013 .map(|[u, v]| (run_poles(&controls, u), run_poles(&controls, v)))
1014 .collect();
1015 let mut next = topo.vertices;
1016 let curve_base = curve_poles
1017 .iter()
1018 .map(|&n| {
1019 let base = next;
1020 next += n - 2;
1021 base
1022 })
1023 .collect();
1024 let patch_base = dims
1025 .iter()
1026 .map(|&(nu, nv)| {
1027 let base = next;
1028 next += (nu - 2) * (nv - 2);
1029 base
1030 })
1031 .collect();
1032 Self {
1033 controls,
1034 curve_poles,
1035 dims,
1036 curve_base,
1037 patch_base,
1038 slots: next,
1039 }
1040 }
1041
1042 /// A patch's net, `u` poles by `v` poles.
1043 fn dims(&self, p: usize) -> (usize, usize) {
1044 self.dims[p]
1045 }
1046
1047 /// A curve's poles.
1048 fn along(&self, c: usize) -> usize {
1049 self.curve_poles[c]
1050 }
1051
1052 /// The slot of a curve's pole `q`.
1053 fn curve_slot(&self, topo: &Topology, c: usize, q: usize) -> usize {
1054 let n = self.along(c);
1055 let (a, b) = topo.curve_ends[c];
1056 if q == 0 {
1057 a
1058 } else if q == n - 1 {
1059 b
1060 } else {
1061 self.curve_base[c] + q - 1
1062 }
1063 }
1064
1065 /// How many poles along side `k` of patch `p`'s walk curve `c`'s pole
1066 /// `q` stands.
1067 fn walk_index(
1068 &self,
1069 patches: &[ChainedPatch],
1070 (p, k): (usize, usize),
1071 c: usize,
1072 q: usize,
1073 ) -> usize {
1074 let mut offset = 0;
1075 for piece in &patches[p].sides[k] {
1076 let n = self.along(piece.curve);
1077 if piece.curve == c {
1078 return offset + if piece.reversed { n - 1 - q } else { q };
1079 }
1080 offset += n - 1;
1081 }
1082 offset
1083 }
1084
1085 /// The slot of pole `(i, j)` of patch `p`.
1086 fn slot(
1087 &self,
1088 topo: &Topology,
1089 patches: &[ChainedPatch],
1090 p: usize,
1091 (i, j): (usize, usize),
1092 ) -> usize {
1093 let (nu, nv) = self.dims(p);
1094 let corners = topo.corner_vertex[p];
1095 let (k, w) = match (i, j) {
1096 (0, 0) => return corners[0],
1097 (x, 0) if x == nu - 1 => return corners[1],
1098 (x, y) if x == nu - 1 && y == nv - 1 => return corners[2],
1099 (0, y) if y == nv - 1 => return corners[3],
1100 (_, 0) => (0, i),
1101 (x, _) if x == nu - 1 => (1, j),
1102 (_, y) if y == nv - 1 => (2, nu - 1 - i),
1103 (0, _) => (3, nv - 1 - j),
1104 _ => return self.patch_base[p] + (i - 1) * (nv - 2) + (j - 1),
1105 };
1106 // The curve of the side the pole lies on, its curves' poles laid
1107 // end to end along the walk, a junction's pole shared.
1108 let mut offset = 0;
1109 for piece in &patches[p].sides[k] {
1110 let n = self.along(piece.curve);
1111 if w < offset + n {
1112 let q = w - offset;
1113 return self.curve_slot(
1114 topo,
1115 piece.curve,
1116 if piece.reversed { n - 1 - q } else { q },
1117 );
1118 }
1119 offset += n - 1;
1120 }
1121 corners[k]
1122 }
1123}
1124
1125/// The pole of side `k` of an `nu` by `nv` net at `w` poles along its
1126/// walk, and the pole one row in from it.
1127fn side_pole(k: usize, w: usize, (nu, nv): (usize, usize)) -> ((usize, usize), (usize, usize)) {
1128 match k {
1129 0 => ((w, 0), (w, 1)),
1130 1 => ((nu - 1, w), (nu - 2, w)),
1131 2 => ((nu - 1 - w, nv - 1), (nu - 1 - w, nv - 2)),
1132 _ => ((0, nv - 1 - w), (1, nv - 1 - w)),
1133 }
1134}
1135
1136/// The chart point of a patch's corner `k`.
1137fn corner_chart(k: usize) -> (f64, f64) {
1138 match k % 4 {
1139 0 => (0.0, 0.0),
1140 1 => (1.0, 0.0),
1141 2 => (1.0, 1.0),
1142 _ => (0.0, 1.0),
1143 }
1144}
1145
1146/// The strips along the walk of each curve of side `k` of patch `p`.
1147fn side_parts(topo: &Topology, patches: &[ChainedPatch], (p, k): (usize, usize)) -> Vec<usize> {
1148 patches[p].sides[k]
1149 .iter()
1150 .map(|s| topo.curve_strip[s.curve].len())
1151 .collect()
1152}
1153
1154/// The share of side `k`'s walk, from its start, that its `i`-th curve
1155/// covers, `parts` the strips each of its curves crosses in walking order
1156/// and `bounds` the bounds of the shares of the strips along the side's
1157/// direction.
1158fn walk_share(k: usize, i: usize, parts: &[usize], bounds: &[f64]) -> (f64, f64) {
1159 if parts.len() < 2 {
1160 return (0.0, 1.0);
1161 }
1162 let m = bounds.len() - 1;
1163 let (from, to) = (
1164 parts[..i].iter().sum::<usize>(),
1165 parts[..=i].iter().sum::<usize>(),
1166 );
1167 if k < 2 {
1168 (bounds[from], bounds[to])
1169 } else {
1170 (1.0 - bounds[m - from], 1.0 - bounds[m - to])
1171 }
1172}
1173
1174/// Which curve of side `k` holds the point at `x` along the side's
1175/// direction, `parts` the strips each of its curves crosses in walking
1176/// order and `bounds` the bounds of the shares of its strips.
1177fn piece_at(k: usize, x: f64, parts: &[usize], bounds: &[f64]) -> usize {
1178 if parts.len() < 2 {
1179 return 0;
1180 }
1181 let m = bounds.len() - 1;
1182 let strip = bounds[1..m].iter().filter(|&&b| x > b).count();
1183 let walked = if k < 2 { strip } else { m - 1 - strip };
1184 let mut end = 0;
1185 for (i, n) in parts.iter().enumerate() {
1186 end += n;
1187 if walked < end {
1188 return i;
1189 }
1190 }
1191 parts.len() - 1
1192}
1193
1194/// The seed patches the points are parameterised over.
1195struct Seeds {
1196 surfaces: Vec<SurfaceGeometry>,
1197 searches: Vec<SurfaceSeeds>,
1198 /// The box of each seed's net, which holds the patch.
1199 boxes: Vec<(Point, Point)>,
1200 /// The bounds of each seed's strips' shares along `u` and `v`.
1201 bounds: Vec<[Vec<f64>; 2]>,
1202}
1203
1204/// The points each patch takes and where on it.
1205#[derive(Clone)]
1206struct Taken {
1207 patch: Vec<usize>,
1208 point: Vec<Point>,
1209 at: Vec<(f64, f64)>,
1210 /// Each patch's points, by index.
1211 of_patch: Vec<Vec<usize>>,
1212 outside: usize,
1213}
1214
1215impl Seeds {
1216 #[allow(clippy::too_many_lines, reason = "the seed curves, then the patches")]
1217 #[allow(clippy::too_many_arguments, reason = "one construction, all its data")]
1218 fn new(
1219 topo: &Topology,
1220 patches: &[ChainedPatch],
1221 drawn: &[Vec<Point>],
1222 corners: &[Point],
1223 weights: &[f64],
1224 options: &NetworkOptions,
1225 params: &[Option<Vec<f64>>],
1226 tol: Tolerances,
1227 ) -> OgeomResult<Self> {
1228 // Each curve's chord parameters over the unit interval, or the
1229 // parameters a held curve is drawn at.
1230 let chords: Vec<Vec<f64>> = drawn
1231 .iter()
1232 .zip(params)
1233 .map(|(d, given)| {
1234 if let Some(given) = given {
1235 return given.clone();
1236 }
1237 let mut at = vec![0.0];
1238 for w in d.windows(2) {
1239 at.push(at[at.len() - 1] + w[0].distance(w[1]));
1240 }
1241 let total = at[at.len() - 1];
1242 at.iter()
1243 .map(|t| if total > 0.0 { t / total } else { 0.0 })
1244 .collect()
1245 })
1246 .collect();
1247 let along = |c: usize, t: f64| -> Point {
1248 let (d, at) = (&drawn[c], &chords[c]);
1249 let k = at.partition_point(|x| *x <= t).clamp(1, d.len() - 1);
1250 let (a, b) = (at[k - 1], at[k]);
1251 let f = if b > a {
1252 ((t - a) / (b - a)).clamp(0.0, 1.0)
1253 } else {
1254 0.0
1255 };
1256 d[k - 1] + (d[k] - d[k - 1]) * f
1257 };
1258 let poles_of = |c: usize, knots: &KnotVector| -> Vec<Point> {
1259 let mut poles: Vec<Point> = greville(knots).iter().map(|&t| along(c, t)).collect();
1260 let (a, b) = topo.curve_ends[c];
1261 let n = poles.len();
1262 poles[0] = corners[a];
1263 poles[n - 1] = corners[b];
1264 poles
1265 };
1266 // Each strip's spans: doubled until every curve crossing it passes
1267 // within the tolerance of its points.
1268 let mut spans = vec![options.spans; topo.strips];
1269 let mut settled = vec![false; drawn.len()];
1270 loop {
1271 let mut raise = vec![false; topo.strips];
1272 for c in 0..drawn.len() {
1273 if settled[c] {
1274 continue;
1275 }
1276 let (knots, _) = run_knots(&topo.curve_strip[c], &[], &spans, weights, &[])?;
1277 let curve = ogeom_geom::BSplineCurve::new(knots.clone(), poles_of(c, &knots), tol)?;
1278 let mut within = true;
1279 for (p, &t) in drawn[c].iter().zip(&chords[c]) {
1280 use ogeom_geom::Curve3d as _;
1281 if curve.point_at(t, tol)?.distance(*p) > options.tolerance {
1282 within = false;
1283 }
1284 }
1285 let raised: Vec<usize> = topo.curve_strip[c]
1286 .iter()
1287 .copied()
1288 .filter(|&s| spans[s] < options.most)
1289 .collect();
1290 if within || raised.is_empty() {
1291 settled[c] = true;
1292 }
1293 if !within {
1294 for s in raised {
1295 raise[s] = true;
1296 }
1297 }
1298 }
1299 if !raise.contains(&true) {
1300 break;
1301 }
1302 for (s, spans) in spans.iter_mut().enumerate() {
1303 if raise[s] {
1304 *spans = (2 * *spans).min(options.most);
1305 }
1306 }
1307 // A curve crossing a raised strip is drawn again.
1308 for (c, strips) in topo.curve_strip.iter().enumerate() {
1309 if strips.iter().any(|&s| raise[s]) {
1310 settled[c] = false;
1311 }
1312 }
1313 }
1314 let mut surfaces = Vec::with_capacity(patches.len());
1315 let mut searches = Vec::with_capacity(patches.len());
1316 let mut boxes = Vec::with_capacity(patches.len());
1317 let mut all_bounds = Vec::with_capacity(patches.len());
1318 for (p, patch) in patches.iter().enumerate() {
1319 let [su, sv] = &topo.patch_strips[p];
1320 let (ku, bu) = run_knots(su, &[], &spans, weights, &[])?;
1321 let (kv, bv) = run_knots(sv, &[], &spans, weights, &[])?;
1322 let (nu, nv) = (ku.control_point_count(), kv.control_point_count());
1323 let mut net = vec![Point::ORIGIN; nu * nv];
1324 for (k, side) in patch.sides.iter().enumerate() {
1325 let n = if k % 2 == 0 { nu } else { nv };
1326 let poles = if side.is_empty() {
1327 vec![corners[topo.corner_vertex[p][k]]; n]
1328 } else {
1329 let mut poles: Vec<Point> = Vec::with_capacity(n);
1330 for piece in side {
1331 let (knots, _) =
1332 run_knots(&topo.curve_strip[piece.curve], &[], &spans, weights, &[])?;
1333 let mut own = poles_of(piece.curve, &knots);
1334 if piece.reversed {
1335 own.reverse();
1336 }
1337 // A junction's pole ends one curve and starts the
1338 // next.
1339 let skip = usize::from(!poles.is_empty());
1340 poles.extend(own.into_iter().skip(skip));
1341 }
1342 poles
1343 };
1344 for (w, pole) in poles.into_iter().enumerate() {
1345 let ((i, j), _) = side_pole(k, w, (nu, nv));
1346 net[i * nv + j] = pole;
1347 }
1348 }
1349 // The bilinearly blended Coons patch of the four sides: on a
1350 // net whose inner poles stand at the Greville abscissae's blend,
1351 // linear precision makes it the Coons surface exactly.
1352 let (xs, ys) = (greville(&ku), greville(&kv));
1353 let at = |i: usize, j: usize| net[i * nv + j].to_vector();
1354 let (c00, c10, c11, c01) = (at(0, 0), at(nu - 1, 0), at(nu - 1, nv - 1), at(0, nv - 1));
1355 let mut filled = net.clone();
1356 for i in 1..nu - 1 {
1357 for j in 1..nv - 1 {
1358 let (x, y) = (xs[i], ys[j]);
1359 let ruled = at(i, 0) * (1.0 - y)
1360 + at(i, nv - 1) * y
1361 + at(0, j) * (1.0 - x)
1362 + at(nu - 1, j) * x;
1363 let bilinear = c00 * ((1.0 - x) * (1.0 - y))
1364 + c10 * (x * (1.0 - y))
1365 + c11 * (x * y)
1366 + c01 * ((1.0 - x) * y);
1367 filled[i * nv + j] = Point::ORIGIN + (ruled - bilinear);
1368 }
1369 }
1370 boxes.push(net_box(&filled));
1371 let grid = ControlGrid::new(filled, nu, nv)?;
1372 let surface = SurfaceGeometry::BSpline(BSplineSurface::new(ku, kv, &grid, tol)?);
1373 // A B-spline patch seeds its projections by its span boxes; the
1374 // sample count is for surfaces without them.
1375 searches.push(SurfaceSeeds::over(&surface, 0, tol)?);
1376 surfaces.push(surface);
1377 all_bounds.push([bu, bv]);
1378 }
1379 Ok(Self {
1380 surfaces,
1381 searches,
1382 boxes,
1383 bounds: all_bounds,
1384 })
1385 }
1386
1387 /// Give each point to the seed nearest it, at the parameters of its
1388 /// nearest point there; a point whose nearest point on a seed lies on a
1389 /// side no other patch shares is past that seed.
1390 fn take(
1391 &self,
1392 topo: &Topology,
1393 patches: &[ChainedPatch],
1394 cloud: &[Point],
1395 tol: Tolerances,
1396 ) -> OgeomResult<Taken> {
1397 // Whether each curve of each side is on the network's border, and
1398 // the strips each crosses.
1399 let border: Vec<[(Vec<bool>, Vec<usize>); 4]> = (0..patches.len())
1400 .map(|p| {
1401 core::array::from_fn(|k| {
1402 (
1403 patches[p].sides[k]
1404 .iter()
1405 .map(|s| topo.users[s.curve].len() < 2)
1406 .collect(),
1407 side_parts(topo, patches, (p, k)),
1408 )
1409 })
1410 })
1411 .collect();
1412 let past = |p: usize, (u, v): (f64, f64)| {
1413 let free = |k: usize, x: f64| {
1414 let (sides, parts) = &border[p][k];
1415 let i = piece_at(k, x, parts, &self.bounds[p][k % 2]);
1416 sides.get(i).copied().unwrap_or(false)
1417 };
1418 (v <= 0.0 && free(0, u))
1419 || (u >= 1.0 && free(1, v))
1420 || (v >= 1.0 && free(2, u))
1421 || (u <= 0.0 && free(3, v))
1422 };
1423 let starts: Vec<usize> = (0..cloud.len()).step_by(POINT_BATCH).collect();
1424 let found = ogeom_core::parallel::map_ordered(&starts, |_, &start| {
1425 let mut out = Vec::new();
1426 let mut order: Vec<(f64, usize)> = Vec::with_capacity(self.surfaces.len());
1427 for &q in &cloud[start..(start + POINT_BATCH).min(cloud.len())] {
1428 order.clear();
1429 order.extend(
1430 self.boxes
1431 .iter()
1432 .enumerate()
1433 .map(|(p, b)| (box_distance(*b, q), p)),
1434 );
1435 order.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)));
1436 let mut best: Option<(usize, (f64, f64), f64)> = None;
1437 for &(reach, p) in &order {
1438 if best.is_some_and(|b| reach > b.2) {
1439 break;
1440 }
1441 let foot = self.searches[p].project(&self.surfaces[p], q, tol)?;
1442 if best.is_none_or(|b| foot.distance < b.2) {
1443 best = Some((p, foot.parameters, foot.distance));
1444 }
1445 }
1446 out.push(best.filter(|b| !past(b.0, b.1)).map(|(p, at, _)| (p, at)));
1447 }
1448 Ok::<_, ogeom_core::OgeomError>(out)
1449 });
1450 let mut taken = Taken {
1451 patch: Vec::new(),
1452 point: Vec::new(),
1453 at: Vec::new(),
1454 of_patch: vec![Vec::new(); patches.len()],
1455 outside: 0,
1456 };
1457 let mut i = 0;
1458 for batch in found {
1459 for found in batch? {
1460 match found {
1461 Some((p, at)) => {
1462 taken.of_patch[p].push(taken.patch.len());
1463 taken.patch.push(p);
1464 taken.point.push(cloud[i]);
1465 taken.at.push(at);
1466 }
1467 None => taken.outside += 1,
1468 }
1469 i += 1;
1470 }
1471 }
1472 Ok(taken)
1473 }
1474}
1475
1476/// The box of a net's poles.
1477fn net_box(net: &[Point]) -> (Point, Point) {
1478 let mut lo = net[0];
1479 let mut hi = net[0];
1480 for p in net {
1481 lo = Point::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
1482 hi = Point::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
1483 }
1484 (lo, hi)
1485}
1486
1487/// The distance from a point to a box, nought inside it.
1488fn box_distance((lo, hi): (Point, Point), p: Point) -> f64 {
1489 let gap = |x: f64, a: f64, b: f64| (a - x).max(0.0).max(x - b);
1490 let (dx, dy, dz) = (
1491 gap(p.x, lo.x, hi.x),
1492 gap(p.y, lo.y, hi.y),
1493 gap(p.z, lo.z, hi.z),
1494 );
1495 dz.mul_add(dz, dx.mul_add(dx, dy * dy)).sqrt()
1496}
1497
1498/// The integrals over a knot vector's domain of the products of its basis
1499/// functions' derivatives, for orders nought to two: `grams[k][i][d]` is
1500/// the integral of `N_i^(k) N_(i+d)^(k)`, for `d` up to the degree.
1501fn derivative_grams(knots: &KnotVector) -> [Vec<Vec<f64>>; 3] {
1502 let p = knots.degree();
1503 let n = knots.control_point_count();
1504 let mut grams: [Vec<Vec<f64>>; 3] = core::array::from_fn(|_| vec![vec![0.0; p + 1]; n]);
1505 let distinct = knots.distinct();
1506 for w in distinct.windows(2) {
1507 for (t, weight) in ogeom_math::gauss_legendre_rule(w[0].0, w[1].0) {
1508 let span = knots.span_unchecked(t);
1509 let rows = knots.basis_derivatives(span, t, 2);
1510 for (k, gram) in grams.iter_mut().enumerate() {
1511 let Some(row) = rows.get(k) else { continue };
1512 for i in 0..=p {
1513 for j in i..=p {
1514 gram[span - p + i][j - i] += weight * row[i] * row[j];
1515 }
1516 }
1517 }
1518 }
1519 }
1520 grams
1521}
1522
1523/// One patch's own normal equations over its net, `u` outer: the lower
1524/// band of its points' Gram matrix and thin-plate energy, and the
1525/// right-hand sides.
1526struct Local {
1527 nv: usize,
1528 band: usize,
1529 lower: Vec<Vec<f64>>,
1530 rhs: Vec<[f64; 3]>,
1531}
1532
1533/// Which corners of other than four patches are held exactly tangent.
1534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1535enum Corners {
1536 /// None: every curve into one is fitted near tangent.
1537 Loose,
1538 /// The corners of an odd number of patches whose curves all run on to
1539 /// the border, `γ` linear along each whole curve.
1540 Whole,
1541 /// The corners of an odd number of patches, or an even number above
1542 /// four, held by the span of each curve next to them: that span a
1543 /// quadratic, `γ` quadratic over it and nought past it, so that past it
1544 /// the curve is held by the pole triples and may run on to a corner of
1545 /// four patches, a junction or another such corner.
1546 Spans,
1547 /// The corners of an odd number of patches, or an even number above
1548 /// four, whose curves all run on to the border or to another such
1549 /// corner, `γ` linear along each whole curve, every knot inside the
1550 /// curve's strip doubled and the curve held `C²` across each.
1551 Spread,
1552}
1553
1554/// A curve the caller holds, read at a parameter over the unit interval.
1555#[derive(Debug, Clone)]
1556enum HeldCurve<'a> {
1557 /// A curve over its range.
1558 Curve(&'a Curve, (f64, f64)),
1559 /// A polyline at its chord parameters.
1560 Polyline(&'a [Point], Vec<f64>),
1561}
1562
1563impl HeldCurve<'_> {
1564 /// The derivative at `t` over the unit interval: the curve's own,
1565 /// scaled to it, or a polyline's chord at that end.
1566 fn d1(&self, t: f64, tol: Tolerances) -> OgeomResult<Vector> {
1567 match self {
1568 Self::Curve(curve, (lo, hi)) => {
1569 use ogeom_geom::Curve3d as _;
1570 Ok(curve.d1_at((hi - lo).mul_add(t, *lo), tol)? * (hi - lo))
1571 }
1572 Self::Polyline(points, at) => {
1573 let k = at.partition_point(|x| *x <= t).clamp(1, points.len() - 1);
1574 let span = at[k] - at[k - 1];
1575 if span > 0.0 {
1576 Ok((points[k] - points[k - 1]) / span)
1577 } else {
1578 Ok(Vector::ZERO)
1579 }
1580 }
1581 }
1582 }
1583
1584 /// The point at `t` over the unit interval.
1585 fn at(&self, t: f64, tol: Tolerances) -> OgeomResult<Point> {
1586 match self {
1587 Self::Curve(curve, (lo, hi)) => {
1588 use ogeom_geom::Curve3d as _;
1589 curve.point_at((hi - lo).mul_add(t, *lo), tol)
1590 }
1591 Self::Polyline(points, at) => {
1592 let k = at.partition_point(|x| *x <= t).clamp(1, points.len() - 1);
1593 let (a, b) = (at[k - 1], at[k]);
1594 let f = if b > a {
1595 ((t - a) / (b - a)).clamp(0.0, 1.0)
1596 } else {
1597 0.0
1598 };
1599 Ok(points[k - 1] + (points[k] - points[k - 1]) * f)
1600 }
1601 }
1602 }
1603}
1604
1605/// Five-point Gauss-Legendre nodes and weights on `[-1, 1]`: exact over a
1606/// span for the products of two cubic pieces and more.
1607const GAUSS: [(f64, f64); 5] = [
1608 (0.0, 0.568_888_888_888_888_9),
1609 (-0.538_469_310_105_683_1, 0.478_628_670_499_366_5),
1610 (0.538_469_310_105_683_1, 0.478_628_670_499_366_5),
1611 (-0.906_179_845_938_664, 0.236_926_885_056_189_1),
1612 (0.906_179_845_938_664, 0.236_926_885_056_189_1),
1613];
1614
1615/// The fitting problem, fixed across rounds.
1616struct Fit<'a> {
1617 topo: &'a Topology,
1618 patches: &'a [ChainedPatch],
1619 taken: &'a Taken,
1620 weights: &'a [f64],
1621 smoothing: f64,
1622 /// Which corners of other than four patches are held exactly tangent,
1623 /// and how.
1624 corners: Corners,
1625 /// Whether the fans are held exactly tangent.
1626 fans: bool,
1627 /// Each curve the caller holds, by index.
1628 held: &'a [Option<HeldCurve<'a>>],
1629 /// How the patch leaves each held border curve, where the caller says.
1630 across: &'a [Option<Across>],
1631 /// Where each corner stands.
1632 vertex_points: &'a [Point],
1633 tol: Tolerances,
1634}
1635
1636/// A fit over given spans, each point at its nearest point on it.
1637struct Round {
1638 layout: Layout,
1639 /// Each patch's knots along `u` and `v`.
1640 patch_knots: Vec<[KnotVector; 2]>,
1641 /// The bounds of each patch's strips' shares along `u` and `v`.
1642 patch_bounds: Vec<[Vec<f64>; 2]>,
1643 /// Each curve's knots.
1644 curve_knots: Vec<KnotVector>,
1645 /// Each slot's pole.
1646 poles: Vec<Point>,
1647 surfaces: Vec<BSplineSurface>,
1648 /// How many unknowns the constraints leave free.
1649 free: usize,
1650 feet: Vec<(f64, f64)>,
1651 distances: Vec<f64>,
1652 /// Whether every corner the way of holding them names is held.
1653 held: bool,
1654}
1655
1656impl Round {
1657 /// The sum of the squared distances.
1658 fn square(&self) -> f64 {
1659 self.distances.iter().map(|d| d * d).sum()
1660 }
1661}
1662
1663/// Slots and their weights.
1664type Weighted = Vec<(usize, f64)>;
1665
1666/// A constraint row: slots and their coefficients, summing to nought.
1667type Row = Vec<(usize, f64)>;
1668
1669/// How the curves into a corner of other than four patches relate there.
1670struct Relation {
1671 /// The corner's vertex.
1672 vertex: usize,
1673 /// Each curve's pole next to the corner and the poles beside it there in
1674 /// its first and second patch.
1675 near: Vec<(usize, usize, usize)>,
1676 /// The curves in turn round the corner.
1677 order: Vec<usize>,
1678 /// Each curve's `λ` and `g` there.
1679 weights: Vec<(f64, f64)>,
1680 /// Each curve's first pole off the corner, in the plane the relation
1681 /// is read in.
1682 flat: Vec<(f64, f64)>,
1683 /// The curves either side of each: the one whose first pole lies in
1684 /// its first patch, and the one in its second.
1685 sides: Vec<(usize, usize)>,
1686}
1687
1688/// Each curve's `λ` and `g` at a corner, `e₁ + λ e₂ + g e = 0` among its
1689/// own first pole `e` and those of the curves either side in the plane;
1690/// `None` where a ratio is not positive.
1691fn plane_weights(flat: &[(f64, f64)], sides: &[(usize, usize)]) -> Option<Vec<(f64, f64)>> {
1692 let mut weights = Vec::with_capacity(flat.len());
1693 for (i, &(l, r)) in sides.iter().enumerate() {
1694 let ((lx, ly), (ox, oy), (rx, ry)) = (flat[l], flat[i], flat[r]);
1695 let det = rx.mul_add(oy, -(ry * ox));
1696 let lambda = (ly.mul_add(ox, -(lx * oy))) / det;
1697 let g = (lx.mul_add(ry, -(ly * rx))) / det;
1698 if !(lambda > 0.0 && lambda.is_finite() && g.is_finite()) {
1699 return None;
1700 }
1701 weights.push((lambda, g));
1702 }
1703 Some(weights)
1704}
1705
1706impl Relation {
1707 /// Scale the curves' first poles so each curve of `wanted` takes its
1708 /// ratio there, then hold it to that ratio to the bit; false, the
1709 /// relation untouched, where a ratio is then not positive.
1710 ///
1711 /// Scaling the first pole of the curve on a curve's first side by `a`
1712 /// and that on its second by `b` scales its `λ` by `a / b`, and its own
1713 /// leaves `λ` alone: the ratios wanted are differences of the scales'
1714 /// logarithms along the curves either side of each, each run of them
1715 /// settled with the mean logarithm nought. Those pairs close in one
1716 /// cycle round an odd corner and two round an even one; where every
1717 /// curve of a cycle is wanted, the ratios wanted have been [closed
1718 /// round it](close_cycles), and its last pair follows from the others.
1719 fn rescale(&mut self, wanted: &[(usize, f64)]) -> bool {
1720 let n = self.flat.len();
1721 // Each scale's logarithm against its run's first: a union-find
1722 // whose links carry the difference.
1723 let mut parent: Vec<usize> = (0..n).collect();
1724 let mut offset = vec![0.0; n];
1725 let root = |parent: &[usize], offset: &[f64], mut x: usize| {
1726 let mut sum = 0.0;
1727 while parent[x] != x {
1728 sum += offset[x];
1729 x = parent[x];
1730 }
1731 (x, sum)
1732 };
1733 for &(i, target) in wanted {
1734 let (l, r) = self.sides[i];
1735 // `log a_l − log a_r = log(target / λ)`.
1736 let step = (target / self.weights[i].0).ln();
1737 let ((rl, ol), (rr, or)) = (root(&parent, &offset, l), root(&parent, &offset, r));
1738 if rl == rr {
1739 continue;
1740 }
1741 parent[rr] = rl;
1742 offset[rr] = ol - step - or;
1743 }
1744 let logs: Vec<(usize, f64)> = (0..n).map(|x| root(&parent, &offset, x)).collect();
1745 let mut means = vec![(0.0, 0usize); n];
1746 for &(r, log) in &logs {
1747 means[r].0 += log;
1748 means[r].1 += 1;
1749 }
1750 let flat: Vec<(f64, f64)> = self
1751 .flat
1752 .iter()
1753 .zip(&logs)
1754 .map(|(&(x, y), &(r, log))| {
1755 let scale = (log - means[r].0 / precise(means[r].1)).exp();
1756 (x * scale, y * scale)
1757 })
1758 .collect();
1759 let Some(mut weights) = plane_weights(&flat, &self.sides) else {
1760 return false;
1761 };
1762 for &(i, target) in wanted {
1763 weights[i].0 = target;
1764 }
1765 self.flat = flat;
1766 self.weights = weights;
1767 true
1768 }
1769
1770 /// The rows holding the relation: `n − 2` consecutive curves' own, the
1771 /// other two following from them.
1772 fn rows(&self) -> Vec<Vec<(usize, f64)>> {
1773 self.order[..self.order.len() - 2]
1774 .iter()
1775 .map(|&i| {
1776 let (left, own, right) = self.near[i];
1777 let (lambda, g) = self.weights[i];
1778 vec![
1779 (left, 1.0),
1780 (right, lambda),
1781 (own, g),
1782 (self.vertex, -(1.0 + lambda + g)),
1783 ]
1784 })
1785 .collect()
1786 }
1787}
1788
1789/// A corner held by its spans, with its relation where it has one.
1790struct Related {
1791 vertex: usize,
1792 curves: Vec<usize>,
1793 relation: Option<Relation>,
1794}
1795
1796/// A constraint row keyed by its slots: the middle one, then the other two
1797/// in order.
1798type Keyed = BTreeMap<(usize, usize, usize), Vec<(usize, f64)>>;
1799
1800/// The parameter lines through the junctions: each a patch's rows across
1801/// one junction, joined with the rows of the patches beyond the curves
1802/// that carry the junction on.
1803struct Lines {
1804 /// Each junction row's line, and whether the row as kept runs against
1805 /// the line's direction.
1806 of: BTreeMap<(usize, usize, usize), (usize, bool)>,
1807 /// The number of lines.
1808 count: usize,
1809 /// The lines met both ways, which hold their patches' own ratio alone.
1810 torn: Vec<bool>,
1811}
1812
1813/// What the tangency rows come to: the rows, which chains are left untied,
1814/// the junction rows held at a ratio of their own in place of their
1815/// patches' knot spacings, and the triples held.
1816struct Tangency {
1817 rows: Vec<Row>,
1818 untied: Vec<bool>,
1819 relaxed: Keyed,
1820 /// Each triple held, by its curve's pole: the poles beside it in the
1821 /// curve's first and second patch, and `λ`.
1822 held: BTreeMap<usize, (usize, usize, f64)>,
1823}
1824
1825/// The members of a class of chains or of junction lines, each with
1826/// whether its ratio is the inverse of the class's root's.
1827type Members = Vec<(usize, bool)>;
1828
1829/// A class of chains and junction lines that corners pin: each corner's
1830/// pin (the corner, its curve, and the ratio it pins the root to), and its
1831/// members.
1832struct Pinned {
1833 pins: Vec<(usize, usize, f64)>,
1834 chains: Members,
1835 lines: Members,
1836}
1837
1838/// A class of chains and junction lines a corner settles on one ratio:
1839/// the ratio of the class's root, and each chain and line with whether its
1840/// own is the inverse.
1841struct Settled {
1842 ratio: f64,
1843 chains: Members,
1844 lines: Members,
1845}
1846
1847/// A curve of a corner asked the ratio of a settled class: the curve's
1848/// place round the corner, the class, whether the chain's ratio is the
1849/// class's inverse, and whether the curve's is its chain's.
1850#[derive(Debug, Clone, Copy)]
1851struct Asked {
1852 curve: usize,
1853 class: usize,
1854 odd: bool,
1855 inverse: bool,
1856}
1857
1858/// A combination of the classes' log ratios with integer coefficients,
1859/// and a constant.
1860type Potential = (BTreeMap<usize, i64>, f64);
1861
1862/// `a + sign b`.
1863fn potential_add(a: &Potential, b: &Potential, sign: i64) -> Potential {
1864 let mut out = a.0.clone();
1865 for (&f, &x) in &b.0 {
1866 *out.entry(f).or_insert(0) += sign * x;
1867 }
1868 out.retain(|_, x| *x != 0);
1869 (out, precise_signed(sign).mul_add(b.1, a.1))
1870}
1871
1872/// Move the log ratios `logs` of the settled classes as little as lets
1873/// every corner meet every ratio asked of it; the classes that cannot be
1874/// met so.
1875///
1876/// A corner [rescaled](Relation::rescale) meets the ratios asked of its
1877/// curves through the differences of its scales' logarithms, one per
1878/// curve between the scales of the curves either side. Those pairs close
1879/// in one cycle round an odd corner and two round an even one, and where
1880/// every curve of a cycle is asked a ratio, the differences round it sum
1881/// to nought: the ratios asked, signed as the cycle meets each curve, sum
1882/// to the sum of the curves' own log ratios. Round an odd corner that sum
1883/// is nought, as the ratios multiply to one; round an even one it is the
1884/// cycle's own. Each such cycle is a linear condition with integer
1885/// coefficients on the classes' log ratios, so the ratios are moved to
1886/// the nearest that meet them all, by least squares over independent
1887/// ones. Conditions that depend on one another are met where all of them
1888/// are an odd corner's, their sums all nought; otherwise, or where the
1889/// integers outgrow their type, every class in a condition fails.
1890fn close_cycles(
1891 corners: &[Related],
1892 asked: &BTreeMap<usize, Vec<Asked>>,
1893 logs: &mut [Option<f64>],
1894) -> Vec<bool> {
1895 let mut failed = vec![false; logs.len()];
1896 // Each cycle's condition: the coefficients, the value they sum to,
1897 // and whether it is an even corner's.
1898 let mut conditions: Vec<(BTreeMap<usize, i64>, f64, bool)> = Vec::new();
1899 for (&k, links) in asked {
1900 let Some(relation) = &corners[k].relation else {
1901 continue;
1902 };
1903 let n = relation.flat.len();
1904 let even = n.is_multiple_of(2);
1905 // Each scale's log against its run's root: a union-find whose
1906 // links carry the difference.
1907 let mut parent: Vec<usize> = (0..n).collect();
1908 let mut offset: Vec<Potential> = vec![(BTreeMap::new(), 0.0); n];
1909 let root = |parent: &[usize], offset: &[Potential], mut x: usize| {
1910 let mut sum: Potential = (BTreeMap::new(), 0.0);
1911 while parent[x] != x {
1912 sum = potential_add(&sum, &offset[x], 1);
1913 x = parent[x];
1914 }
1915 (x, sum)
1916 };
1917 for a in links {
1918 let (l, r) = relation.sides[a.curve];
1919 // `log a_l − log a_r = ±x − log λ`, `x` the class's log ratio.
1920 let sign = if a.odd ^ a.inverse { -1 } else { 1 };
1921 let step: Potential = (
1922 BTreeMap::from([(a.class, sign)]),
1923 -relation.weights[a.curve].0.ln(),
1924 );
1925 let ((rl, ol), (rr, or)) = (root(&parent, &offset, l), root(&parent, &offset, r));
1926 let gap = potential_add(&potential_add(&ol, &step, -1), &or, -1);
1927 if rl == rr {
1928 // `ol − or − step` is nought: its coefficients against
1929 // minus its constant, nought round an odd corner.
1930 conditions.push((gap.0, if even { -gap.1 } else { 0.0 }, even));
1931 } else {
1932 parent[rr] = rl;
1933 offset[rr] = gap;
1934 }
1935 }
1936 }
1937 if conditions.is_empty() {
1938 return failed;
1939 }
1940 let fail_all = |failed: &mut Vec<bool>| {
1941 for (row, _, _) in &conditions {
1942 for &f in row.keys() {
1943 failed[f] = true;
1944 }
1945 }
1946 };
1947 // The independent conditions, by fraction-free elimination over the
1948 // integers: each kept row with its pivot column, zero in the pivot
1949 // columns of the rows kept before it.
1950 let columns = logs.len();
1951 let mut kept: Vec<(usize, Vec<i128>)> = Vec::new();
1952 let mut independent = Vec::new();
1953 let mut dependent = false;
1954 for (index, (row, _, _)) in conditions.iter().enumerate() {
1955 let mut dense = vec![0i128; columns];
1956 for (&f, &c) in row {
1957 dense[f] = i128::from(c);
1958 }
1959 for (pivot, base) in &kept {
1960 let x = dense[*pivot];
1961 if x == 0 {
1962 continue;
1963 }
1964 let y = base[*pivot];
1965 let mut divisor = 0i128;
1966 for (d, &b) in dense.iter_mut().zip(base) {
1967 let Some(v) = d
1968 .checked_mul(y)
1969 .and_then(|v| v.checked_sub(b.checked_mul(x)?))
1970 else {
1971 fail_all(&mut failed);
1972 return failed;
1973 };
1974 *d = v;
1975 divisor = gcd(divisor, v);
1976 }
1977 if divisor > 1 {
1978 for d in &mut dense {
1979 *d /= divisor;
1980 }
1981 }
1982 }
1983 match dense.iter().position(|&d| d != 0) {
1984 Some(pivot) => {
1985 kept.push((pivot, dense));
1986 independent.push(index);
1987 }
1988 None => dependent = true,
1989 }
1990 }
1991 if dependent && conditions.iter().any(|c| c.2) {
1992 fail_all(&mut failed);
1993 return failed;
1994 }
1995 // The nearest log ratios meeting the independent conditions `C x = d`:
1996 // `x − Cᵀ (C Cᵀ)⁻¹ (C x − d)`.
1997 let rows: Vec<&(BTreeMap<usize, i64>, f64, bool)> =
1998 independent.iter().map(|&i| &conditions[i]).collect();
1999 let value = |f: usize| logs[f].unwrap_or(0.0);
2000 let mut gram = Lower::default();
2001 for (i, a) in rows.iter().enumerate() {
2002 for (j, b) in rows.iter().enumerate().take(i + 1) {
2003 let dot: i64 =
2004 a.0.iter()
2005 .filter_map(|(f, x)| b.0.get(f).map(|y| x * y))
2006 .sum();
2007 gram.add(i, j, precise_signed(dot));
2008 }
2009 }
2010 let residual: Vec<f64> = rows
2011 .iter()
2012 .map(|(row, d, _)| {
2013 row.iter()
2014 .map(|(&f, &x)| precise_signed(x) * value(f))
2015 .sum::<f64>()
2016 - d
2017 })
2018 .collect();
2019 let Some(solved) = solve_spd(rows.len(), &gram, &[residual]) else {
2020 fail_all(&mut failed);
2021 return failed;
2022 };
2023 let mut shift: BTreeMap<usize, f64> = BTreeMap::new();
2024 for ((row, _, _), y) in rows.iter().zip(&solved[0]) {
2025 for (&f, &x) in row {
2026 *shift.entry(f).or_insert(0.0) += precise_signed(x) * y;
2027 }
2028 }
2029 for (f, moved) in shift {
2030 if let Some(log) = logs[f].as_mut() {
2031 *log -= moved;
2032 }
2033 }
2034 failed
2035}
2036
2037/// The greatest common divisor of two integers' magnitudes.
2038fn gcd(a: i128, b: i128) -> i128 {
2039 let (mut a, mut b) = (a.abs(), b.abs());
2040 while b != 0 {
2041 (a, b) = (b, a % b);
2042 }
2043 a
2044}
2045
2046/// A small signed count as a float.
2047#[allow(clippy::cast_precision_loss, reason = "coefficients far below 2^52")]
2048fn precise_signed(n: i64) -> f64 {
2049 n as f64
2050}
2051
2052/// `γ` at curve `c`'s start, where `starts`, or its end, for a corner
2053/// there whose relation gives the curve `g`: `T` there is `3 / h` times
2054/// the first pole's difference from the corner, `h` the knot span there,
2055/// and the other way about at the end.
2056fn end_gamma(layout: &Layout, c: usize, g: f64, knots: &KnotVector, starts: bool) -> f64 {
2057 let t = knots.knots();
2058 let n = layout.along(c);
2059 if starts {
2060 g * (t[DEGREE + 1] - t[DEGREE]) / precise(DEGREE)
2061 } else {
2062 -g * (t[n] - t[n - 1]) / precise(DEGREE)
2063 }
2064}
2065
2066/// Two points inside each span of `knots` and the end away from the
2067/// corner, at its start where `starts`.
2068fn along_points(knots: &KnotVector, starts: bool) -> Vec<f64> {
2069 let mut at = Vec::new();
2070 for w in knots.distinct().windows(2) {
2071 let (a, b) = (w[0].0, w[1].0);
2072 at.push((b - a).mul_add(1.0 / 3.0, a));
2073 at.push((b - a).mul_add(2.0 / 3.0, a));
2074 }
2075 at.push(if starts { 1.0 } else { 0.0 });
2076 at
2077}
2078
2079/// `γ₁` in `γ = (1 − s) γ₀ + s γ₁`, `γ₀` being `near`: the least squares
2080/// value over `terms`, read in the fit `loose`.
2081fn least_far(loose: &[Point], terms: &[(Weighted, Weighted, f64)], near: f64) -> f64 {
2082 let sum = |row: &[(usize, f64)]| {
2083 row.iter()
2084 .fold(Vector::ZERO, |acc, &(s, w)| acc + loose[s].to_vector() * w)
2085 };
2086 let (mut num, mut den) = (0.0, 0.0);
2087 for (cross, slope, share) in terms {
2088 let tangent = sum(slope);
2089 let rest = sum(cross) + tangent * ((1.0 - share) * near);
2090 let along = tangent * *share;
2091 num -= rest.dot(along);
2092 den += along.dot(along);
2093 }
2094 if den > 0.0 { num / den } else { near }
2095}
2096
2097/// The row `cross + gamma slope`, its slots merged.
2098fn gamma_row(cross: &[(usize, f64)], slope: &[(usize, f64)], gamma: f64) -> Row {
2099 let mut row: BTreeMap<usize, f64> = BTreeMap::new();
2100 for &(s, w) in cross {
2101 *row.entry(s).or_insert(0.0) += w;
2102 }
2103 for &(s, w) in slope {
2104 *row.entry(s).or_insert(0.0) += gamma * w;
2105 }
2106 row.into_iter().filter(|t| t.1 != 0.0).collect()
2107}
2108
2109/// The rows holding curve `c` `C²` across each doubled knot inside it,
2110/// `knots` its knots: its second derivative the same either side, scaled
2111/// by the square of the width of the span before.
2112fn smooth_rows(layout: &Layout, topo: &Topology, c: usize, knots: &KnotVector) -> Vec<Row> {
2113 let t = knots.knots();
2114 let distinct = knots.distinct();
2115 let mut rows = Vec::new();
2116 let mut first = 0;
2117 for (k, &(x, m)) in distinct.iter().enumerate() {
2118 if k > 0 && k + 1 < distinct.len() && m == 2 {
2119 let (left, right) = (first - 1, first + 1);
2120 let width = x - t[left];
2121 let scale = width * width;
2122 let mut row: BTreeMap<usize, f64> = BTreeMap::new();
2123 for (span, sign) in [(left, scale), (right, -scale)] {
2124 let bend = knots.basis_derivatives(span, x, 2);
2125 for (i, d) in bend[2].iter().enumerate() {
2126 *row.entry(layout.curve_slot(topo, c, span - DEGREE + i))
2127 .or_insert(0.0) += sign * d;
2128 }
2129 }
2130 rows.push(row.into_iter().filter(|e| e.1 != 0.0).collect());
2131 }
2132 first += m;
2133 }
2134 rows
2135}
2136
2137impl Fit<'_> {
2138 /// Fit from [`NetworkOptions::spans`] per strip, raising them while
2139 /// a point misses, as [`fit_network`] describes; the spans, the fit and
2140 /// why refinement stopped.
2141 fn refine(&self, options: &NetworkOptions) -> OgeomResult<(Vec<usize>, Round, Ended)> {
2142 let spans = vec![options.spans; self.topo.strips];
2143 let current = self.round(&spans, &self.taken.at)?;
2144 self.refine_from(options, spans, current, None)
2145 }
2146
2147 /// [`refine`](Self::refine) from the fit `current` at `spans`, the
2148 /// points where `self` takes them; the noise's variance in Mallows'
2149 /// criterion read off each fit's median distance, or held at `held`.
2150 fn refine_from(
2151 &self,
2152 options: &NetworkOptions,
2153 mut spans: Vec<usize>,
2154 mut current: Round,
2155 held: Option<f64>,
2156 ) -> OgeomResult<(Vec<usize>, Round, Ended)> {
2157 let (topo, tolerance) = (self.topo, options.tolerance);
2158 let ended = loop {
2159 if current.distances.iter().all(|d| *d <= tolerance) {
2160 break Ended::Met;
2161 }
2162 // The patches that miss; where the variance is held, every
2163 // patch a point misses, strays and all.
2164 let patches: Vec<usize> = if held.is_some() {
2165 (0..self.patches.len())
2166 .filter(|&p| {
2167 self.taken.of_patch[p]
2168 .iter()
2169 .any(|&i| current.distances[i] > tolerance)
2170 })
2171 .collect()
2172 } else {
2173 let (missed, any, strays) =
2174 self.missed(¤t, tolerance, (DEGREE + 1) * (DEGREE + 1));
2175 if !any && strays {
2176 break Ended::Strays;
2177 }
2178 missed.iter().map(|&(_, p)| p).collect()
2179 };
2180 // The strips of every patch that misses, each tried alone: a
2181 // patch can miss for a neighbour too coarse to follow the
2182 // shape, which its tangency binds it to.
2183 let mut tries: Vec<usize> = patches
2184 .iter()
2185 .flat_map(|&p| topo.patch_strips[p].iter().flatten().copied())
2186 .filter(|&s| spans[s] < options.most)
2187 .collect();
2188 tries.sort_unstable();
2189 tries.dedup();
2190 if tries.is_empty() {
2191 break Ended::Bounds;
2192 }
2193 // Mallows' criterion, as the single-patch refinement scores:
2194 // the squared distances plus twice the noise's variance per
2195 // free pole, the variance read off the median distance unless
2196 // held.
2197 let variance =
2198 held.unwrap_or_else(|| (median(¤t.distances) / NORMAL_QUARTILE).powi(2));
2199 let score = |r: &Round| 2.0f64.mul_add(variance * precise(r.free), r.square());
2200 let mut best: Option<(f64, Vec<usize>, Round)> = None;
2201 for raised in tries {
2202 let mut next = spans.clone();
2203 next[raised] = (2 * next[raised]).min(options.most);
2204 let tried = self.round(&next, &self.taken.at)?;
2205 let scored = score(&tried);
2206 if best.as_ref().is_none_or(|(s, _, _)| scored < *s) {
2207 best = Some((scored, next, tried));
2208 }
2209 }
2210 match best {
2211 Some((scored, next, tried)) if scored < score(¤t) => {
2212 spans = next;
2213 current = tried;
2214 }
2215 _ => break Ended::Noise,
2216 }
2217 };
2218 Ok((spans, current, ended))
2219 }
2220
2221 /// Fit over `spans` per strip, the points at `at`.
2222 fn round(&self, spans: &[usize], at: &[(f64, f64)]) -> OgeomResult<Round> {
2223 let topo = self.topo;
2224 let doubled = self.doubled();
2225 let layout = Layout::new(topo, spans, &doubled);
2226 let mut patch_knots = Vec::with_capacity(self.patches.len());
2227 let mut patch_bounds = Vec::with_capacity(self.patches.len());
2228 for ([su, sv], [fu, fv]) in topo.patch_strips.iter().zip(&topo.patch_flip) {
2229 let (ku, bu) = run_knots(su, fu, spans, self.weights, &doubled)?;
2230 let (kv, bv) = run_knots(sv, fv, spans, self.weights, &doubled)?;
2231 patch_knots.push([ku, kv]);
2232 patch_bounds.push([bu, bv]);
2233 }
2234 let curve_knots: Vec<KnotVector> = topo
2235 .curve_strip
2236 .iter()
2237 .zip(&topo.curve_flip)
2238 .map(|(strips, flips)| {
2239 run_knots(strips, flips, spans, self.weights, &doubled).map(|k| k.0)
2240 })
2241 .collect::<OgeomResult<_>>()?;
2242 let locals: Vec<Local> = ogeom_core::parallel::map_ordered(
2243 &(0..self.patches.len()).collect::<Vec<_>>(),
2244 |_, &p| self.local(p, at, &patch_knots[p]),
2245 );
2246
2247 // Without the tangency, for the ratios across each curve; the
2248 // patches held smooth across their junctions all the same.
2249 let (junctions, lines) = self.junction_rows(&layout, spans);
2250 let known = self.held_poles(&layout, &curve_knots)?;
2251 let loose_basis = if !known.is_empty() {
2252 self.basis_known(
2253 &layout,
2254 &junctions.values().cloned().collect::<Vec<_>>(),
2255 &known,
2256 )?
2257 } else if junctions.is_empty() {
2258 Basis::identity(layout.slots)
2259 } else {
2260 null_space(
2261 layout.slots,
2262 &junctions.values().cloned().collect::<Vec<_>>(),
2263 )
2264 };
2265 let loose = self.solve(&layout, &locals, &loose_basis)?;
2266 let junction_rows = |relaxed: &Keyed| -> Vec<Row> {
2267 junctions
2268 .iter()
2269 .map(|(key, row)| relaxed.get(key).unwrap_or(row).clone())
2270 .collect()
2271 };
2272 let mut rows: Vec<Row>;
2273 let mut held = true;
2274 let triples;
2275 match self.corners {
2276 Corners::Loose => {
2277 let held = self.tangency(&layout, &loose, &junctions, &lines, &mut [], &known)?;
2278 rows = junction_rows(&held.relaxed);
2279 rows.extend(held.rows);
2280 triples = held.held;
2281 }
2282 Corners::Whole => {
2283 let held = self.tangency(&layout, &loose, &junctions, &lines, &mut [], &known)?;
2284 rows = junction_rows(&held.relaxed);
2285 rows.extend(held.rows);
2286 triples = held.held;
2287 rows.extend(self.corner_rows(&layout, &loose, &curve_knots));
2288 }
2289 Corners::Spread => {
2290 let tangent =
2291 self.tangency(&layout, &loose, &junctions, &lines, &mut [], &known)?;
2292 rows = junction_rows(&tangent.relaxed);
2293 rows.extend(tangent.rows);
2294 triples = tangent.held;
2295 match self.spread_rows(&layout, &loose, &curve_knots) {
2296 Some(spread) => rows.extend(spread),
2297 None => held = false,
2298 }
2299 }
2300 Corners::Spans => {
2301 let mut related: Vec<Related> = self
2302 .topo
2303 .span_corners
2304 .iter()
2305 .map(|(v, curves)| Related {
2306 vertex: *v,
2307 curves: curves.clone(),
2308 relation: self.relate(&layout, &loose, *v, curves),
2309 })
2310 .collect();
2311 let held =
2312 self.tangency(&layout, &loose, &junctions, &lines, &mut related, &known)?;
2313 rows = junction_rows(&held.relaxed);
2314 rows.extend(held.rows);
2315 triples = held.held;
2316 let untied = held.untied;
2317 let holding = &self.topo.spanned;
2318 for corner in &related {
2319 let Some(relation) = &corner.relation else {
2320 continue;
2321 };
2322 let tied = corner
2323 .curves
2324 .iter()
2325 .all(|&c| holding.chain[c].is_some_and(|(chain, _)| !untied[chain]));
2326 if !tied {
2327 continue;
2328 }
2329 rows.extend(relation.rows());
2330 for (i, &c) in corner.curves.iter().enumerate() {
2331 rows.extend(self.span_rows(
2332 &layout,
2333 c,
2334 corner.vertex,
2335 relation.weights[i],
2336 &curve_knots[c],
2337 ));
2338 }
2339 }
2340 }
2341 }
2342 if self.fans {
2343 match self.fan_rows(&layout, &loose, &triples) {
2344 Some(fan) => rows.extend(fan),
2345 None => held = false,
2346 }
2347 }
2348 let known = self.across_poles(&layout, &loose, known);
2349 let basis = if known.is_empty() {
2350 null_space(layout.slots, &rows)
2351 } else {
2352 self.basis_known(&layout, &rows, &known)?
2353 };
2354 let poles = self.solve(&layout, &locals, &basis)?;
2355 if self.fans && held {
2356 held = self.fans_regular(&layout, &patch_knots, &poles)?;
2357 }
2358
2359 let mut surfaces = Vec::with_capacity(self.patches.len());
2360 for (p, [ku, kv]) in patch_knots.iter().enumerate() {
2361 let (nu, nv) = layout.dims(p);
2362 let mut net = Vec::with_capacity(nu * nv);
2363 for i in 0..nu {
2364 for j in 0..nv {
2365 net.push(poles[layout.slot(topo, self.patches, p, (i, j))]);
2366 }
2367 }
2368 let grid = ControlGrid::new(net, nu, nv)?;
2369 surfaces.push(BSplineSurface::new(
2370 ku.clone(),
2371 kv.clone(),
2372 &grid,
2373 self.tol,
2374 )?);
2375 }
2376 let geometry: Vec<SurfaceGeometry> = surfaces
2377 .iter()
2378 .map(|s| SurfaceGeometry::BSpline(s.clone()))
2379 .collect();
2380 let taken = self.taken;
2381 let starts: Vec<usize> = (0..taken.point.len()).step_by(POINT_BATCH).collect();
2382 let found = ogeom_core::parallel::map_ordered(&starts, |_, &start| {
2383 (start..(start + POINT_BATCH).min(taken.point.len()))
2384 .map(|i| {
2385 project_on_surface_from(
2386 &geometry[taken.patch[i]],
2387 taken.point[i],
2388 at[i],
2389 self.tol,
2390 )
2391 })
2392 .collect::<OgeomResult<Vec<_>>>()
2393 });
2394 let mut feet = Vec::with_capacity(taken.point.len());
2395 let mut distances = Vec::with_capacity(taken.point.len());
2396 for batch in found {
2397 for foot in batch? {
2398 feet.push(foot.parameters);
2399 distances.push(foot.distance);
2400 }
2401 }
2402 Ok(Round {
2403 layout,
2404 patch_knots,
2405 patch_bounds,
2406 curve_knots,
2407 poles,
2408 surfaces,
2409 free: basis.free,
2410 feet,
2411 distances,
2412 held,
2413 })
2414 }
2415
2416 /// The rows holding each patch `C¹` across the parameter line through
2417 /// the junctions of its split sides, along every row of poles across
2418 /// it: the pole at the junction's knot and its neighbours either side
2419 /// on one line, `(h₁ + h₂) J − h₂ P − h₁ Q = 0`, `h₁` and `h₂` the knot
2420 /// spacings either side. A row two patches hold alike (along the curve
2421 /// carrying the junction between them) is kept once: the spacings are
2422 /// each strip's weight over its spans, the same numbers from either
2423 /// patch.
2424 ///
2425 /// Also the lines the rows lie on: a patch's rows across one junction
2426 /// are one line, and lines sharing a row are one.
2427 fn junction_rows(&self, layout: &Layout, spans: &[usize]) -> (Keyed, Lines) {
2428 let topo = self.topo;
2429 let spacing = |s: usize| self.weights[s] / precise(spans[s]);
2430 let mut rows = Keyed::new();
2431 // Each row's first patch line and the pole before its middle there.
2432 let mut owner: BTreeMap<(usize, usize, usize), (usize, usize)> = BTreeMap::new();
2433 let mut joined = Vec::new();
2434 let mut patch_lines = 0;
2435 for p in 0..self.patches.len() {
2436 let (nu, nv) = layout.dims(p);
2437 for (dir, strips) in topo.patch_strips[p].iter().enumerate() {
2438 let across = if dir == 0 { nv } else { nu };
2439 let mut at = 0;
2440 for pair in strips.windows(2) {
2441 at += layout.controls[pair[0]] - 1;
2442 let (h1, h2) = (spacing(pair[0]), spacing(pair[1]));
2443 let line = patch_lines;
2444 patch_lines += 1;
2445 for j in 0..across {
2446 let slot = |i: usize| {
2447 let pole = if dir == 0 { (i, j) } else { (j, i) };
2448 layout.slot(topo, self.patches, p, pole)
2449 };
2450 let (mid, before, after) = (slot(at), slot(at - 1), slot(at + 1));
2451 // A row shrunk to a missing side's point holds
2452 // nothing.
2453 if mid == before || mid == after || before == after {
2454 continue;
2455 }
2456 let key = (mid, before.min(after), before.max(after));
2457 match owner.get(&key) {
2458 Some(&(first, first_before)) => {
2459 joined.push((first, line, first_before != before));
2460 }
2461 None => {
2462 owner.insert(key, (line, before));
2463 }
2464 }
2465 rows.entry(key)
2466 .or_insert_with(|| vec![(mid, h1 + h2), (before, -h2), (after, -h1)]);
2467 }
2468 }
2469 }
2470 }
2471 let mut classes = topology::Classes::new(patch_lines);
2472 let mut torn_lines = Vec::new();
2473 for (a, b, odd) in joined {
2474 if !classes.join(a, b, odd) {
2475 torn_lines.push(a);
2476 }
2477 }
2478 let (named, count) = classes.numbered(0..patch_lines);
2479 let mut torn = vec![false; count];
2480 for line in torn_lines {
2481 if let Some(id) = named[line] {
2482 torn[id] = true;
2483 }
2484 }
2485 let of = owner
2486 .into_iter()
2487 .map(|(key, (line, _))| {
2488 let flip = classes.find(line).1;
2489 (key, (named[line].unwrap_or(0), flip))
2490 })
2491 .collect();
2492 (rows, Lines { of, count, torn })
2493 }
2494
2495 /// Patch `p`'s normal equations over its own net.
2496 fn local(&self, p: usize, at: &[(f64, f64)], [ku, kv]: &[KnotVector; 2]) -> Local {
2497 let (nu, nv) = (ku.control_point_count(), kv.control_point_count());
2498 let band = DEGREE * nv + DEGREE;
2499 let mut lower = vec![vec![0.0; band + 1]; nu * nv];
2500 let mut rhs = vec![[0.0; 3]; nu * nv];
2501 let mine = &self.taken.of_patch[p];
2502 if !mine.is_empty() {
2503 let share = 1.0 / precise(mine.len());
2504 let mut terms: Vec<(usize, f64)> = Vec::with_capacity((DEGREE + 1) * (DEGREE + 1));
2505 for &i in mine {
2506 let (u, v) = at[i];
2507 let q = self.taken.point[i];
2508 let (us, vs) = (ku.span_unchecked(u), kv.span_unchecked(v));
2509 let (bu, bv) = (ku.basis(us, u), kv.basis(vs, v));
2510 terms.clear();
2511 for (a, x) in bu.iter().enumerate() {
2512 for (b, y) in bv.iter().enumerate() {
2513 terms.push(((us - DEGREE + a) * nv + vs - DEGREE + b, x * y));
2514 }
2515 }
2516 for (k, &(r, x)) in terms.iter().enumerate() {
2517 rhs[r][0] += share * x * q.x;
2518 rhs[r][1] += share * x * q.y;
2519 rhs[r][2] += share * x * q.z;
2520 for &(c, y) in &terms[..=k] {
2521 let (r, c) = if r >= c { (r, c) } else { (c, r) };
2522 lower[r][r - c] += share * x * y;
2523 }
2524 }
2525 }
2526 }
2527 if self.smoothing > 0.0 {
2528 let (gu, gv) = (derivative_grams(ku), derivative_grams(kv));
2529 for a in 0..nu {
2530 for b in 0..nv {
2531 let r = a * nv + b;
2532 for c in a.saturating_sub(DEGREE)..=a {
2533 for d in b.saturating_sub(DEGREE)..(b + DEGREE + 1).min(nv) {
2534 let col = c * nv + d;
2535 if col > r {
2536 continue;
2537 }
2538 let (f, x) = (a.min(c), a.abs_diff(c));
2539 let (e, y) = (b.min(d), b.abs_diff(d));
2540 let energy = gu[2][f][x] * gv[0][e][y]
2541 + 2.0 * gu[1][f][x] * gv[1][e][y]
2542 + gu[0][f][x] * gv[2][e][y];
2543 lower[r][r - col] += self.smoothing * energy;
2544 }
2545 }
2546 }
2547 }
2548 }
2549 Local {
2550 nv,
2551 band,
2552 lower,
2553 rhs,
2554 }
2555 }
2556
2557 /// Solve the patches' equations together over the free unknowns of
2558 /// `basis`; every slot's pole.
2559 fn solve(&self, layout: &Layout, locals: &[Local], basis: &Basis) -> OgeomResult<Vec<Point>> {
2560 let mut normal = Lower::default();
2561 let mut rhs = vec![vec![0.0; basis.free]; 3];
2562 for (p, local) in locals.iter().enumerate() {
2563 let slot_of =
2564 |r: usize| layout.slot(self.topo, self.patches, p, (r / local.nv, r % local.nv));
2565 // With prescribed slots the least squares is over the free part
2566 // alone: each patch's right-hand side less its normal matrix
2567 // times the prescribed constants.
2568 let shifted: Option<Vec<[f64; 3]>> = basis.offset.as_ref().map(|offset| {
2569 let mut shifted = local.rhs.clone();
2570 for (r, row) in local.lower.iter().enumerate() {
2571 for (d, &value) in row.iter().enumerate().take(local.band.min(r) + 1) {
2572 if value == 0.0 {
2573 continue;
2574 }
2575 let (cr, cc) = (offset[slot_of(r)], offset[slot_of(r - d)]);
2576 for axis in 0..3 {
2577 shifted[r][axis] -= value * cc[axis];
2578 if d > 0 {
2579 shifted[r - d][axis] -= value * cr[axis];
2580 }
2581 }
2582 }
2583 }
2584 shifted
2585 });
2586 let local_rhs = shifted.as_ref().unwrap_or(&local.rhs);
2587 for (r, row) in local.lower.iter().enumerate() {
2588 let sr = &basis.of_slot[slot_of(r)];
2589 for (y, z) in sr {
2590 for (side, value) in rhs.iter_mut().zip(local_rhs[r]) {
2591 side[*y] += z * value;
2592 }
2593 }
2594 for (d, &value) in row.iter().enumerate().take(local.band.min(r) + 1) {
2595 if value == 0.0 {
2596 continue;
2597 }
2598 if d == 0 {
2599 for &(y1, z1) in sr {
2600 for &(y2, z2) in sr {
2601 normal.add(y1, y2, z1 * z2 * value);
2602 }
2603 }
2604 } else {
2605 let sc = &basis.of_slot[slot_of(r - d)];
2606 for &(y1, z1) in sr {
2607 for &(y2, z2) in sc {
2608 normal.add(y1, y2, z1 * z2 * value);
2609 normal.add(y2, y1, z1 * z2 * value);
2610 }
2611 }
2612 }
2613 }
2614 }
2615 }
2616 let Some(solved) = solve_spd(basis.free, &normal, &rhs) else {
2617 ogeom_bail!(
2618 Numeric,
2619 "the network's system is singular; raise the smoothing so the fairing settles \
2620 the poles no point reaches"
2621 );
2622 };
2623 Ok(basis
2624 .of_slot
2625 .iter()
2626 .enumerate()
2627 .map(|(s, combo)| {
2628 let mut p = [0.0; 3];
2629 for &(y, z) in combo {
2630 for (axis, value) in p.iter_mut().enumerate() {
2631 *value += z * solved[axis][y];
2632 }
2633 }
2634 if let Some(offset) = &basis.offset {
2635 for (value, constant) in p.iter_mut().zip(offset[s]) {
2636 *value += constant;
2637 }
2638 }
2639 Point::new(p[0], p[1], p[2])
2640 })
2641 .collect())
2642 }
2643
2644 /// The slots the held curves prescribe at this round's knots: each
2645 /// curve's end poles at the corners, the poles beside them set by the
2646 /// caller's curve's own derivative at its ends (so two held curves
2647 /// running on through a point leave it along one line), and the rest
2648 /// the least-squares projection of the caller's curve on `knots[c]`,
2649 /// read at the Gauss nodes of every span.
2650 fn held_poles(
2651 &self,
2652 layout: &Layout,
2653 knots: &[KnotVector],
2654 ) -> OgeomResult<Vec<(usize, [f64; 3])>> {
2655 let mut known: Vec<(usize, [f64; 3])> = Vec::new();
2656 for (c, held) in self.held.iter().enumerate() {
2657 let Some(held) = held else {
2658 continue;
2659 };
2660 let k = &knots[c];
2661 let t = k.knots();
2662 let n = layout.along(c);
2663 let (a, b) = self.topo.curve_ends[c];
2664 let p = precise(DEGREE);
2665 // The fixed poles: the ends, and beside them the poles the
2666 // end derivatives set, `C'(0) = p (P₁ − P₀) / (t_{p+1} − t₁)`.
2667 let mut fixed: Vec<Option<Point>> = vec![None; n];
2668 let (start, end) = (self.vertex_points[a], self.vertex_points[b]);
2669 fixed[0] = Some(start);
2670 fixed[n - 1] = Some(end);
2671 fixed[1] = Some(start + held.d1(0.0, self.tol)? * ((t[DEGREE + 1] - t[1]) / p));
2672 fixed[n - 2] =
2673 Some(end - held.d1(1.0, self.tol)? * ((t[n - 1 + DEGREE] - t[n - 1]) / p));
2674 let free: Vec<usize> = (0..n).filter(|&q| fixed[q].is_none()).collect();
2675 let row_of = |q: usize| free.iter().position(|&f| f == q);
2676 let mut gram = vec![vec![0.0; free.len()]; free.len()];
2677 let mut rhs = vec![[0.0; 3]; free.len()];
2678 if !free.is_empty() {
2679 for span in DEGREE..t.len() - DEGREE - 1 {
2680 let (lo, hi) = (t[span], t[span + 1]);
2681 if hi <= lo {
2682 continue;
2683 }
2684 let (mid, half) = (f64::midpoint(lo, hi), (hi - lo) / 2.0);
2685 for (x, w) in GAUSS {
2686 let u = half.mul_add(x, mid);
2687 let basis = k.basis(span, u);
2688 let mut residual = held.at(u, self.tol)?.to_vector();
2689 for (i, value) in basis.iter().enumerate() {
2690 if let Some(pole) = fixed[span - DEGREE + i] {
2691 residual -= pole.to_vector() * *value;
2692 }
2693 }
2694 let weight = w * half;
2695 for (i, vi) in basis.iter().enumerate() {
2696 let Some(row) = row_of(span - DEGREE + i) else {
2697 continue;
2698 };
2699 rhs[row][0] += weight * vi * residual.x;
2700 rhs[row][1] += weight * vi * residual.y;
2701 rhs[row][2] += weight * vi * residual.z;
2702 for (j, vj) in basis.iter().enumerate() {
2703 if let Some(column) = row_of(span - DEGREE + j) {
2704 gram[row][column] += weight * vi * vj;
2705 }
2706 }
2707 }
2708 }
2709 }
2710 }
2711 let solved = solve_dense(gram, rhs).ok_or_else(|| {
2712 ogeom_core::ogeom_err!(
2713 Numeric,
2714 "held curve {c} does not settle its poles on its knots"
2715 )
2716 })?;
2717 for (q, pole) in fixed.iter().enumerate() {
2718 if let Some(pole) = pole {
2719 known.push((layout.curve_slot(self.topo, c, q), [pole.x, pole.y, pole.z]));
2720 }
2721 }
2722 for (&q, pole) in free.iter().zip(solved) {
2723 known.push((layout.curve_slot(self.topo, c, q), pole));
2724 }
2725 }
2726 // A corner two held curves share is prescribed once.
2727 known.sort_by_key(|&(slot, _)| slot);
2728 known.dedup_by_key(|&mut (slot, _)| slot);
2729 Ok(known)
2730 }
2731
2732 /// `known` with the poles one row in from each curve held across
2733 /// prescribed too: each moved from where the fit without them put it to
2734 /// the nearest point at which its step off every pole of a held curve
2735 /// beside it goes as that curve's [`Across`] says. A pole a held curve
2736 /// already prescribes stays as it is.
2737 fn across_poles(
2738 &self,
2739 layout: &Layout,
2740 loose: &[Point],
2741 mut known: Vec<(usize, [f64; 3])>,
2742 ) -> Vec<(usize, [f64; 3])> {
2743 if self.across.iter().all(Option::is_none) {
2744 return known;
2745 }
2746 // Each inner slot's conditions: unit rows `a·P = a·B`.
2747 let mut asked: BTreeMap<usize, Vec<(Vector, Point)>> = BTreeMap::new();
2748 for (p, patch) in self.patches.iter().enumerate() {
2749 let dims = layout.dims(p);
2750 for (k, side) in patch.sides.iter().enumerate() {
2751 let mut offset = 0;
2752 for piece in side {
2753 let n = layout.along(piece.curve);
2754 if let Some(how) = self.across[piece.curve] {
2755 for w in offset..offset + n {
2756 let (base, inner) = side_pole(k, w, dims);
2757 let (bs, is) = (
2758 layout.slot(self.topo, self.patches, p, base),
2759 layout.slot(self.topo, self.patches, p, inner),
2760 );
2761 let b = loose[bs];
2762 let rows = asked.entry(is).or_default();
2763 match how {
2764 Across::Plane(normal) => {
2765 rows.push((normal / normal.magnitude(), b));
2766 }
2767 Across::Along(along) => {
2768 let d = along / along.magnitude();
2769 let pick = if d.x.abs() < 0.9 {
2770 Vector::new(1.0, 0.0, 0.0)
2771 } else {
2772 Vector::new(0.0, 1.0, 0.0)
2773 };
2774 let e1 = d.cross(pick);
2775 let e1 = e1 / e1.magnitude();
2776 rows.push((e1, b));
2777 rows.push((d.cross(e1), b));
2778 }
2779 }
2780 }
2781 }
2782 offset += n - 1;
2783 }
2784 }
2785 }
2786 // A pole of a curve two patches share is placed by that curve's
2787 // pole triples, which keep it on the conditions its neighbours
2788 // meet; it is not held twice.
2789 let shared = |slot: usize| -> bool {
2790 (0..self.topo.users.len()).any(|c| {
2791 let base = layout.curve_base[c];
2792 self.topo.users[c].len() == 2 && (base..base + layout.along(c) - 2).contains(&slot)
2793 })
2794 };
2795 for (slot, rows) in asked {
2796 if known.iter().any(|&(s, _)| s == slot) || shared(slot) {
2797 continue;
2798 }
2799 // The nearest point meeting every row: each row made
2800 // orthogonal to those before it, a row that repeats them
2801 // dropped, and the point moved along each in turn.
2802 let mut p = loose[slot];
2803 let mut basis: Vec<(Vector, f64)> = Vec::new();
2804 for (a, b) in rows {
2805 let mut a = a;
2806 let mut target = a.dot(b.to_vector());
2807 for &(q, t) in &basis {
2808 let along = a.dot(q);
2809 a -= q * along;
2810 target -= along * t;
2811 }
2812 let length = a.magnitude();
2813 if length <= self.tol.angular().sqrt() {
2814 continue;
2815 }
2816 let (a, target) = (a / length, target / length);
2817 basis.push((a, target));
2818 p += a * (target - a.dot(p.to_vector()));
2819 }
2820 known.push((slot, [p.x, p.y, p.z]));
2821 }
2822 known.sort_by_key(|&(slot, _)| slot);
2823 known
2824 }
2825
2826 /// The basis over `rows` with the held slots prescribed, refused by
2827 /// name where a row asks the held curves for what they do not hold.
2828 fn basis_known(
2829 &self,
2830 layout: &Layout,
2831 rows: &[Row],
2832 known: &[(usize, [f64; 3])],
2833 ) -> OgeomResult<Basis> {
2834 null_space_known(layout.slots, rows, known).ok_or_else(|| {
2835 ogeom_core::ogeom_err!(
2836 Construction,
2837 "a held curve breaks a condition the network holds across it"
2838 )
2839 })
2840 }
2841
2842 /// The curves held by the pole triples, as the corners and fans are
2843 /// held.
2844 fn holding(&self) -> &topology::Holding {
2845 match (self.corners == Corners::Spans, self.fans) {
2846 (true, false) => &self.topo.spanned,
2847 (false, false) => &self.topo.plain,
2848 (true, true) => &self.topo.spanned_fans,
2849 (false, true) => &self.topo.plain_fans,
2850 }
2851 }
2852
2853 /// Which knots inside each strip are doubled: the first knot in from
2854 /// the end at each corner held by its spans, of each curve into it, and
2855 /// every knot inside the strip of each curve into a corner whose gluing
2856 /// is spread along its curves.
2857 fn doubled(&self) -> Vec<Doubled> {
2858 let topo = self.topo;
2859 let mut doubled = vec![Doubled::default(); topo.strips];
2860 if self.corners == Corners::Spans {
2861 for (v, curves) in &topo.span_corners {
2862 for &c in curves {
2863 let starts = topo.curve_ends[c].0 == *v;
2864 doubled[topo.curve_strip[c][0]].ends
2865 [usize::from(starts == topo.curve_flip[c][0])] = true;
2866 }
2867 }
2868 }
2869 if self.corners == Corners::Spread {
2870 for (_, curves) in &topo.spread_corners {
2871 for &c in curves {
2872 doubled[topo.curve_strip[c][0]].every = true;
2873 }
2874 }
2875 }
2876 doubled
2877 }
2878
2879 /// The tangency rows: for each pole of each curve held tangent, the
2880 /// pole and the poles one row into either patch on one line,
2881 /// `(1 + λ) B − L − λ R = 0`, `L` in the curve's first patch, `λ` its
2882 /// chain's ratio of pole spacings in the fit `loose`; which chains are
2883 /// left untied; and the junction rows held at a ratio of their own.
2884 ///
2885 /// A triple that is one of the `junctions` rows (a curve ending at a
2886 /// T-junction, the line on through it the patch's own) is held by that
2887 /// row, and pins its chain's ratio to the row's. A curve into one of the
2888 /// `corners` pins its chain's ratio to its own `λ` there, and its two
2889 /// poles nearest the corner are held by the corner's rows instead; a
2890 /// corner without a relation unties its curves' chains. A chain pinned
2891 /// to two ratios is left untied, its curves near tangent, but where a
2892 /// corner pins it:
2893 ///
2894 /// - the lines through the junctions it meets are held at the corner's
2895 /// ratio rather than their knot spacings', pole by pole as across a
2896 /// curve, so their patches are tangent across them though not `C¹`;
2897 /// - where it meets several corners, it takes the geometric mean of
2898 /// their ratios, moved as little as [closes](close_cycles) every
2899 /// cycle of a corner's scales asked a ratio at each of its curves, and
2900 /// each corner is [rescaled](Relation::rescale) to it.
2901 ///
2902 /// Chains and lines that pin one another are settled together, and
2903 /// where one of them cannot be, all of them are left as before.
2904 #[allow(
2905 clippy::too_many_lines,
2906 reason = "the pins, then the chains they settle"
2907 )]
2908 fn tangency(
2909 &self,
2910 layout: &Layout,
2911 loose: &[Point],
2912 junctions: &Keyed,
2913 lines: &Lines,
2914 corners: &mut [Related],
2915 known: &[(usize, [f64; 3])],
2916 ) -> OgeomResult<Tangency> {
2917 let topo = self.topo;
2918 let holding = self.holding();
2919 let prescribed = |slot: usize| known.binary_search_by_key(&slot, |&(s, _)| s).is_ok();
2920 // The poles of each curve the corners hold.
2921 let mut skipped: Vec<[bool; 2]> = vec![[false; 2]; topo.users.len()];
2922 for corner in corners.iter() {
2923 for &c in &corner.curves {
2924 skipped[c][usize::from(topo.curve_ends[c].0 != corner.vertex)] = true;
2925 }
2926 }
2927 // Each held triple: (curve, B, L, R) slots.
2928 let mut triples: Vec<(usize, usize, usize, usize)> = Vec::new();
2929 for (c, [at_start, at_end]) in skipped.into_iter().enumerate() {
2930 if !holding.held[c] {
2931 continue;
2932 }
2933 let n = layout.along(c);
2934 let beside = |user: (usize, usize), q: usize| self.beside(layout, user, c, q);
2935 for q in 0..n {
2936 if (at_start && q < 2) || (at_end && q + 2 >= n) {
2937 continue;
2938 }
2939 let (b, l, r) = (
2940 layout.curve_slot(topo, c, q),
2941 beside(topo.users[c][0], q),
2942 beside(topo.users[c][1], q),
2943 );
2944 // At a fan's point both rows beside the curve shrink to
2945 // it, and the triple holds nothing. Where held curves
2946 // prescribe all three, they already stand on one line (a
2947 // held curve leaves its ends along the caller's
2948 // derivative), and the triple would only ask them a ratio.
2949 if (b == l && b == r) || (prescribed(b) && prescribed(l) && prescribed(r)) {
2950 continue;
2951 }
2952 triples.push((c, b, l, r));
2953 }
2954 }
2955 let settled = self.settle(&triples, junctions, lines, corners);
2956
2957 // Each chain's pin, as the ratio's numerator and denominator, and
2958 // whether two pins disagree.
2959 let mut pins: Vec<Option<(f64, f64)>> = vec![None; holding.chains];
2960 let mut untied = vec![false; holding.chains];
2961 let mut pin = |chain: usize, pinned: (f64, f64)| match pins[chain] {
2962 None => pins[chain] = Some(pinned),
2963 Some(held) => {
2964 if held.0 * pinned.1 != pinned.0 * held.1 {
2965 untied[chain] = true;
2966 }
2967 }
2968 };
2969 let mut unpinned = Vec::new();
2970 for corner in corners.iter() {
2971 for (i, &c) in corner.curves.iter().enumerate() {
2972 let Some((chain, inverse)) = holding.chain[c] else {
2973 continue;
2974 };
2975 match &corner.relation {
2976 Some(relation) => {
2977 let lambda = relation.weights[i].0;
2978 pin(
2979 chain,
2980 if inverse {
2981 (1.0, lambda)
2982 } else {
2983 (lambda, 1.0)
2984 },
2985 );
2986 }
2987 None => unpinned.push(chain),
2988 }
2989 }
2990 }
2991 let mut sums = vec![(0.0, 0.0); holding.chains];
2992 for &(c, b, l, r) in &triples {
2993 let Some((chain, inverse)) = holding.chain[c] else {
2994 continue;
2995 };
2996 if let Some(row) = junctions.get(&(b, l.min(r), l.max(r))) {
2997 // The row reads `a B − x L − y R`, so `λ = y / x`.
2998 let of = |slot: usize| row.iter().find(|t| t.0 == slot).map_or(0.0, |t| -t.1);
2999 let (x, y) = (of(l), of(r));
3000 pin(chain, if inverse { (x, y) } else { (y, x) });
3001 continue;
3002 }
3003 let (near, far) = (loose[b].distance(loose[l]), loose[r].distance(loose[b]));
3004 let (num, den) = if inverse { (far, near) } else { (near, far) };
3005 sums[chain].0 += num;
3006 sums[chain].1 += den;
3007 }
3008 for chain in unpinned {
3009 untied[chain] = true;
3010 }
3011 let mut ratio = Vec::with_capacity(holding.chains);
3012 for (chain, &(num, den)) in sums.iter().enumerate() {
3013 if let Some((num, den)) = pins[chain] {
3014 // Opposite parities somewhere are served by a ratio of one
3015 // alone.
3016 if holding.conflicted[chain] && num != den {
3017 untied[chain] = true;
3018 }
3019 ratio.push(num / den);
3020 continue;
3021 }
3022 if holding.conflicted[chain] {
3023 ratio.push(1.0);
3024 continue;
3025 }
3026 let rho = num / den;
3027 if !(rho > 0.0 && rho.is_finite()) {
3028 ogeom_bail!(
3029 Numeric,
3030 "the poles either side of a tangent curve collapse onto it; raise the \
3031 smoothing"
3032 );
3033 }
3034 ratio.push(rho);
3035 }
3036
3037 // The settled classes override their chains, and a corner with a
3038 // curve untied is held by none of its rows: its curves are untied
3039 // together. A settled class that loses a chain so is unsettled, and
3040 // the corners are gone over again.
3041 let (pinned_ratio, pinned_untied) = (ratio.clone(), untied.clone());
3042 let mut kept = vec![true; settled.len()];
3043 loop {
3044 ratio.clone_from(&pinned_ratio);
3045 untied.clone_from(&pinned_untied);
3046 for (class, keep) in settled.iter().zip(&kept) {
3047 if !keep {
3048 continue;
3049 }
3050 for &(chain, odd) in &class.chains {
3051 ratio[chain] = if odd { 1.0 / class.ratio } else { class.ratio };
3052 untied[chain] = false;
3053 }
3054 }
3055 loop {
3056 let mut changed = false;
3057 for corner in corners.iter() {
3058 let chains: Vec<usize> = corner
3059 .curves
3060 .iter()
3061 .filter_map(|&c| holding.chain[c].map(|(chain, _)| chain))
3062 .collect();
3063 if chains.iter().any(|&chain| untied[chain]) {
3064 for chain in chains {
3065 changed |= !untied[chain];
3066 untied[chain] = true;
3067 }
3068 }
3069 }
3070 if !changed {
3071 break;
3072 }
3073 }
3074 let mut lost = false;
3075 for (class, keep) in settled.iter().zip(kept.iter_mut()) {
3076 if *keep && class.chains.iter().any(|&(chain, _)| untied[chain]) {
3077 *keep = false;
3078 lost = true;
3079 }
3080 }
3081 if !lost {
3082 break;
3083 }
3084 }
3085 let mut relaxed = Keyed::new();
3086 for (class, _) in settled.iter().zip(&kept).filter(|(_, keep)| **keep) {
3087 for &(line, odd) in &class.lines {
3088 let mu = if odd { 1.0 / class.ratio } else { class.ratio };
3089 for (key, &(of, flip)) in &lines.of {
3090 if of != line {
3091 continue;
3092 }
3093 let row = &junctions[key];
3094 let (mid, before, after) = if flip {
3095 (row[0].0, row[2].0, row[1].0)
3096 } else {
3097 (row[0].0, row[1].0, row[2].0)
3098 };
3099 relaxed.insert(*key, vec![(mid, 1.0 + mu), (before, -1.0), (after, -mu)]);
3100 }
3101 }
3102 }
3103 let mut held = BTreeMap::new();
3104 let rows = triples
3105 .iter()
3106 .filter_map(|&(c, b, l, r)| {
3107 let (chain, inverse) = holding.chain[c]?;
3108 if untied[chain] || junctions.contains_key(&(b, l.min(r), l.max(r))) {
3109 return None;
3110 }
3111 let lambda = if inverse {
3112 1.0 / ratio[chain]
3113 } else {
3114 ratio[chain]
3115 };
3116 held.insert(b, (l, r, lambda));
3117 Some(vec![(b, 1.0 + lambda), (l, -1.0), (r, -lambda)])
3118 })
3119 .collect();
3120 Ok(Tangency {
3121 rows,
3122 untied,
3123 relaxed,
3124 held,
3125 })
3126 }
3127
3128 /// The chains and junction lines a corner pins along with something
3129 /// else, settled on one ratio each, as [`tangency`](Self::tangency)
3130 /// describes; the corners they meet more than once are rescaled to it.
3131 fn settle(
3132 &self,
3133 triples: &[(usize, usize, usize, usize)],
3134 junctions: &Keyed,
3135 lines: &Lines,
3136 corners: &mut [Related],
3137 ) -> Vec<Settled> {
3138 let holding = self.holding();
3139 let chains = holding.chains;
3140 // Chains, then lines, each node's ratio that of its class's root or
3141 // its inverse.
3142 let mut classes = topology::Classes::new(chains + lines.count);
3143 let mut torn = Vec::new();
3144 for &(c, b, l, r) in triples {
3145 let Some((chain, inverse)) = holding.chain[c] else {
3146 continue;
3147 };
3148 let key = (b, l.min(r), l.max(r));
3149 let (Some(row), Some(&(line, flip))) = (junctions.get(&key), lines.of.get(&key)) else {
3150 continue;
3151 };
3152 // The line's own direction runs from the pole before its middle
3153 // to the one after, as the row was first kept, or against it.
3154 let before = if flip { row[2].0 } else { row[1].0 };
3155 if !classes.join(chain, chains + line, inverse ^ (l != before)) {
3156 torn.push(chain);
3157 }
3158 }
3159 for (line, &bad) in lines.torn.iter().enumerate() {
3160 if bad {
3161 torn.push(chains + line);
3162 }
3163 }
3164 // Each class's corners' pins: the corner, the curve, and the ratio
3165 // they pin the class's root to.
3166 let mut pinned: BTreeMap<usize, Vec<(usize, usize, f64)>> = BTreeMap::new();
3167 let mut spoiled: Vec<usize> = Vec::new();
3168 for (k, corner) in corners.iter().enumerate() {
3169 for (i, &c) in corner.curves.iter().enumerate() {
3170 let Some((chain, inverse)) = holding.chain[c] else {
3171 continue;
3172 };
3173 let (root, odd) = classes.find(chain);
3174 match &corner.relation {
3175 Some(relation) => {
3176 let lambda = relation.weights[i].0;
3177 let value = if inverse ^ odd { 1.0 / lambda } else { lambda };
3178 pinned.entry(root).or_default().push((k, i, value));
3179 }
3180 None => spoiled.push(root),
3181 }
3182 }
3183 }
3184 for node in torn {
3185 spoiled.push(classes.find(node).0);
3186 }
3187 for (chain, &conflicted) in holding.conflicted.iter().enumerate().take(chains) {
3188 if conflicted {
3189 spoiled.push(classes.find(chain).0);
3190 }
3191 }
3192 let mut members: BTreeMap<usize, (Members, Members)> = BTreeMap::new();
3193 for node in 0..chains + lines.count {
3194 let (root, odd) = classes.find(node);
3195 let entry = members.entry(root).or_default();
3196 if node < chains {
3197 entry.0.push((node, odd));
3198 } else {
3199 entry.1.push((node - chains, odd));
3200 }
3201 }
3202 // The classes pinned more than once or meeting a junction, with
3203 // whether their corners were rescaled to them.
3204 let mut found: Vec<Pinned> = Vec::new();
3205 for (root, pins) in pinned {
3206 let Some((chain_members, line_members)) = members.remove(&root) else {
3207 continue;
3208 };
3209 if spoiled.contains(&root) || (pins.len() < 2 && line_members.is_empty()) {
3210 continue;
3211 }
3212 found.push(Pinned {
3213 pins,
3214 chains: chain_members,
3215 lines: line_members,
3216 });
3217 }
3218 // A class pinned by several corners takes the geometric mean of
3219 // their ratios, read before any corner is rescaled, moved as little
3220 // as [closes](close_cycles) the corners asked a ratio for every
3221 // curve of a cycle of their scales; each corner is then rescaled
3222 // once, to every ratio asked of it.
3223 let mut logs: Vec<Option<f64>> = vec![None; found.len()];
3224 let mut asked: BTreeMap<usize, Vec<Asked>> = BTreeMap::new();
3225 for (f, class) in found.iter().enumerate() {
3226 let (pins, chain_members) = (&class.pins, &class.chains);
3227 if pins.len() < 2 {
3228 continue;
3229 }
3230 let sum: f64 = pins.iter().map(|p| p.2.ln()).sum();
3231 logs[f] = Some(sum / precise(pins.len()));
3232 for &(k, i, _) in pins {
3233 let Some((chain, inverse)) = holding.chain[corners[k].curves[i]] else {
3234 continue;
3235 };
3236 let odd = chain_members
3237 .iter()
3238 .find(|m| m.0 == chain)
3239 .is_some_and(|m| m.1);
3240 asked.entry(k).or_default().push(Asked {
3241 curve: i,
3242 class: f,
3243 odd,
3244 inverse,
3245 });
3246 }
3247 }
3248 let mut failed = close_cycles(corners, &asked, &mut logs);
3249 let ratios: Vec<Option<f64>> = logs.iter().map(|l| l.map(f64::exp)).collect();
3250 for (k, asked) in asked {
3251 let mut pairs = Vec::with_capacity(asked.len());
3252 for a in &asked {
3253 let Some(ratio) = ratios[a.class].filter(|_| !failed[a.class]) else {
3254 continue;
3255 };
3256 // The ratio in the curve's own direction, as the triples
3257 // read it.
3258 let rho = if a.odd { 1.0 / ratio } else { ratio };
3259 pairs.push((a.curve, if a.inverse { 1.0 / rho } else { rho }));
3260 }
3261 let rescaled = pairs.len() == asked.len()
3262 && corners[k]
3263 .relation
3264 .as_mut()
3265 .is_some_and(|relation| relation.rescale(&pairs));
3266 if !rescaled {
3267 for a in asked {
3268 failed[a.class] = true;
3269 }
3270 }
3271 }
3272 // A class pinned by one corner takes its ratio, as rescaled.
3273 let mut settled = Vec::new();
3274 for (f, class) in found.into_iter().enumerate() {
3275 let Pinned {
3276 pins,
3277 chains,
3278 lines,
3279 } = class;
3280 if failed[f] {
3281 continue;
3282 }
3283 let ratio = match ratios[f] {
3284 Some(ratio) => ratio,
3285 None => {
3286 let (k, i, _) = pins[0];
3287 let Some(relation) = &corners[k].relation else {
3288 continue;
3289 };
3290 let Some((chain, inverse)) = holding.chain[corners[k].curves[i]] else {
3291 continue;
3292 };
3293 let odd = chains.iter().find(|m| m.0 == chain).is_some_and(|m| m.1);
3294 let lambda = relation.weights[i].0;
3295 if inverse ^ odd { 1.0 / lambda } else { lambda }
3296 }
3297 };
3298 settled.push(Settled {
3299 ratio,
3300 chains,
3301 lines,
3302 });
3303 }
3304 settled
3305 }
3306
3307 /// The slot of the pole one row into patch side `user` beside curve
3308 /// `c`'s pole `q`.
3309 /// The slots of the row of poles one in from the missing side of fan
3310 /// patch `p`, `k` that side, along the patch's knots in that side's
3311 /// direction: its ends the poles next to the fan's point of the curves
3312 /// either side.
3313 fn fan_ring(&self, layout: &Layout, (p, k): (usize, usize)) -> Vec<usize> {
3314 let (nu, nv) = layout.dims(p);
3315 let pole = |w: usize| match k {
3316 0 => (w, 1),
3317 1 => (nu - 2, w),
3318 2 => (w, nv - 2),
3319 _ => (1, w),
3320 };
3321 let along = if k % 2 == 0 { nu } else { nv };
3322 (0..along)
3323 .map(|w| layout.slot(self.topo, self.patches, p, pole(w)))
3324 .collect()
3325 }
3326
3327 /// The rows holding each fan's patches to one tangent plane at its
3328 /// point; `None` where a curve into a fan is not held by its triple
3329 /// next to the point, or the poles round it are not spread over a
3330 /// plane in the fit `loose`.
3331 ///
3332 /// At a fan's point every patch's cross derivative along its missing
3333 /// side is nought, and its normal there is the limit `S_d × S_dw`, `d`
3334 /// across the side and `w` along it: from the point to the row of poles
3335 /// one in from the side, crossed with that row's own derivative. The
3336 /// patches share a tangent plane there where those rows, every patch's,
3337 /// lie in one plane through the point. That plane is an unknown
3338 /// direction, which rows on the poles cannot name, so the rows are laid
3339 /// out as they lie in the fit without the constraints, projected on
3340 /// the plane through the point that their consecutive poles' cross
3341 /// products, each row walked as its patch walks round, sum to the
3342 /// normal of: each pole is held to two of them, the
3343 /// anchors, as `P − (1 − α − β) V − α A − β B = 0`, `α` and `β` its
3344 /// coordinates there against theirs. The plane and the layout's scale
3345 /// and shear stay free through `A` and `B`. A pole next to the point on
3346 /// a curve into the fan is held instead by the curve's triple, which
3347 /// puts it on the line through the poles beside it.
3348 fn fan_rows(
3349 &self,
3350 layout: &Layout,
3351 loose: &[Point],
3352 triples: &BTreeMap<usize, (usize, usize, f64)>,
3353 ) -> Option<Vec<Row>> {
3354 let topo = self.topo;
3355 let mut rows = Vec::new();
3356 for fan in &topo.fans {
3357 let v = fan.vertex;
3358 for &c in &fan.curves {
3359 let n = layout.along(c);
3360 let next = if topo.curve_ends[c].0 == v { 1 } else { n - 2 };
3361 triples.get(&layout.curve_slot(topo, c, next))?;
3362 }
3363 let rings: Vec<Vec<usize>> = fan
3364 .patches
3365 .iter()
3366 .map(|&side| self.fan_ring(layout, side))
3367 .collect();
3368 let e = |slot: usize| loose[slot] - loose[v];
3369 // Each row walked as its patch walks its missing side, which
3370 // turns every patch's one way round the point.
3371 let mut normal = Vector::ZERO;
3372 for (ring, &(_, k)) in rings.iter().zip(&fan.patches) {
3373 for w in ring.windows(2) {
3374 let turn = e(w[0]).cross(e(w[1]));
3375 normal += if k < 2 { turn } else { -turn };
3376 }
3377 }
3378 let inner: Vec<usize> = rings
3379 .iter()
3380 .flat_map(|ring| ring[1..ring.len() - 1].iter().copied())
3381 .collect();
3382 let first = e(inner[0]);
3383 let x = first - normal * (first.dot(normal) / normal.dot(normal));
3384 let x = x / x.magnitude();
3385 let y = (normal / normal.magnitude()).cross(x);
3386 let flat = |slot: usize| {
3387 let d = e(slot);
3388 (d.dot(x), d.dot(y))
3389 };
3390 let flats: Vec<(f64, f64)> = inner.iter().map(|&s| flat(s)).collect();
3391 let (ax, ay) = flats[0];
3392 let cross = |(bx, by): (f64, f64)| ax.mul_add(by, -(ay * bx));
3393 let b = (1..flats.len()).max_by(|&i, &j| {
3394 let sine = |k: usize| {
3395 let (bx, by) = flats[k];
3396 cross(flats[k]).abs() / bx.hypot(by)
3397 };
3398 sine(i).total_cmp(&sine(j))
3399 })?;
3400 let det = cross(flats[b]);
3401 if !(det.abs() > 0.0 && det.is_finite()) {
3402 return None;
3403 }
3404 let (bx, by) = flats[b];
3405 for (i, &(px, py)) in flats.iter().enumerate() {
3406 if i == 0 || i == b {
3407 continue;
3408 }
3409 let alpha = px.mul_add(by, -(py * bx)) / det;
3410 let beta = ax.mul_add(py, -(ay * px)) / det;
3411 if !(alpha.is_finite() && beta.is_finite()) {
3412 return None;
3413 }
3414 rows.push(vec![
3415 (inner[i], 1.0),
3416 (inner[0], -alpha),
3417 (inner[b], -beta),
3418 (v, alpha + beta - 1.0),
3419 ]);
3420 }
3421 }
3422 Some(rows)
3423 }
3424
3425 /// Whether every fan patch's normal at its fan's point, held to one
3426 /// plane by [`fan_rows`](Self::fan_rows), is defined all along its
3427 /// missing side and points one way there: the limit normal is
3428 /// `(R − V) × R'` along the row `R` of poles one in from the side, a
3429 /// spline whose Bernstein coefficients, span by span, all on one side
3430 /// of the plane bound it off nought. Across each curve into the fan
3431 /// the triples hold the two patches' normals alike, and so up to the
3432 /// point, so every patch's normal there is the same.
3433 fn fans_regular(
3434 &self,
3435 layout: &Layout,
3436 patch_knots: &[[KnotVector; 2]],
3437 poles: &[Point],
3438 ) -> OgeomResult<bool> {
3439 for fan in &self.topo.fans {
3440 let at = poles[fan.vertex];
3441 for &(p, k) in &fan.patches {
3442 let ring: Vec<Vector> = self
3443 .fan_ring(layout, (p, k))
3444 .iter()
3445 .map(|&s| poles[s] - at)
3446 .collect();
3447 let segments = ogeom_math::bspline::to_bezier_segments(
3448 &patch_knots[p][k % 2],
3449 &ring,
3450 self.tol,
3451 )?;
3452 // `(R − V) × R'` over each span as a quintic, from the
3453 // cubic's poles `b` and its derivative's, `Δb`: each
3454 // coefficient the sum of `C(3,i) C(2,j) b_i × Δb_j` over
3455 // `i + j = k`, over `C(5,k)`, dropping the derivative's
3456 // positive scale.
3457 const CUBIC: [f64; 4] = [1.0, 3.0, 3.0, 1.0];
3458 const QUADRATIC: [f64; 3] = [1.0, 2.0, 1.0];
3459 const QUINTIC: [f64; 6] = [1.0, 5.0, 10.0, 10.0, 5.0, 1.0];
3460 let mut coefficients = Vec::new();
3461 for (_, b) in &segments {
3462 let d: Vec<Vector> = b.windows(2).map(|w| w[1] - w[0]).collect();
3463 for (n, &q) in QUINTIC.iter().enumerate() {
3464 let mut sum = Vector::ZERO;
3465 for (i, &ci) in CUBIC.iter().enumerate() {
3466 if let Some(j) = n.checked_sub(i)
3467 && j < 3
3468 {
3469 sum += b[i].cross(d[j]) * (ci * QUADRATIC[j]);
3470 }
3471 }
3472 coefficients.push(sum / q);
3473 }
3474 }
3475 let normal = coefficients.iter().fold(Vector::ZERO, |a, &c| a + c);
3476 if !coefficients.iter().all(|c| c.dot(normal) > 0.0) {
3477 return Ok(false);
3478 }
3479 }
3480 }
3481 Ok(true)
3482 }
3483
3484 fn beside(&self, layout: &Layout, user: (usize, usize), c: usize, q: usize) -> usize {
3485 let w = layout.walk_index(self.patches, user, c, q);
3486 let (_, inner) = side_pole(user.1, w, layout.dims(user.0));
3487 layout.slot(self.topo, self.patches, user.0, inner)
3488 }
3489
3490 /// The rows holding tangent every curve into a corner of an odd number
3491 /// of patches whose curves all run on to the border.
3492 ///
3493 /// Across such a curve the cross derivatives of its two patches are
3494 /// `D₁(t)` and `D₂(t)` (the differences of the poles one row in from
3495 /// the curve's, on the curve's knots), and the patches share a tangent
3496 /// plane wherever `D₁ + λ D₂ + γ T = 0`, `T` the curve's own derivative
3497 /// and `λ > 0`. At the corner the condition is the corner's
3498 /// [relation](Self::relate). With `λ` constant and `γ` linear along the
3499 /// curve the left side is a cubic spline with double knots, `C¹` where
3500 /// the curve's knots are, so it is nought throughout where it is nought
3501 /// at the corner and at two points inside each span and the far end,
3502 /// which is as many points as it has coefficients: an exact, linear
3503 /// condition on the poles, though no pole-by-pole one.
3504 fn corner_rows(
3505 &self,
3506 layout: &Layout,
3507 loose: &[Point],
3508 curve_knots: &[KnotVector],
3509 ) -> Vec<Vec<(usize, f64)>> {
3510 let mut rows = Vec::new();
3511 for (v, curves) in &self.topo.odd_corners {
3512 let Some(relation) = self.relate(layout, loose, *v, curves) else {
3513 continue;
3514 };
3515 rows.extend(relation.rows());
3516 for (i, &c) in curves.iter().enumerate() {
3517 rows.extend(self.along_rows(
3518 layout,
3519 loose,
3520 c,
3521 *v,
3522 relation.weights[i],
3523 &curve_knots[c],
3524 ));
3525 }
3526 }
3527 rows
3528 }
3529
3530 /// The rows holding tangent every curve into a corner whose gluing is
3531 /// spread along its curves; `None` where a corner has no relation or the
3532 /// corners a curve joins cannot be scaled to one ratio for it.
3533 ///
3534 /// Each curve is held with `λ` constant and `γ` linear along it, as
3535 /// [`corner_rows`](Self::corner_rows) holds a curve, but every knot
3536 /// inside its strip is doubled, so its patches' cross derivatives are
3537 /// only `C¹` across those knots, and the curve is held `C²` across
3538 /// each, so `γ T` is `C¹` there too: the left side is a `C¹` cubic
3539 /// spline on the doubled knots, which [`spread_along_rows`](Self::spread_along_rows)
3540 /// holds nought. With single knots `γ T` would jump in its second
3541 /// derivative by `γ` times the jump of `T''` at each knot, binding the
3542 /// curve to a single cubic; with them doubled it is a `C²` cubic spline,
3543 /// refined as any. A curve joining two such corners is asked a ratio by
3544 /// each: they are scaled to the geometric mean of the two, moved as
3545 /// little as [closes](close_cycles) every cycle of a corner's scales,
3546 /// and `γ` runs from the one corner's `g` to the other's. The other
3547 /// curves along the doubled strips carry no gluing, and are held `C²`
3548 /// across the doubled knots as they are where the knots are single.
3549 fn spread_rows(
3550 &self,
3551 layout: &Layout,
3552 loose: &[Point],
3553 curve_knots: &[KnotVector],
3554 ) -> Option<Vec<Row>> {
3555 let topo = self.topo;
3556 let mut corners = Vec::with_capacity(topo.spread_corners.len());
3557 for (v, curves) in &topo.spread_corners {
3558 let relation = self.relate(layout, loose, *v, curves)?;
3559 corners.push(Related {
3560 vertex: *v,
3561 curves: curves.clone(),
3562 relation: Some(relation),
3563 });
3564 }
3565 // Each curve's place round the corners it runs into.
3566 let mut ends: BTreeMap<usize, Vec<(usize, usize)>> = BTreeMap::new();
3567 for (k, corner) in corners.iter().enumerate() {
3568 for (i, &c) in corner.curves.iter().enumerate() {
3569 ends.entry(c).or_default().push((k, i));
3570 }
3571 }
3572 let mut logs = Vec::new();
3573 let mut asked: BTreeMap<usize, Vec<Asked>> = BTreeMap::new();
3574 for at in ends.values().filter(|at| at.len() == 2) {
3575 let class = logs.len();
3576 let mut sum = 0.0;
3577 for &(k, i) in at {
3578 sum += corners[k].relation.as_ref()?.weights[i].0.ln();
3579 asked.entry(k).or_default().push(Asked {
3580 curve: i,
3581 class,
3582 odd: false,
3583 inverse: false,
3584 });
3585 }
3586 logs.push(Some(sum / 2.0));
3587 }
3588 if close_cycles(&corners, &asked, &mut logs).contains(&true) {
3589 return None;
3590 }
3591 for (&k, asked) in &asked {
3592 let pairs = asked
3593 .iter()
3594 .map(|a| Some((a.curve, logs[a.class]?.exp())))
3595 .collect::<Option<Vec<_>>>()?;
3596 if !corners[k].relation.as_mut()?.rescale(&pairs) {
3597 return None;
3598 }
3599 }
3600 let mut rows = Vec::new();
3601 for corner in &corners {
3602 rows.extend(corner.relation.as_ref()?.rows());
3603 }
3604 for (&c, at) in &ends {
3605 let (k, i) = at[0];
3606 let relation = corners[k].relation.as_ref()?;
3607 let other = match at.get(1) {
3608 Some(&(m, j)) => Some(corners[m].relation.as_ref()?.weights[j].1),
3609 None => None,
3610 };
3611 rows.extend(self.spread_along_rows(
3612 layout,
3613 loose,
3614 c,
3615 corners[k].vertex,
3616 relation.weights[i],
3617 &curve_knots[c],
3618 other,
3619 ));
3620 rows.extend(smooth_rows(layout, topo, c, &curve_knots[c]));
3621 }
3622 let doubled = self.doubled();
3623 for (c, strips) in topo.curve_strip.iter().enumerate() {
3624 if !ends.contains_key(&c) && strips.iter().any(|&s| doubled[s].every) {
3625 rows.extend(smooth_rows(layout, topo, c, &curve_knots[c]));
3626 }
3627 }
3628 Some(rows)
3629 }
3630
3631 /// How the curves into corner `v` of other than four patches relate
3632 /// there, read off the fit `loose`; `None` where they do not close
3633 /// round it or a curve's ratio is not positive.
3634 ///
3635 /// At the corner `D₁` and `D₂` run along the neighbouring curves, so
3636 /// there `D₁ + λ D₂ + γ T = 0` is a relation among the first poles of
3637 /// the curves along each of them: they lie in one plane through the
3638 /// corner. The relations come from `loose`: its first poles projected
3639 /// on the plane their consecutive cross products face, each curve's `λ`
3640 /// and `γ` the unique relation among its own first pole and the two
3641 /// beside it there. Those of `n` curves round a corner have `n − 2`
3642 /// independent ones, so `n − 2` consecutive ones are held and the other
3643 /// two follow.
3644 ///
3645 /// The cross derivatives at the corner differentiated along each curve
3646 /// bring in each patch's twist pole, once from either side, in a cycle.
3647 /// Each curve's ratio is that of the cross products of its first pole
3648 /// with those of the curves either side, so the ratios round the corner
3649 /// multiply to one: the cycle is never singular at an odd number of
3650 /// curves and always singular at an even one (the vertex enclosure
3651 /// condition), where it leaves one condition on the curves' own poles.
3652 /// Corners held whole are odd only; a corner held by its spans or with
3653 /// its gluing spread may be even, the condition then falling on the
3654 /// poles of the curves' spans next to it, or on their second poles.
3655 fn relate(
3656 &self,
3657 layout: &Layout,
3658 loose: &[Point],
3659 v: usize,
3660 curves: &[usize],
3661 ) -> Option<Relation> {
3662 let topo = self.topo;
3663 // Each curve's pole next to the corner, and the poles beside it
3664 // there in its first and second patch: those of the curves either
3665 // side.
3666 let near: Vec<(usize, usize, usize)> = curves
3667 .iter()
3668 .map(|&c| {
3669 let n = layout.along(c);
3670 let (at, next) = if topo.curve_ends[c].0 == v {
3671 (0, 1)
3672 } else {
3673 (n - 1, n - 2)
3674 };
3675 let [first, second] = [topo.users[c][0], topo.users[c][1]];
3676 (
3677 self.beside(layout, first, c, at),
3678 layout.curve_slot(topo, c, next),
3679 self.beside(layout, second, c, at),
3680 )
3681 })
3682 .collect();
3683 // The curves in turn round the corner.
3684 let mut order = vec![0];
3685 while order.len() < curves.len() {
3686 let last = near[order[order.len() - 1]];
3687 let next = (0..curves.len())
3688 .find(|&i| !order.contains(&i) && (near[i].1 == last.0 || near[i].1 == last.2))?;
3689 order.push(next);
3690 }
3691 let (first, last) = (near[order[0]], near[order[order.len() - 1]]);
3692 if !(first.0 == last.1 || first.2 == last.1) {
3693 return None;
3694 }
3695 let e = |slot: usize| loose[slot] - loose[v];
3696 let mut normal = Vector::ZERO;
3697 for (k, &i) in order.iter().enumerate() {
3698 let j = order[(k + 1) % order.len()];
3699 normal += e(near[i].1).cross(e(near[j].1));
3700 }
3701 let first = e(near[order[0]].1);
3702 let x = first - normal * (first.dot(normal) / normal.dot(normal));
3703 let y = normal.cross(x);
3704 let flat = |slot: usize| {
3705 let d = e(slot);
3706 (d.dot(x), d.dot(y))
3707 };
3708 let flat: Vec<(f64, f64)> = near.iter().map(|&(_, own, _)| flat(own)).collect();
3709 let curve_at = |slot: usize| near.iter().position(|n| n.1 == slot);
3710 let sides = near
3711 .iter()
3712 .map(|&(left, _, right)| Some((curve_at(left)?, curve_at(right)?)))
3713 .collect::<Option<Vec<_>>>()?;
3714 let weights = plane_weights(&flat, &sides)?;
3715 Some(Relation {
3716 vertex: v,
3717 near,
3718 order,
3719 weights,
3720 flat,
3721 sides,
3722 })
3723 }
3724
3725 /// The rows holding `D₁ + λ D₂ + γ T` nought along the span of curve
3726 /// `c` next to its end at corner `v`, which the curve's other poles'
3727 /// triples leave free, and that span a quadratic; `weights` are `λ`
3728 /// and the relation's `g` at `v`.
3729 ///
3730 /// The knot past that span is doubled, so over it the two basis
3731 /// functions nearest the corner are the cubic Bernstein polynomials
3732 /// `(1 − x)³` and `3 x (1 − x)²`, `x` running across the span from the
3733 /// corner, and the patches are only `C¹` across its far end. With `γ`
3734 /// quadratic over the span, `γ(0) (1 − x)²`, and nought past it, and `T`
3735 /// linear over it, `γ T` is `γ(0) T(0)` times the first and `γ(0) T(1)
3736 /// / 3` times the second; the curve's triples past those two poles hold
3737 /// the rest nought. So the triple of the pole next to the corner is
3738 /// `−γ(0) T(0)`, which is the corner's relation, and that of the pole
3739 /// after it `−γ(0) T(1) / 3`, `T(1)` the curve's derivative at the far
3740 /// end of the span: the left side is then nought throughout, `C¹` across
3741 /// the span's far end on either side, as `γ` has a double root there.
3742 /// Past the span `γ` is nought, so the curve is held as a curve between
3743 /// corners of four patches is, and its far end may be one.
3744 fn span_rows(
3745 &self,
3746 layout: &Layout,
3747 c: usize,
3748 v: usize,
3749 (lambda, g): (f64, f64),
3750 knots: &KnotVector,
3751 ) -> Vec<Vec<(usize, f64)>> {
3752 let topo = self.topo;
3753 let t = knots.knots();
3754 let n = layout.along(c);
3755 let starts = topo.curve_ends[c].0 == v;
3756 // The span next to the corner, its far end, its width, and the pole
3757 // after the corner's next one.
3758 let (span, far, width, next) = if starts {
3759 (DEGREE, t[DEGREE + 1], t[DEGREE + 1] - t[DEGREE], 1)
3760 } else {
3761 (n - 1, t[n - 1], t[n] - t[n - 1], n - 2)
3762 };
3763 // `γ(0) T` in the poles' differences: `T` at the corner is `3 / h`
3764 // times the first pole's difference from it, and the other way
3765 // about at the curve's end.
3766 let near = if starts {
3767 g * width / precise(DEGREE)
3768 } else {
3769 -g * width / precise(DEGREE)
3770 };
3771 let own = |i: usize| layout.curve_slot(topo, c, span - DEGREE + i);
3772 let [first, second] = [topo.users[c][0], topo.users[c][1]];
3773 let mut row: BTreeMap<usize, f64> = BTreeMap::new();
3774 *row.entry(self.beside(layout, first, c, next))
3775 .or_insert(0.0) += 1.0;
3776 *row.entry(self.beside(layout, second, c, next))
3777 .or_insert(0.0) += lambda;
3778 *row.entry(layout.curve_slot(topo, c, next)).or_insert(0.0) -= 1.0 + lambda;
3779 let slope = knots.basis_derivatives(span, far, 1);
3780 for (i, rate) in slope[1].iter().enumerate() {
3781 *row.entry(own(i)).or_insert(0.0) += near / precise(DEGREE) * rate;
3782 }
3783 // The span's third derivative, nought, scaled to its width.
3784 let middle = f64::midpoint(t[span], t[span + 1]);
3785 let jerk = knots.basis_derivatives(span, middle, DEGREE);
3786 let cube = width * width * width;
3787 vec![
3788 row.into_iter().filter(|e| e.1 != 0.0).collect(),
3789 jerk[DEGREE]
3790 .iter()
3791 .enumerate()
3792 .map(|(i, d)| (own(i), d * cube))
3793 .collect(),
3794 ]
3795 }
3796
3797 /// The rows holding `D₁ + λ D₂ + γ T` nought along curve `c` past its
3798 /// end at `v`: at two points inside each span and at its other end.
3799 /// `weights` are `λ` and the relation's `g` at `v`, `γ T` there being
3800 /// `g` times the first pole's difference from `v`. `γ` runs linearly
3801 /// from there to the other end, which keeps `γ T` a cubic spline on the
3802 /// curve's knots; its value at the other end is the one that leaves the
3803 /// fit `loose` least out of tangency at the same points.
3804 fn along_rows(
3805 &self,
3806 layout: &Layout,
3807 loose: &[Point],
3808 c: usize,
3809 v: usize,
3810 (lambda, g): (f64, f64),
3811 knots: &KnotVector,
3812 ) -> Vec<Vec<(usize, f64)>> {
3813 let starts = self.topo.curve_ends[c].0 == v;
3814 let near = end_gamma(layout, c, g, knots, starts);
3815 let terms = self.along_terms(
3816 layout,
3817 c,
3818 lambda,
3819 knots,
3820 starts,
3821 &along_points(knots, starts),
3822 );
3823 let far = least_far(loose, &terms, near);
3824 terms
3825 .iter()
3826 .map(|(cross, slope, share)| {
3827 gamma_row(cross, slope, (1.0 - share).mul_add(near, share * far))
3828 })
3829 .collect()
3830 }
3831
3832 /// At each of `at` along curve `c`, the slots and their weights in
3833 /// `D₁ + λ D₂` and in `T`, and the share of the way from the corner
3834 /// the rows hold it from, at its start where `starts`.
3835 fn along_terms(
3836 &self,
3837 layout: &Layout,
3838 c: usize,
3839 lambda: f64,
3840 knots: &KnotVector,
3841 starts: bool,
3842 at: &[f64],
3843 ) -> Vec<(Weighted, Weighted, f64)> {
3844 let topo = self.topo;
3845 let [first, second] = [topo.users[c][0], topo.users[c][1]];
3846 at.iter()
3847 .map(|&x| {
3848 let span = knots.span_unchecked(x);
3849 let basis = knots.basis_derivatives(span, x, 1);
3850 let mut cross = Vec::with_capacity(3 * (DEGREE + 1));
3851 let mut slope = Vec::with_capacity(DEGREE + 1);
3852 for i in 0..=DEGREE {
3853 let q = span - DEGREE + i;
3854 let (value, rate) = (basis[0][i], basis[1][i]);
3855 let own = layout.curve_slot(topo, c, q);
3856 cross.push((self.beside(layout, first, c, q), value));
3857 cross.push((self.beside(layout, second, c, q), lambda * value));
3858 cross.push((own, -(1.0 + lambda) * value));
3859 slope.push((own, rate));
3860 }
3861 (cross, slope, if starts { x } else { 1.0 - x })
3862 })
3863 .collect()
3864 }
3865
3866 /// The rows holding `D₁ + λ D₂ + γ T` nought along curve `c` from its
3867 /// end at corner `v`, every knot inside it doubled and the curve held
3868 /// `C²` across each by [`smooth_rows`]: the left side is then a `C¹`
3869 /// cubic spline on the doubled knots, nought where it and its
3870 /// derivative are nought at every knot. `weights` are `λ` and the
3871 /// relation's `g` at `v`, and `γ` runs linearly from there to the
3872 /// other end, its value there as [`along_rows`](Self::along_rows) reads
3873 /// it, or, where the other end is a corner whose relation gives the
3874 /// curve `g` there as `other`, that corner's. The value at a corner is
3875 /// its relation's to hold, so only the derivative is held there.
3876 ///
3877 /// Held at the knots, each row bears on the poles of the spans either
3878 /// side alone, however many spans the curve has; collocated inside the
3879 /// spans as [`along_rows`](Self::along_rows) holds them, the rows would
3880 /// tie each span to the last, and the far end's row would follow from
3881 /// the others ever more nearly as the spans multiply.
3882 #[allow(
3883 clippy::too_many_arguments,
3884 reason = "the curve, its corner's weights, and the other end's"
3885 )]
3886 fn spread_along_rows(
3887 &self,
3888 layout: &Layout,
3889 loose: &[Point],
3890 c: usize,
3891 v: usize,
3892 (lambda, g): (f64, f64),
3893 knots: &KnotVector,
3894 other: Option<f64>,
3895 ) -> Vec<Row> {
3896 let topo = self.topo;
3897 let starts = topo.curve_ends[c].0 == v;
3898 let near = end_gamma(layout, c, g, knots, starts);
3899 let far = match other {
3900 // The other corner's `g` at the curve's other end, read as
3901 // `near` reads its own.
3902 Some(other) => end_gamma(layout, c, other, knots, !starts),
3903 None => {
3904 let points = along_points(knots, starts);
3905 least_far(
3906 loose,
3907 &self.along_terms(layout, c, lambda, knots, starts, &points),
3908 near,
3909 )
3910 }
3911 };
3912 // `γ` and its derivative along the curve's parameter.
3913 let gamma = |share: f64| (1.0 - share).mul_add(near, share * far);
3914 let rate = if starts { far - near } else { near - far };
3915 let [first, second] = [topo.users[c][0], topo.users[c][1]];
3916 let mut rows = Vec::new();
3917 let distinct = knots.distinct();
3918 let last = distinct.len() - 1;
3919 for (k, &(x, _)) in distinct.iter().enumerate() {
3920 let share = if starts { x } else { 1.0 - x };
3921 let (at_v, at_other) = if starts {
3922 (k == 0, k == last)
3923 } else {
3924 (k == last, k == 0)
3925 };
3926 if !(at_v || (at_other && other.is_some())) {
3927 let terms = self.along_terms(layout, c, lambda, knots, starts, &[x]);
3928 rows.extend(
3929 terms
3930 .iter()
3931 .map(|(cross, slope, share)| gamma_row(cross, slope, gamma(*share))),
3932 );
3933 }
3934 // The derivative, scaled to the width of the span it is read
3935 // in, the one after the knot but at the curve's end.
3936 let span = knots.span_unchecked(x);
3937 let width = if k < last {
3938 distinct[k + 1].0 - x
3939 } else {
3940 x - distinct[k - 1].0
3941 };
3942 let basis = knots.basis_derivatives(span, x, 2);
3943 let mut row: BTreeMap<usize, f64> = BTreeMap::new();
3944 for i in 0..=DEGREE {
3945 let q = span - DEGREE + i;
3946 let (slope, bend) = (basis[1][i], basis[2][i]);
3947 let own = layout.curve_slot(topo, c, q);
3948 *row.entry(self.beside(layout, first, c, q)).or_insert(0.0) += width * slope;
3949 *row.entry(self.beside(layout, second, c, q)).or_insert(0.0) +=
3950 width * lambda * slope;
3951 *row.entry(own).or_insert(0.0) +=
3952 width * (rate.mul_add(slope, gamma(share) * bend) - (1.0 + lambda) * slope);
3953 }
3954 rows.push(row.into_iter().filter(|e| e.1 != 0.0).collect());
3955 }
3956 rows
3957 }
3958
3959 /// Finish a try that missed at `spans`, from its fit `start` at the
3960 /// seed's parameters: while not every point is within the tolerance,
3961 /// the points are re-taken at their nearest points on the fit, the
3962 /// patches fitted again at the same spans, and the fit refined on from
3963 /// there. The spans, the fit and the points as re-taken where it comes
3964 /// within the tolerance holding every corner the try names; `None`
3965 /// where it stops first.
3966 ///
3967 /// The noise's variance is read once, off `start`'s median distance as
3968 /// refinement reads it, and held: re-read after a correction, whose
3969 /// distances are all along the normal, it would shrink and let the fit
3970 /// follow the noise. A fit is kept only where it holds every corner the
3971 /// try names, takes off more of the squared distances than twice that
3972 /// variance (what Mallows' criterion charges a free pole), and neither
3973 /// the worst miss that is not a stray nor the worst distance grows. The
3974 /// corrections stop where the squared distances are down to the noise
3975 /// left over the free controls, `σ̂₀²(m − free)`, and after
3976 /// [`FINISH_STEPS`].
3977 ///
3978 /// Refinement then scores with the same variance and tries the strips
3979 /// of every patch a point misses, strays too: on a clean scan the rest
3980 /// of a cell's spread is the fit's own error, not noise, and a point
3981 /// past it is a miss the finished fit must still come within the
3982 /// tolerance of, as it is kept only where every point does.
3983 fn finish(
3984 &self,
3985 options: &NetworkOptions,
3986 spans: &[usize],
3987 start: Round,
3988 ) -> OgeomResult<Option<(Vec<usize>, Round, Taken)>> {
3989 let tolerance = options.tolerance;
3990 let judged = (DEGREE + 1) * (DEGREE + 1);
3991 let variance = (median(&start.distances) / NORMAL_QUARTILE).powi(2);
3992 let worst = |fit: &Fit, round: &Round| {
3993 fit.missed(round, tolerance, judged)
3994 .0
3995 .first()
3996 .map_or(0.0, |w| w.0)
3997 };
3998 let widest = |round: &Round| round.distances.iter().copied().fold(0.0, f64::max);
3999 let mut taken = self.taken.clone();
4000 let mut spans = spans.to_vec();
4001 let mut current = start;
4002 for step in 0..=FINISH_STEPS {
4003 if current.held && current.distances.iter().all(|d| *d <= tolerance) {
4004 return Ok(Some((spans, current, taken)));
4005 }
4006 let floor = variance * precise(current.distances.len().saturating_sub(current.free));
4007 if step == FINISH_STEPS || variance <= 0.0 || current.square() <= floor {
4008 return Ok(None);
4009 }
4010 let fit = Fit {
4011 taken: &taken,
4012 ..*self
4013 };
4014 let again = fit.retake(¤t)?;
4015 let refit = Fit {
4016 taken: &again,
4017 ..*self
4018 };
4019 let next = refit.round(&spans, &again.at)?;
4020 let kept = next.held
4021 && current.square() - next.square() > 2.0 * variance
4022 && worst(&refit, &next) <= worst(&fit, ¤t)
4023 && widest(&next) <= widest(¤t);
4024 if !kept {
4025 return Ok(None);
4026 }
4027 // Refined on from there, the points where they now lie and
4028 // the variance held.
4029 let (raised, refined, _) = refit.refine_from(options, spans, next, Some(variance))?;
4030 (taken, spans, current) = (again, raised, refined);
4031 }
4032 Ok(None)
4033 }
4034
4035 /// The points re-taken at their nearest points on `round`'s patches:
4036 /// each by its own patch, or, where that foot lies on a side its patch
4037 /// shares, by the patch across the side where that is nearer. Such a
4038 /// point lies past its patch's side, on the other's side of the curve,
4039 /// and the nearest patch takes it, as the seeds took every point.
4040 fn retake(&self, round: &Round) -> OgeomResult<Taken> {
4041 let topo = self.topo;
4042 let geometry: Vec<SurfaceGeometry> = round
4043 .surfaces
4044 .iter()
4045 .map(|s| SurfaceGeometry::BSpline(s.clone()))
4046 .collect();
4047 let mut taken = self.taken.clone();
4048 taken.at.clone_from(&round.feet);
4049 for (i, &p) in self.taken.patch.iter().enumerate() {
4050 let (u, v) = round.feet[i];
4051 let mut best: Option<(usize, (f64, f64), f64)> = None;
4052 for (k, x, on) in [
4053 (0, u, v <= 0.0),
4054 (1, v, u >= 1.0),
4055 (2, u, v >= 1.0),
4056 (3, v, u <= 0.0),
4057 ] {
4058 if !on {
4059 continue;
4060 }
4061 let parts = side_parts(topo, self.patches, (p, k));
4062 let piece = piece_at(k, x, &parts, &round.patch_bounds[p][k % 2]);
4063 let Some(side) = self.patches[p].sides[k].get(piece) else {
4064 continue;
4065 };
4066 for &(q, _) in &topo.users[side.curve] {
4067 if q == p {
4068 continue;
4069 }
4070 let foot =
4071 ogeom_geom::project_on_surface(&geometry[q], taken.point[i], 0, self.tol)?;
4072 if foot.distance < best.map_or(round.distances[i], |b| b.2) {
4073 best = Some((q, foot.parameters, foot.distance));
4074 }
4075 }
4076 }
4077 if let Some((q, at, _)) = best {
4078 taken.patch[i] = q;
4079 taken.at[i] = at;
4080 }
4081 }
4082 for mine in &mut taken.of_patch {
4083 mine.clear();
4084 }
4085 for (i, &p) in taken.patch.iter().enumerate() {
4086 taken.of_patch[p].push(i);
4087 }
4088 Ok(taken)
4089 }
4090
4091 /// The worst miss of each patch among its cells holding at least
4092 /// `judged` points, that is not a stray, with the patch; worst first.
4093 /// Then whether any point misses without being a stray, and whether
4094 /// any is a stray.
4095 ///
4096 /// A stray stands farther from the fit than noise as wide as the rest
4097 /// of its cell (their root mean square distance) reaches over the
4098 /// whole cloud, `√(2 ln m)` deviations.
4099 fn missed(
4100 &self,
4101 round: &Round,
4102 tolerance: f64,
4103 judged: usize,
4104 ) -> (Vec<(f64, usize)>, bool, bool) {
4105 let reach = (2.0 * precise(round.distances.len().max(1)).ln()).sqrt();
4106 let (mut any, mut strays) = (false, false);
4107 let mut missed = Vec::new();
4108 for (p, mine) in self.taken.of_patch.iter().enumerate() {
4109 let [ku, kv] = &round.patch_knots[p];
4110 let (cu, cv) = (
4111 ku.control_point_count() - DEGREE,
4112 kv.control_point_count() - DEGREE,
4113 );
4114 let cell_of = |i: usize| {
4115 let (u, v) = round.feet[i];
4116 (ku.span_unchecked(u) - DEGREE) * cv + kv.span_unchecked(v) - DEGREE
4117 };
4118 let mut count = vec![0usize; cu * cv];
4119 let mut square = vec![0.0; cu * cv];
4120 for &i in mine {
4121 let k = cell_of(i);
4122 count[k] += 1;
4123 square[k] += round.distances[i] * round.distances[i];
4124 }
4125 let mut worst: Option<f64> = None;
4126 for &i in mine {
4127 let d = round.distances[i];
4128 if d <= tolerance {
4129 continue;
4130 }
4131 let k = cell_of(i);
4132 let rest = count[k] - 1;
4133 let stray =
4134 rest > 0 && d > reach * ((square[k] - d * d).max(0.0) / precise(rest)).sqrt();
4135 if stray {
4136 strays = true;
4137 continue;
4138 }
4139 any = true;
4140 if count[k] >= judged && worst.is_none_or(|w| d > w) {
4141 worst = Some(d);
4142 }
4143 }
4144 if let Some(d) = worst {
4145 missed.push((d, p));
4146 }
4147 }
4148 missed.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
4149 (missed, any, strays)
4150 }
4151}
4152
4153/// One side of a loop [`fit_loop_quads`] fills: its curve over a range,
4154/// and how the fill leaves it.
4155#[derive(Debug, Clone)]
4156pub struct LoopSide {
4157 /// The side's curve.
4158 pub curve: Curve,
4159 /// The range of it the side runs over, in the loop's direction.
4160 pub range: (f64, f64),
4161 /// How the fill leaves the side.
4162 pub across: Across,
4163}
4164
4165/// Fill a closed loop of sides with one four-sided patch per corner of the
4166/// loop, every side held exactly and held across as it says.
4167///
4168/// Each side is split at the middle of its range; a spoke runs from each
4169/// side's middle to a centre, every spoke ending there (`spokes[i]` from
4170/// side `i`'s middle to the centre, drawn on the shape the points sample).
4171/// The patch at the corner between side `i` and side `i + 1` is bounded by
4172/// the second half of side `i`, the first half of side `i + 1` and the two
4173/// spokes from their middles: the n-sided fill as quads round a centre. The
4174/// halves are held to the sides' curves ([`NetworkOptions::held`]) and held
4175/// across them ([`NetworkOptions::across`]), so every patch meets its
4176/// sides' supports tangent exactly, up to the halves' own projection; the
4177/// spokes and the centre are fitted and held tangent as any network's
4178/// curves and corners are. The caller's `held` and `across` are replaced.
4179///
4180/// # Errors
4181///
4182/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) for
4183/// fewer than three sides, a spoke count other than the sides', or a range
4184/// that runs nowhere; as [`fit_network`] otherwise.
4185pub fn fit_loop_quads(
4186 model: &mut Model,
4187 sides: &[LoopSide],
4188 spokes: &[Vec<Point>],
4189 points: &PointIndex,
4190 options: &NetworkOptions,
4191 tol: Tolerances,
4192) -> OgeomResult<Network> {
4193 let n = sides.len();
4194 if n < 3 {
4195 ogeom_bail!(Construction, "a loop of {n} sides bounds no fill");
4196 }
4197 if spokes.len() != n {
4198 ogeom_bail!(
4199 Construction,
4200 "a loop of {n} sides needs {n} spokes, one from each side's middle; got {}",
4201 spokes.len()
4202 );
4203 }
4204 let mut curves: Vec<CurveDef> = Vec::with_capacity(3 * n);
4205 for (i, side) in sides.iter().enumerate() {
4206 let (lo, hi) = side.range;
4207 if !(lo.is_finite() && hi.is_finite()) || lo == hi {
4208 ogeom_bail!(Construction, "side {i}'s range runs nowhere");
4209 }
4210 let mid = f64::midpoint(lo, hi);
4211 curves.push(CurveDef::Curve {
4212 curve: side.curve.clone(),
4213 range: (lo, mid),
4214 });
4215 curves.push(CurveDef::Curve {
4216 curve: side.curve.clone(),
4217 range: (mid, hi),
4218 });
4219 }
4220 curves.extend(spokes.iter().map(|s| CurveDef::Polyline(s.clone())));
4221 let patches: Vec<PatchDef> = (0..n)
4222 .map(|i| {
4223 let next = (i + 1) % n;
4224 PatchDef::four([
4225 (2 * i + 1, false),
4226 (2 * next, false),
4227 (2 * n + next, false),
4228 (2 * n + i, true),
4229 ])
4230 })
4231 .collect();
4232 let held_options = NetworkOptions {
4233 held: (0..2 * n).collect(),
4234 across: (0..2 * n).map(|h| (h, sides[h / 2].across)).collect(),
4235 ..options.clone()
4236 };
4237 fit_network(model, &patches, &curves, points, &held_options, tol)
4238}