BREP_kernel 0.18.0

A boundary representation (BREP) geometry kernel for building CAD applications.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
use super::*;

/// How far a loop travels along its own pcurves in one parameter: nothing, one
/// period forward or back, or something this path does not handle (a partial
/// or double wind, a loop broken by a jump inside a coedge).
fn winding(travel: f64, period: f64) -> Option<i32> {
    let tolerance = 1e-3 * period.abs().max(1e-300);
    if travel.abs() < tolerance {
        Some(0)
    } else if (travel - period).abs() < tolerance {
        Some(1)
    } else if (travel + period).abs() < tolerance {
        Some(-1)
    } else {
        None
    }
}

/// Integrate `kinds` over a periodic face whose loops WIND — every loop either
/// closes in the covering plane or runs one full period in the surface's
/// periodic direction (a rim, a pole line, a wavy full-wrap cut) — or return
/// None when the face is not that shape.
///
/// Such loops bound no region of the covering plane, which is why the chord
/// polygon paths treat them by rebuilding a proxy region. This path needs no
/// region: with `G(u, v) = ∫_{v0}^{v} g(u, s) ds` the integral of `g` over
/// the material between u-winding loops is `∮ −G du` along the loops' pcurves
/// alone (Green's theorem in the covering plane; the two cuts along the seam
/// carry `G` at `u0` and `u0 + period`, equal because `g` is periodic, and
/// cancel; a collapsed pole line at `v0` contributes nothing and one at `v1`
/// the whole column). A v-winding face swaps the roles (`H(u, v) = ∫_{u0}^{u}
/// g(s, v) ds`, `∮ H dv`). Every station is a pcurve Gauss point with the
/// antiderivative taken by Gauss across the surface's knot spans, so the
/// trim is read exactly as given — no chord, no proxy, no sample count.
/// Loops that do not wind (a hole inside the band) integrate the same way.
/// The overall orientation is normalised through the area, as the polygon
/// path does, and `same_sense` enters through the weighted normal.
///
/// Singly periodic surfaces only. On a torus two winding loops bound two
/// strips — between them and across the other seam — and only the loops'
/// orientation says which one is material; normalising through the area
/// would always pick the strip between them. Those faces stay with the
/// biperiodic band analysis (`biperiodic_band_range`), which reads the
/// orientation against `same_sense` for exactly that choice.
pub(super) fn winding_band_integral(
    face: &FaceRecord,
    kinds: &[Integrand],
) -> Result<Option<Vec<f64>>, String> {
    winding_band_integral_with(face, kinds, 1)
}

/// `winding_band_integral` with every pcurve span (after the surface-knot
/// cuts) subdivided `refine` times — a convergence check for the tests; the
/// integrator itself runs at 1.
pub(super) fn winding_band_integral_with(
    face: &FaceRecord,
    kinds: &[Integrand],
    refine: usize,
) -> Result<Option<Vec<f64>>, String> {
    static POLE_GAUGE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
    let pole_gauge = *POLE_GAUGE.get_or_init(|| {
        std::env::var("BREP_MASS_SPHERE_POLE_GAUGE").as_deref() == Ok("1")
    });
    winding_band_integral_with_pole_gauge(face, kinds, refine, pole_gauge)
}

pub(super) fn winding_band_integral_with_pole_gauge(
    face: &FaceRecord,
    kinds: &[Integrand],
    refine: usize,
    pole_gauge: bool,
) -> Result<Option<Vec<f64>>, String> {
    let (closed_u, closed_v) = face.surface.closed_directions()?;
    if closed_u == closed_v {
        return Ok(None);
    }
    let [u0, u1] = face.surface.domain_u()?;
    let [v0, v1] = face.surface.domain_v()?;
    let (mut winds_u, mut winds_v) = (false, false);
    let mut net_u = 0;
    for loop_record in &face.loops {
        let (mut travel_u, mut travel_v) = (0.0, 0.0);
        for coedge in &loop_record.coedges {
            let [t0, t1] = coedge.pcurve.domain()?;
            let start = coedge.pcurve.evaluate(t0)?;
            let end = coedge.pcurve.evaluate(t1)?;
            travel_u += end.x - start.x;
            travel_v += end.y - start.y;
        }
        match (winding(travel_u, u1 - u0), winding(travel_v, v1 - v0)) {
            (Some(0), Some(0)) => {}
            (Some(w), Some(0)) if closed_u => {
                winds_u = true;
                net_u += w;
            }
            (Some(0), Some(_)) if closed_v => winds_v = true,
            _ => return Ok(None),
        }
    }
    let p_is_u = match (winds_u, winds_v) {
        (true, false) => true,
        (false, true) => false,
        _ => return Ok(None),
    };
    if pole_gauge && p_is_u && net_u != 0 && complete_sphere(face)? {
        // A rim enclosing a pole is closed on the sphere but open in its
        // chart. Base the primitive at the retained pole: taking the sign
        // of a primitive based at v0 always selects the v0 cap. Choosing v1
        // is equivalent to adding the whole-carrier gauge contribution,
        // without cancellation when the retained cap is very small.
        // Balanced rims, including explicit pole lines, need no correction.
        let sign = if face.same_sense { 1.0 } else { -1.0 };
        if net_u.abs() == 1 {
            let from_upper = sign * net_u as f64 > 0.0;
            let mut values = winding_loops_integral_oriented(face, kinds, true, refine, false, from_upper, false)?;
            for value in &mut values {
                *value *= sign;
            }
            return Ok(Some(values));
        }
    }
    Ok(Some(winding_loops_integral(face, kinds, p_is_u, refine)?))
}

/// The boundary integral itself: `∮ −G du` (or `∮ H dv`) over every loop of
/// `face`, `G` the antiderivative across the cross parameter from the domain
/// start, every pcurve span cut at the surface's knot levels. Orientation is
/// normalised through the area, so the result is the material between the
/// winding loops read positively. `refine` subdivides every span (a
/// convergence check); the integrators call with 1.
///
/// The caller decides that the face's loops wind in `p_is_u` and that the
/// region between them is the one wanted: [`winding_band_integral_with`]
/// for a singly periodic surface (where the choice is forced), and the
/// bi-periodic band path (`biperiodic_band_integral`) for a torus strip
/// whose rims are not iso lines, where `complement` has already been read
/// off the rims' orientation and only the strip's quadrature is needed.
pub(super) fn winding_loops_integral(
    face: &FaceRecord,
    kinds: &[Integrand],
    p_is_u: bool,
    refine: usize,
) -> Result<Vec<f64>, String> {
    winding_loops_integral_oriented(face, kinds, p_is_u, refine, true, false, false)
}

pub(super) fn lifted_loops_integral(face: &FaceRecord, kinds: &[Integrand], p_is_u: bool, refine: usize) -> Result<Vec<f64>, String> {
    winding_loops_integral_oriented(face, kinds, p_is_u, refine, true, false, true)
}

fn winding_loops_integral_oriented(
    face: &FaceRecord,
    kinds: &[Integrand],
    p_is_u: bool,
    refine: usize,
    normalize: bool,
    from_upper: bool,
    periodic_cross: bool,
) -> Result<Vec<f64>, String> {
    let [u0, u1] = face.surface.domain_u()?;
    let [v0, v1] = face.surface.domain_v()?;
    let sign = if face.same_sense { 1.0 } else { -1.0 };
    let (u_breaks, v_breaks) = surface_breaks(&face.surface)?;
    let primitive = if periodic_cross { periodic_antiderivative } else { antiderivative };
    // Along a pcurve the integrand is `G(u(t), v(t))`, and `G` changes
    // polynomial piece wherever `u(t)` crosses one of the SURFACE's u-knots
    // (a rational sphere's four arcs, say); the pcurve's own knots know
    // nothing of those. A single-span pole line or rim over four surface
    // spans read by one Gauss rule was 0.3% off — so every span is cut at
    // the crossings too.
    let (level_breaks, level_period) = if p_is_u { (&u_breaks, u1 - u0) } else { (&v_breaks, v1 - v0) };
    // Every requested kind plus the area, which fixes the orientation.
    let all_kinds: Vec<Integrand> = kinds
        .iter()
        .copied()
        .chain(std::iter::once(Integrand::Area))
        .collect();
    let mut totals = vec![0.0f64; all_kinds.len()];
    let mut inner = vec![0.0f64; all_kinds.len()];
    // The antiderivative crosses the same knot spans at every station; its
    // weight roots are read once per cell here.
    let mut rules = rule::SurfaceRules::new(&face.surface)?;
    for loop_record in &face.loops {
        let mut joins = Vec::new();
        if switch!("BREP_MASS_TRIM_JOINS", false) {
            for (index, coedge) in loop_record.coedges.iter().enumerate() {
                let next = &loop_record.coedges[(index + 1) % loop_record.coedges.len()];
                let end = coedge.pcurve.evaluate(coedge.pcurve.domain()?[1])?;
                let mut start = next.pcurve.evaluate(next.pcurve.domain()?[0])?;
                // Reconnect in the covering chart. A whole-period seam jump
                // is not a closing chord, but the remaining real gap is.
                if p_is_u || periodic_cross {
                    start.x += ((end.x - start.x) / (u1 - u0)).round() * (u1 - u0);
                }
                if !p_is_u || periodic_cross {
                    start.y += ((end.y - start.y) / (v1 - v0)).round() * (v1 - v0);
                }
                // The primitive's differential is du (or dv). A join with
                // exactly zero travel in that direction contributes zero.
                if if p_is_u { end.x != start.x } else { end.y != start.y } {
                    joins.push(crate::make_line(
                        Vec3::new(end.x, end.y, 0.0), Vec3::new(start.x, start.y, 0.0),
                    )?);
                }
            }
        }
        for curve in loop_record.coedges.iter().map(|coedge| &coedge.pcurve).chain(joins.iter()) {
            let mut spans = split_at_surface_breaks(curve, curve_breaks(curve)?, level_breaks, level_period, p_is_u, true)?;
            if refine > 1 {
                let coarse = std::mem::take(&mut spans);
                for pair in coarse.windows(2) {
                    for k in 0..refine {
                        spans.push(pair[0] + (pair[1] - pair[0]) * k as f64 / refine as f64);
                    }
                }
                spans.push(*coarse.last().unwrap());
            }
            for pair in spans.windows(2) {
                // The pcurve's own weights decide this span's panels and
                // order; the antiderivative across the cross parameter asks
                // the SURFACE's weights the same question.
                for panel in rule::curve_panels(curve, pair[0], pair[1])? {
                    for (parameter, weight) in panel.stations() {
                        let (uv, tangent) = curve.deriv1(parameter)?;
                        if p_is_u {
                            primitive(face, &all_kinds, uv.x, if from_upper { v1 } else { v0 }, uv.y, &v_breaks, sign, true, &mut inner, &mut rules)?;
                            for (total, value) in totals.iter_mut().zip(&inner) {
                                *total -= weight * tangent.x * value;
                            }
                        } else {
                            primitive(face, &all_kinds, uv.y, u0, uv.x, &u_breaks, sign, false, &mut inner, &mut rules)?;
                            for (total, value) in totals.iter_mut().zip(&inner) {
                                *total += weight * tangent.y * value;
                            }
                        }
                    }
                }
            }
        }
    }
    let orientation = if normalize { totals[kinds.len()].signum() } else { 1.0 };
    Ok(totals[..kinds.len()].iter().map(|v| v * orientation).collect())
}

/// The gauge correction requires both poles of the spherical support, in
/// increasing latitude order. Partial carriers keep their existing reading.
fn complete_sphere(face: &FaceRecord) -> Result<bool, String> {
    let Some(crate::AnalyticSurface::Sphere { frame, radius }) = face.surface.analytic() else {
        return Ok(false);
    };
    let [u0, _] = face.surface.domain_u()?;
    let [v0, v1] = face.surface.domain_v()?;
    let south = face.surface.evaluate(u0, v0)?;
    let north = face.surface.evaluate(u0, v1)?;
    let pole = frame.axis.scale(*radius);
    let tolerance = 1e-8 * radius;
    Ok(south.sub(frame.origin.sub(pole)).length() <= tolerance
        && north.sub(frame.origin.add(pole)).length() <= tolerance)
}

/// The pcurve's spans cut further wherever its `u` (`take_u`) or `v`
/// coordinate crosses one of the surface's knot levels, taken modulo the
/// period so a pcurve carried beyond the domain (a crease crossing the seam)
/// meets the same levels. Crossings are bracketed on 32 samples per span
/// and bisected; a segment lying on a level is not a crossing.
///
/// `cut_on_level` decides what a sample sitting exactly ON a level is. For
/// this module's integrand it is a cut: the antiderivative `G` is taken
/// ACROSS the surface's knot spans, so a trim lying on a level changes piece
/// there. For a caller whose integrand is a function of the point and the
/// tangent alone — `brep/soundness.rs`'s vector-area closure — it is not:
/// nothing about the integrand changes ALONG a level, and cutting there turns
/// one iso-line trim into thirty-one spans that integrate to what one span
/// already read.
///
/// Both readings still cut where the trim CROSSES a level, which is the cut
/// that carries the accuracy: the surface's derivative jumps across a knot
/// line, so a Gauss rule read straight through one has a kink inside it.
/// Measured on the notched cap's `F7` (`vector_area_census document`, 18
/// faces), the three readings of the same shell:
///
/// ```text
/// no cut at a knot at all      residual 4.941978e-6   error bar 6.820e-7      0.72 s
/// cut at every on-level sample residual 5.106706e-6   error bar 4.196e-10   807,012 spans, 9.37 s
/// this, the lying-on skipped   residual 5.106706e-6   error bar 4.204e-10    29,428 spans, 0.34 s
/// ```
///
/// The residual is the same number to seven figures either way it is cut, and
/// the error bar under it is the same 4.2e-10; what the skip drops is 27x the
/// spans and 27x the wall clock. Without any cut the residual is only seven
/// times its own error bar, which is not a measurement.
pub(crate) fn split_at_surface_breaks(
    pcurve: &NurbsCurve,
    spans: Vec<f64>,
    levels: &[f64],
    period: f64,
    take_u: bool,
    cut_on_level: bool,
) -> Result<Vec<f64>, String> {
    let interior: Vec<f64> = levels[1..levels.len().saturating_sub(1)].to_vec();
    // The same levels, sorted, for the windowed walk below (`BREP_SURFACE_BREAKS_WINDOW`).
    let window = surface_breaks_window();
    let sorted: Vec<f64> = if window {
        let mut sorted: Vec<f64> = interior.iter().copied().chain(levels.first().copied()).collect();
        sorted.sort_by(f64::total_cmp);
        sorted
    } else {
        Vec::new()
    };
    let coordinate = |t: f64| -> Result<f64, String> {
        let uv = pcurve.evaluate(t)?;
        Ok(if take_u { uv.x } else { uv.y })
    };
    let mut out = Vec::with_capacity(spans.len() * 2);
    for pair in spans.windows(2) {
        let (a, b) = (pair[0], pair[1]);
        out.push(a);
        if b - a <= 0.0 {
            continue;
        }
        const SAMPLES: usize = 32;
        // Taken up front because the on-level decision below reads the NEXT
        // sample as well as the previous one.
        let mut sampled = Vec::with_capacity(SAMPLES + 1);
        for k in 0..=SAMPLES {
            sampled.push(coordinate(a + (b - a) * k as f64 / SAMPLES as f64)?);
        }
        let mut previous_t = a;
        let mut previous_c = sampled[0];
        let mut cuts = Vec::new();
        for k in 1..=SAMPLES {
            let t = a + (b - a) * k as f64 / SAMPLES as f64;
            let c = sampled[k];
            let (lo, hi) = (previous_c.min(c), previous_c.max(c));
            // Every level image `l + n·period` inside (lo, hi).
            let first = ((lo - levels[0]) / period).floor() as i64 - 1;
            let last = ((hi - levels[0]) / period).ceil() as i64 + 1;
            for n in first..=last {
                // Only a level whose image lies in [lo, hi] can pass either test
                // below (equal to a sample, or strictly between two). The window
                // walks just those, through the same tests in the same
                // arithmetic; the full walk reads every level of the surface for
                // every one of the 32 sub-spans of every span (most of the
                // closure gate's time on a dense carrier). The margin covers the
                // rounding of `l + n·period` against `l`.
                let candidates: &[f64] = if window {
                    let shift = n as f64 * period;
                    let margin = 1e-9 * (hi - lo) + 16.0 * f64::EPSILON * (lo.abs() + hi.abs() + shift.abs() + period.abs() + 1.0);
                    let (low, high) = (lo - shift - margin, hi - shift + margin);
                    let start = sorted.partition_point(|value| *value < low);
                    let end = sorted.partition_point(|value| *value <= high);
                    &sorted[start..end.max(start)]
                } else {
                    &[]
                };
                // The full walk: every interior level, then the first.
                let (head, tail): (&[f64], Option<&f64>) =
                    if window { (candidates, None) } else { (&interior[..], Some(&levels[0])) };
                for &l in head.iter().chain(tail) {
                    let level = l + n as f64 * period;
                    if c == level {
                        // A sample sitting on the level (a pole line's quarter
                        // points on 32 samples) is the crossing itself; the
                        // span ends are cut already.
                        //
                        // Which of the two readings applies is the caller's
                        // `cut_on_level`. For the antiderivative every such
                        // sample is a piece boundary. For a boundary integral
                        // only a sample where the curve LEAVES the level is —
                        // a neighbour off the level — so a trim lying along an
                        // iso-line takes no cut at all while one that crosses
                        // at a sample still takes its one. Without that second
                        // clause the crossing would be lost entirely: this
                        // sample pushes nothing and the next one sees
                        // `previous_c == level` and skips the bisection.
                        let leaves_the_level = previous_c != level
                            || sampled.get(k + 1).map(|next| *next != level).unwrap_or(true);
                        if k < SAMPLES && (cut_on_level || leaves_the_level) {
                            cuts.push(t);
                        }
                    } else if previous_c != level && (previous_c - level) * (c - level) < 0.0 {
                        let (mut t0, mut t1, mut c0) = (previous_t, t, previous_c);
                        for _ in 0..40 {
                            let tm = 0.5 * (t0 + t1);
                            let cm = coordinate(tm)?;
                            if (c0 - level) * (cm - level) <= 0.0 {
                                t1 = tm;
                            } else {
                                t0 = tm;
                                c0 = cm;
                            }
                        }
                        cuts.push(0.5 * (t0 + t1));
                    }
                }
            }
            previous_t = t;
            previous_c = c;
        }
        cuts.sort_by(|x, y| x.partial_cmp(y).unwrap());
        for cut in cuts {
            if cut - out.last().copied().unwrap_or(a) > 1e-12 * (b - a) && b - cut > 1e-12 * (b - a) {
                out.push(cut);
            }
        }
    }
    if let Some(&last) = spans.last() {
        out.push(last);
    }
    Ok(out)
}

/// `BREP_SURFACE_BREAKS_WINDOW` (default on; `0` walks every level): [`split_at_surface_breaks`]
/// walks, per sub-span, only the knot levels whose image can fall inside it
/// (binary search on the sorted levels) instead of every level of the surface.
/// The tests a level passes, the bisections and the cuts are the full walk's,
/// so the breaks are the same numbers.
fn surface_breaks_window() -> bool {
    crate::switch!("BREP_SURFACE_BREAKS_WINDOW", true)
}

/// `∫_{from}^{to} g(fixed, s) ds` (or `g(s, fixed)` when `across_v` is false)
/// for every kind, Gauss–Legendre over the surface's knot spans clipped to
/// the range; a station past the domain wraps through the periodic extension.
#[allow(clippy::too_many_arguments)]
pub(super) fn antiderivative(
    face: &FaceRecord,
    kinds: &[Integrand],
    fixed: f64,
    from: f64,
    to: f64,
    breaks: &[f64],
    sign: f64,
    across_v: bool,
    out: &mut [f64],
    rules: &mut rule::SurfaceRules,
) -> Result<(), String> {
    for value in out.iter_mut() {
        *value = 0.0;
    }
    let (lo, hi) = (from.min(to), from.max(to));
    if hi - lo <= 0.0 {
        return Ok(());
    }
    let direction = if to >= from { 1.0 } else { -1.0 };
    let eps = 1e-9 * (hi - lo);
    let mut edges = vec![lo];
    for &b in breaks {
        if b > lo + eps && b < hi - eps {
            edges.push(b);
        }
    }
    edges.push(hi);
    for pair in edges.windows(2) {
        let panels = rules.direction_panels(&face.surface, !across_v, pair[0], pair[1], fixed)?;
        for panel in panels {
            for (s, span_weight) in panel.stations() {
                let (u, v) = if across_v { (fixed, s) } else { (s, fixed) };
                let (point, su, sv) = face.surface.deriv1_extended(u, v)?;
                let weighted_normal = su.cross(sv).scale(sign);
                let weight = span_weight * direction;
                for (slot, kind) in kinds.iter().enumerate() {
                    out[slot] += weight * integrand_value(*kind, point, weighted_normal);
                }
            }
        }
    }
    Ok(())
}

/// Split the primitive at every periodic image of the domain seam, then use
/// the base-domain knot grid and weight rules on each piece. Both parameters
/// wrap: a translated trim must see the same rational quadrature as its image.
#[allow(clippy::too_many_arguments)]
fn periodic_antiderivative(
    face: &FaceRecord, kinds: &[Integrand], fixed: f64, from: f64, to: f64,
    breaks: &[f64], sign: f64, across_v: bool, out: &mut [f64],
    rules: &mut rule::SurfaceRules,
) -> Result<(), String> {
    out.fill(0.0);
    let domain = if across_v { face.surface.domain_u()? } else { face.surface.domain_v()? };
    let fixed = domain[0] + (fixed - domain[0]).rem_euclid(domain[1] - domain[0]);
    let base = breaks[0];
    let period = breaks[breaks.len() - 1] - base;
    let lo = from.min(to);
    let hi = from.max(to);
    let direction = if to >= from { 1.0 } else { -1.0 };
    let mut part = vec![0.0; kinds.len()];
    let first = ((lo - base) / period).floor() as i64;
    let last = ((hi - base) / period).ceil() as i64;
    for turn in first..last {
        let shift = turn as f64 * period;
        let a = lo.max(base + shift) - shift;
        let b = hi.min(base + period + shift) - shift;
        if b <= a { continue; }
        antiderivative(face, kinds, fixed, a, b, breaks, sign, across_v, &mut part, rules)?;
        for (total, value) in out.iter_mut().zip(&part) { *total += direction * value; }
    }
    Ok(())
}