Coverage Report

Created: 2026-09-02 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/luv.rs
Line
Count
Source
1
/*
2
 * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved.
3
 * //
4
 * // Use of this source code is governed by a BSD-style
5
 * // license that can be found in the LICENSE file.
6
 */
7
8
//! # Luv
9
/// Struct representing a color in CIE LUV, a.k.a. L\*u\*v\*, color space
10
#[repr(C)]
11
#[derive(Debug, Copy, Clone, Default, PartialOrd)]
12
pub struct Luv {
13
    /// The L\* value (achromatic luminance) of the colour in 0–100 range.
14
    pub l: f32,
15
    /// The u\* value of the colour.
16
    ///
17
    /// Together with v\* value, it defines chromaticity of the colour.  The u\*
18
    /// coordinate represents colour’s position on red-green axis with negative
19
    /// values indicating more red and positive more green colour.  Typical
20
    /// values are in -134–220 range (but exact range for ‘valid’ colours
21
    /// depends on luminance and v\* value).
22
    pub u: f32,
23
    /// The u\* value of the colour.
24
    ///
25
    /// Together with u\* value, it defines chromaticity of the colour.  The v\*
26
    /// coordinate represents colour’s position on blue-yellow axis with
27
    /// negative values indicating more blue and positive more yellow colour.
28
    /// Typical values are in -140–122 range (but exact range for ‘valid’
29
    /// colours depends on luminance and u\* value).
30
    pub v: f32,
31
}
32
33
/// Representing a color in cylindrical CIE LCh(uv) color space
34
#[repr(C)]
35
#[derive(Debug, Copy, Clone, Default, PartialOrd)]
36
pub struct LCh {
37
    /// The L\* value (achromatic luminance) of the colour in 0–100 range.
38
    ///
39
    /// This is the same value as in the [`Luv`] object.
40
    pub l: f32,
41
    /// The C\*_uv value (chroma) of the colour.
42
    ///
43
    /// Together with h_uv, it defines chromaticity of the colour.  The typical
44
    /// values of the coordinate go from zero up to around 150 (but exact range
45
    /// for ‘valid’ colours depends on luminance and hue).  Zero represents
46
    /// shade of grey.
47
    pub c: f32,
48
    /// The h_uv value (hue) of the colour measured in radians.
49
    ///
50
    /// Together with C\*_uv, it defines chromaticity of the colour.  The value
51
    /// represents an angle thus it wraps around τ.  Typically, the value will
52
    /// be in the -π–π range.  The value is undefined if C\*_uv is zero.
53
    pub h: f32,
54
}
55
56
use crate::mlaf::mlaf;
57
use crate::{Chromaticity, Lab, Xyz};
58
use num_traits::Pow;
59
use pxfm::{f_atan2f, f_cbrtf, f_hypotf, f_powf, f_sincosf};
60
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
61
62
pub(crate) const LUV_WHITE_U_PRIME: f32 = 4.0f32 * Chromaticity::D50.to_xyz().y
63
    / (Chromaticity::D50.to_xyz().x
64
        + 15.0 * Chromaticity::D50.to_xyz().y
65
        + 3.0 * Chromaticity::D50.to_xyz().z);
66
pub(crate) const LUV_WHITE_V_PRIME: f32 = 9.0f32 * Chromaticity::D50.to_xyz().y
67
    / (Chromaticity::D50.to_xyz().x
68
        + 15.0 * Chromaticity::D50.to_xyz().y
69
        + 3.0 * Chromaticity::D50.to_xyz().z);
70
71
pub(crate) const LUV_CUTOFF_FORWARD_Y: f32 = (6f32 / 29f32) * (6f32 / 29f32) * (6f32 / 29f32);
72
pub(crate) const LUV_MULTIPLIER_FORWARD_Y: f32 = (29f32 / 3f32) * (29f32 / 3f32) * (29f32 / 3f32);
73
pub(crate) const LUV_MULTIPLIER_INVERSE_Y: f32 = (3f32 / 29f32) * (3f32 / 29f32) * (3f32 / 29f32);
74
impl Luv {
75
    /// Converts CIE XYZ to CIE Luv using D50 white point
76
    #[inline]
77
    #[allow(clippy::manual_clamp)]
78
0
    pub fn from_xyz(xyz: Xyz) -> Self {
79
0
        let [x, y, z] = [xyz.x, xyz.y, xyz.z];
80
0
        let den = mlaf(mlaf(x, 15.0, y), 3.0, z);
81
82
0
        let l = (if y < LUV_CUTOFF_FORWARD_Y {
83
0
            LUV_MULTIPLIER_FORWARD_Y * y
84
        } else {
85
0
            116. * f_cbrtf(y) - 16.
86
        })
87
0
        .min(100.)
88
0
        .max(0.);
89
        let (u, v);
90
0
        if den != 0f32 {
91
0
            let u_prime = 4. * x / den;
92
0
            let v_prime = 9. * y / den;
93
0
            u = 13. * l * (u_prime - LUV_WHITE_U_PRIME);
94
0
            v = 13. * l * (v_prime - LUV_WHITE_V_PRIME);
95
0
        } else {
96
0
            u = 0.;
97
0
            v = 0.;
98
0
        }
99
100
0
        Luv { l, u, v }
101
0
    }
102
103
    /// To [Xyz] using D50 colorimetry
104
    #[inline]
105
0
    pub fn to_xyz(&self) -> Xyz {
106
0
        if self.l <= 0. {
107
0
            return Xyz::new(0., 0., 0.);
108
0
        }
109
0
        let l13 = 1. / (13. * self.l);
110
0
        let u = mlaf(LUV_WHITE_U_PRIME, self.u, l13);
111
0
        let v = mlaf(LUV_WHITE_V_PRIME, self.v, l13);
112
0
        let y = if self.l > 8. {
113
0
            let jx = (self.l + 16.) / 116.;
114
0
            jx * jx * jx
115
        } else {
116
0
            self.l * LUV_MULTIPLIER_INVERSE_Y
117
        };
118
        let (x, z);
119
0
        if v != 0. {
120
0
            let den = 1. / (4. * v);
121
0
            x = y * 9. * u * den;
122
0
            z = y * mlaf(mlaf(12.0, -3.0, u), -20., v) * den;
123
0
        } else {
124
0
            x = 0.;
125
0
            z = 0.;
126
0
        }
127
128
0
        Xyz::new(x, y, z)
129
0
    }
130
131
    #[inline]
132
0
    pub const fn new(l: f32, u: f32, v: f32) -> Luv {
133
0
        Luv { l, u, v }
134
0
    }
135
}
136
137
impl LCh {
138
    #[inline]
139
0
    pub const fn new(l: f32, c: f32, h: f32) -> Self {
140
0
        LCh { l, c, h }
141
0
    }
142
143
    /// Converts Lab to LCh(uv)
144
    #[inline]
145
0
    pub fn from_luv(luv: Luv) -> Self {
146
0
        LCh {
147
0
            l: luv.l,
148
0
            c: f_hypotf(luv.u, luv.v),
149
0
            h: f_atan2f(luv.v, luv.u),
150
0
        }
151
0
    }
152
153
    /// Converts Lab to LCh(ab)
154
    #[inline]
155
0
    pub fn from_lab(lab: Lab) -> Self {
156
0
        LCh {
157
0
            l: lab.l,
158
0
            c: f_hypotf(lab.a, lab.b),
159
0
            h: f_atan2f(lab.b, lab.a),
160
0
        }
161
0
    }
162
163
    /// Computes LCh(uv)
164
    #[inline]
165
0
    pub fn from_xyz(xyz: Xyz) -> Self {
166
0
        Self::from_luv(Luv::from_xyz(xyz))
167
0
    }
168
169
    /// Computes LCh(ab)
170
    #[inline]
171
0
    pub fn from_xyz_lab(xyz: Xyz) -> Self {
172
0
        Self::from_lab(Lab::from_xyz(xyz))
173
0
    }
174
175
    /// Converts LCh(uv) to Luv
176
    #[inline]
177
0
    pub fn to_xyz(&self) -> Xyz {
178
0
        self.to_luv().to_xyz()
179
0
    }
180
181
    /// Converts LCh(ab) to Lab
182
    #[inline]
183
0
    pub fn to_xyz_lab(&self) -> Xyz {
184
0
        self.to_lab().to_xyz()
185
0
    }
186
187
    #[inline]
188
0
    pub fn to_luv(&self) -> Luv {
189
0
        let sincos = f_sincosf(self.h);
190
0
        Luv {
191
0
            l: self.l,
192
0
            u: self.c * sincos.1,
193
0
            v: self.c * sincos.0,
194
0
        }
195
0
    }
196
197
    #[inline]
198
0
    pub fn to_lab(&self) -> Lab {
199
0
        let sincos = f_sincosf(self.h);
200
0
        Lab {
201
0
            l: self.l,
202
0
            a: self.c * sincos.1,
203
0
            b: self.c * sincos.0,
204
0
        }
205
0
    }
206
}
207
208
impl PartialEq<Luv> for Luv {
209
    /// Compares two colours ignoring chromaticity if L\* is zero.
210
    #[inline]
211
0
    fn eq(&self, other: &Self) -> bool {
212
0
        if self.l != other.l {
213
0
            false
214
0
        } else if self.l == 0.0 {
215
0
            true
216
        } else {
217
0
            self.u == other.u && self.v == other.v
218
        }
219
0
    }
220
}
221
222
impl PartialEq<LCh> for LCh {
223
    /// Compares two colors ignoring chromaticity if L\* is zero and hue if C\*
224
    /// is zero.  Hues which are τ apart are compared equal.
225
    #[inline]
226
0
    fn eq(&self, other: &Self) -> bool {
227
0
        if self.l != other.l {
228
0
            false
229
0
        } else if self.l == 0.0 {
230
0
            true
231
0
        } else if self.c != other.c {
232
0
            false
233
0
        } else if self.c == 0.0 {
234
0
            true
235
        } else {
236
            use std::f32::consts::TAU;
237
0
            self.h.rem_euclid(TAU) == other.h.rem_euclid(TAU)
238
        }
239
0
    }
240
}
241
242
impl Luv {
243
    #[inline]
244
0
    pub fn euclidean_distance(&self, other: Luv) -> f32 {
245
0
        let dl = self.l - other.l;
246
0
        let du = self.u - other.u;
247
0
        let dv = self.v - other.v;
248
0
        (dl * dl + du * du + dv * dv).sqrt()
249
0
    }
250
}
251
252
impl LCh {
253
    #[inline]
254
0
    pub fn euclidean_distance(&self, other: LCh) -> f32 {
255
0
        let dl = self.l - other.l;
256
0
        let dc = self.c - other.c;
257
0
        let dh = self.h - other.h;
258
0
        (dl * dl + dc * dc + dh * dh).sqrt()
259
0
    }
260
}
261
262
impl Luv {
263
    #[inline]
264
0
    pub const fn taxicab_distance(&self, other: Self) -> f32 {
265
0
        let dl = self.l - other.l;
266
0
        let du = self.u - other.u;
267
0
        let dv = self.v - other.v;
268
0
        dl.abs() + du.abs() + dv.abs()
269
0
    }
270
}
271
272
impl LCh {
273
    #[inline]
274
0
    pub const fn taxicab_distance(&self, other: Self) -> f32 {
275
0
        let dl = self.l - other.l;
276
0
        let dc = self.c - other.c;
277
0
        let dh = self.h - other.h;
278
0
        dl.abs() + dc.abs() + dh.abs()
279
0
    }
280
}
281
282
impl Add<Luv> for Luv {
283
    type Output = Luv;
284
285
    #[inline]
286
0
    fn add(self, rhs: Luv) -> Luv {
287
0
        Luv::new(self.l + rhs.l, self.u + rhs.u, self.v + rhs.v)
288
0
    }
289
}
290
291
impl Add<LCh> for LCh {
292
    type Output = LCh;
293
294
    #[inline]
295
0
    fn add(self, rhs: LCh) -> LCh {
296
0
        LCh::new(self.l + rhs.l, self.c + rhs.c, self.h + rhs.h)
297
0
    }
298
}
299
300
impl Sub<Luv> for Luv {
301
    type Output = Luv;
302
303
    #[inline]
304
0
    fn sub(self, rhs: Luv) -> Luv {
305
0
        Luv::new(self.l - rhs.l, self.u - rhs.u, self.v - rhs.v)
306
0
    }
307
}
308
309
impl Sub<LCh> for LCh {
310
    type Output = LCh;
311
312
    #[inline]
313
0
    fn sub(self, rhs: LCh) -> LCh {
314
0
        LCh::new(self.l - rhs.l, self.c - rhs.c, self.h - rhs.h)
315
0
    }
316
}
317
318
impl Mul<Luv> for Luv {
319
    type Output = Luv;
320
321
    #[inline]
322
0
    fn mul(self, rhs: Luv) -> Luv {
323
0
        Luv::new(self.l * rhs.l, self.u * rhs.u, self.v * rhs.v)
324
0
    }
325
}
326
327
impl Mul<LCh> for LCh {
328
    type Output = LCh;
329
330
    #[inline]
331
0
    fn mul(self, rhs: LCh) -> LCh {
332
0
        LCh::new(self.l * rhs.l, self.c * rhs.c, self.h * rhs.h)
333
0
    }
334
}
335
336
impl Div<Luv> for Luv {
337
    type Output = Luv;
338
339
    #[inline]
340
0
    fn div(self, rhs: Luv) -> Luv {
341
0
        Luv::new(self.l / rhs.l, self.u / rhs.u, self.v / rhs.v)
342
0
    }
343
}
344
345
impl Div<LCh> for LCh {
346
    type Output = LCh;
347
348
    #[inline]
349
0
    fn div(self, rhs: LCh) -> LCh {
350
0
        LCh::new(self.l / rhs.l, self.c / rhs.c, self.h / rhs.h)
351
0
    }
352
}
353
354
impl Add<f32> for Luv {
355
    type Output = Luv;
356
357
    #[inline]
358
0
    fn add(self, rhs: f32) -> Self::Output {
359
0
        Luv::new(self.l + rhs, self.u + rhs, self.v + rhs)
360
0
    }
361
}
362
363
impl Add<f32> for LCh {
364
    type Output = LCh;
365
366
    #[inline]
367
0
    fn add(self, rhs: f32) -> Self::Output {
368
0
        LCh::new(self.l + rhs, self.c + rhs, self.h + rhs)
369
0
    }
370
}
371
372
impl Sub<f32> for Luv {
373
    type Output = Luv;
374
375
    #[inline]
376
0
    fn sub(self, rhs: f32) -> Self::Output {
377
0
        Luv::new(self.l - rhs, self.u - rhs, self.v - rhs)
378
0
    }
379
}
380
381
impl Sub<f32> for LCh {
382
    type Output = LCh;
383
384
    #[inline]
385
0
    fn sub(self, rhs: f32) -> Self::Output {
386
0
        LCh::new(self.l - rhs, self.c - rhs, self.h - rhs)
387
0
    }
388
}
389
390
impl Mul<f32> for Luv {
391
    type Output = Luv;
392
393
    #[inline]
394
0
    fn mul(self, rhs: f32) -> Self::Output {
395
0
        Luv::new(self.l * rhs, self.u * rhs, self.v * rhs)
396
0
    }
397
}
398
399
impl Mul<f32> for LCh {
400
    type Output = LCh;
401
402
    #[inline]
403
0
    fn mul(self, rhs: f32) -> Self::Output {
404
0
        LCh::new(self.l * rhs, self.c * rhs, self.h * rhs)
405
0
    }
406
}
407
408
impl Div<f32> for Luv {
409
    type Output = Luv;
410
411
    #[inline]
412
0
    fn div(self, rhs: f32) -> Self::Output {
413
0
        Luv::new(self.l / rhs, self.u / rhs, self.v / rhs)
414
0
    }
415
}
416
417
impl Div<f32> for LCh {
418
    type Output = LCh;
419
420
    #[inline]
421
0
    fn div(self, rhs: f32) -> Self::Output {
422
0
        LCh::new(self.l / rhs, self.c / rhs, self.h / rhs)
423
0
    }
424
}
425
426
impl AddAssign<Luv> for Luv {
427
    #[inline]
428
0
    fn add_assign(&mut self, rhs: Luv) {
429
0
        self.l += rhs.l;
430
0
        self.u += rhs.u;
431
0
        self.v += rhs.v;
432
0
    }
433
}
434
435
impl AddAssign<LCh> for LCh {
436
    #[inline]
437
0
    fn add_assign(&mut self, rhs: LCh) {
438
0
        self.l += rhs.l;
439
0
        self.c += rhs.c;
440
0
        self.h += rhs.h;
441
0
    }
442
}
443
444
impl SubAssign<Luv> for Luv {
445
    #[inline]
446
0
    fn sub_assign(&mut self, rhs: Luv) {
447
0
        self.l -= rhs.l;
448
0
        self.u -= rhs.u;
449
0
        self.v -= rhs.v;
450
0
    }
451
}
452
453
impl SubAssign<LCh> for LCh {
454
    #[inline]
455
0
    fn sub_assign(&mut self, rhs: LCh) {
456
0
        self.l -= rhs.l;
457
0
        self.c -= rhs.c;
458
0
        self.h -= rhs.h;
459
0
    }
460
}
461
462
impl MulAssign<Luv> for Luv {
463
    #[inline]
464
0
    fn mul_assign(&mut self, rhs: Luv) {
465
0
        self.l *= rhs.l;
466
0
        self.u *= rhs.u;
467
0
        self.v *= rhs.v;
468
0
    }
469
}
470
471
impl MulAssign<LCh> for LCh {
472
    #[inline]
473
0
    fn mul_assign(&mut self, rhs: LCh) {
474
0
        self.l *= rhs.l;
475
0
        self.c *= rhs.c;
476
0
        self.h *= rhs.h;
477
0
    }
478
}
479
480
impl DivAssign<Luv> for Luv {
481
    #[inline]
482
0
    fn div_assign(&mut self, rhs: Luv) {
483
0
        self.l /= rhs.l;
484
0
        self.u /= rhs.u;
485
0
        self.v /= rhs.v;
486
0
    }
487
}
488
489
impl DivAssign<LCh> for LCh {
490
    #[inline]
491
0
    fn div_assign(&mut self, rhs: LCh) {
492
0
        self.l /= rhs.l;
493
0
        self.c /= rhs.c;
494
0
        self.h /= rhs.h;
495
0
    }
496
}
497
498
impl AddAssign<f32> for Luv {
499
    #[inline]
500
0
    fn add_assign(&mut self, rhs: f32) {
501
0
        self.l += rhs;
502
0
        self.u += rhs;
503
0
        self.v += rhs;
504
0
    }
505
}
506
507
impl AddAssign<f32> for LCh {
508
    #[inline]
509
0
    fn add_assign(&mut self, rhs: f32) {
510
0
        self.l += rhs;
511
0
        self.c += rhs;
512
0
        self.h += rhs;
513
0
    }
514
}
515
516
impl SubAssign<f32> for Luv {
517
    #[inline]
518
0
    fn sub_assign(&mut self, rhs: f32) {
519
0
        self.l -= rhs;
520
0
        self.u -= rhs;
521
0
        self.v -= rhs;
522
0
    }
523
}
524
525
impl SubAssign<f32> for LCh {
526
    #[inline]
527
0
    fn sub_assign(&mut self, rhs: f32) {
528
0
        self.l -= rhs;
529
0
        self.c -= rhs;
530
0
        self.h -= rhs;
531
0
    }
532
}
533
534
impl MulAssign<f32> for Luv {
535
    #[inline]
536
0
    fn mul_assign(&mut self, rhs: f32) {
537
0
        self.l *= rhs;
538
0
        self.u *= rhs;
539
0
        self.v *= rhs;
540
0
    }
541
}
542
543
impl MulAssign<f32> for LCh {
544
    #[inline]
545
0
    fn mul_assign(&mut self, rhs: f32) {
546
0
        self.l *= rhs;
547
0
        self.c *= rhs;
548
0
        self.h *= rhs;
549
0
    }
550
}
551
552
impl DivAssign<f32> for Luv {
553
    #[inline]
554
0
    fn div_assign(&mut self, rhs: f32) {
555
0
        self.l /= rhs;
556
0
        self.u /= rhs;
557
0
        self.v /= rhs;
558
0
    }
559
}
560
561
impl DivAssign<f32> for LCh {
562
    #[inline]
563
0
    fn div_assign(&mut self, rhs: f32) {
564
0
        self.l /= rhs;
565
0
        self.c /= rhs;
566
0
        self.h /= rhs;
567
0
    }
568
}
569
570
impl Neg for LCh {
571
    type Output = LCh;
572
573
    #[inline]
574
0
    fn neg(self) -> Self::Output {
575
0
        LCh::new(-self.l, -self.c, -self.h)
576
0
    }
577
}
578
579
impl Neg for Luv {
580
    type Output = Luv;
581
582
    #[inline]
583
0
    fn neg(self) -> Self::Output {
584
0
        Luv::new(-self.l, -self.u, -self.v)
585
0
    }
586
}
587
588
impl Pow<f32> for Luv {
589
    type Output = Luv;
590
591
    #[inline]
592
0
    fn pow(self, rhs: f32) -> Self::Output {
593
0
        Luv::new(
594
0
            f_powf(self.l, rhs),
595
0
            f_powf(self.u, rhs),
596
0
            f_powf(self.v, rhs),
597
        )
598
0
    }
599
}
600
601
impl Pow<f32> for LCh {
602
    type Output = LCh;
603
604
    #[inline]
605
0
    fn pow(self, rhs: f32) -> Self::Output {
606
0
        LCh::new(
607
0
            f_powf(self.l, rhs),
608
0
            f_powf(self.c, rhs),
609
0
            f_powf(self.h, rhs),
610
        )
611
0
    }
612
}
613
614
impl Pow<Luv> for Luv {
615
    type Output = Luv;
616
617
    #[inline]
618
0
    fn pow(self, rhs: Luv) -> Self::Output {
619
0
        Luv::new(
620
0
            f_powf(self.l, rhs.l),
621
0
            f_powf(self.u, rhs.u),
622
0
            f_powf(self.v, rhs.v),
623
        )
624
0
    }
625
}
626
627
impl Pow<LCh> for LCh {
628
    type Output = LCh;
629
630
    #[inline]
631
0
    fn pow(self, rhs: LCh) -> Self::Output {
632
0
        LCh::new(
633
0
            f_powf(self.l, rhs.l),
634
0
            f_powf(self.c, rhs.c),
635
0
            f_powf(self.h, rhs.h),
636
        )
637
0
    }
638
}
639
640
impl Luv {
641
    #[inline]
642
0
    pub fn sqrt(&self) -> Luv {
643
0
        Luv::new(self.l.sqrt(), self.u.sqrt(), self.v.sqrt())
644
0
    }
645
646
    #[inline]
647
0
    pub fn cbrt(&self) -> Luv {
648
0
        Luv::new(f_cbrtf(self.l), f_cbrtf(self.u), f_cbrtf(self.v))
649
0
    }
650
}
651
652
impl LCh {
653
    #[inline]
654
0
    pub fn sqrt(&self) -> LCh {
655
0
        LCh::new(
656
0
            if self.l < 0. { 0. } else { self.l.sqrt() },
657
0
            if self.c < 0. { 0. } else { self.c.sqrt() },
658
0
            if self.h < 0. { 0. } else { self.h.sqrt() },
659
        )
660
0
    }
661
662
    #[inline]
663
0
    pub fn cbrt(&self) -> LCh {
664
0
        LCh::new(f_cbrtf(self.l), f_cbrtf(self.c), f_cbrtf(self.h))
665
0
    }
666
}
667
668
#[cfg(test)]
669
mod tests {
670
    use super::*;
671
672
    #[test]
673
    fn round_trip_luv() {
674
        let xyz = Xyz::new(0.1, 0.2, 0.3);
675
        let lab = Luv::from_xyz(xyz);
676
        let rolled_back = lab.to_xyz();
677
        let dx = (xyz.x - rolled_back.x).abs();
678
        let dy = (xyz.y - rolled_back.y).abs();
679
        let dz = (xyz.z - rolled_back.z).abs();
680
        assert!(dx < 1e-5);
681
        assert!(dy < 1e-5);
682
        assert!(dz < 1e-5);
683
    }
684
685
    #[test]
686
    fn round_trip_lch() {
687
        let xyz = Xyz::new(0.1, 0.2, 0.3);
688
        let luv = Luv::from_xyz(xyz);
689
        let lab = LCh::from_luv(luv);
690
        let rolled_back = lab.to_luv();
691
        let dx = (luv.l - rolled_back.l).abs();
692
        let dy = (luv.u - rolled_back.u).abs();
693
        let dz = (luv.v - rolled_back.v).abs();
694
        assert!(dx < 1e-4);
695
        assert!(dy < 1e-4);
696
        assert!(dz < 1e-4);
697
    }
698
}