offroad 0.5.7

2D offsetting for arc polylines/polygons.
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
#![allow(dead_code)]

use togo::prelude::*;

use crate::offsetraw::OffsetRaw;

const ZERO: f64 = 0f64;

pub fn offset_polyline_raw(plines: &Vec<Vec<OffsetRaw>>, off: f64) -> Vec<Vec<OffsetRaw>> {
    let mut result = Vec::new();
    for pline in plines.iter() {
        result.push(offset_polyline_raw_single(pline, off));
    }
    result
}

fn offset_polyline_raw_single(pline: &Vec<OffsetRaw>, off: f64) -> Vec<OffsetRaw> {
    let mut result = Vec::with_capacity(pline.len());
    for p in pline.iter() {
        let offset = offset_segment(&p.arc, p.orig, p.g, off);
        result.push(offset);
    }
    result
}

pub(crate) fn offset_segment(seg: &Arc, orig: Point, g: f64, off: f64) -> OffsetRaw {
    if seg.is_seg() {
        line_offset(seg, orig, off)
    } else {
        arc_offset(seg, orig, g, off)
    }
}

// Offsets line segment on right side
// #00028
fn line_offset(seg: &Arc, orig: Point, offset: f64) -> OffsetRaw {
    // line segment
    let perp = seg.b - seg.a;
    let (perp, _) = point(perp.y, -perp.x).normalize(false);
    let offset_vec = perp * offset;
    let mut arc = arcseg(seg.a + offset_vec, seg.b + offset_vec);
    arc.id(seg.id);
    return OffsetRaw {
        arc,
        orig: orig,
        g: ZERO,
    };
}

const EPS_COLLAPSED: f64 = 1E-10; // TODO: what should be the exact value.
// Offsets arc on right side
// #00028
fn arc_offset(seg: &Arc, orig: Point, bulge: f64, offset: f64) -> OffsetRaw {
    // Arc is always CCW
    //let seg = arc_from_bulge(seg.a, seg.b, bulge);
    let (v0_to_center, _) = (seg.a - seg.c).normalize(false);
    let (v1_to_center, _) = (seg.b - seg.c).normalize(false);

    // For negative bulge (reversed arc), negate the offset to maintain "right side" direction
    let off = if bulge < ZERO { -offset } else { offset };
    let offset_radius = seg.r + off;
    let a = seg.a + v0_to_center * off;
    let b = seg.b + v1_to_center * off;
    if offset_radius < EPS_COLLAPSED || offset_radius.is_nan() || a.close_enough(b, EPS_COLLAPSED) {
        // Collapsed arc is now line
        let mut arc = arcseg(b, a);
        arc.id(seg.id);
        return OffsetRaw {
            arc: arc,
            orig: orig,
            g: ZERO,
        };
    } else {
        let mut arc = arc(a, b, seg.c, offset_radius);
        arc.id(seg.id);
        OffsetRaw {
            arc: arc,
            orig: orig,
            g: bulge,
        }
    }
}

pub fn poly_to_raws(plines: &Vec<Polyline>) -> Vec<Vec<OffsetRaw>> {
    let mut varcs: Vec<Vec<OffsetRaw>> = Vec::new();
    for pline in plines {
        varcs.push(poly_to_raws_single(pline));
    }
    varcs
}

pub fn poly_to_raws_single(pline: &Polyline) -> Vec<OffsetRaw> {
    let mut offs = Vec::with_capacity(pline.len());
    let n = pline.len();

    // Cyclic loop: for each vertex i, create arc from vertex i to vertex (i+1) mod n
    for i in 0..n {
        let bulge = pline[i].b;
        let next_i = (i + 1) % n; // Cyclic wrap-around
        let seg = arc_from_bulge(pline[i].p, pline[next_i].p, bulge);
        let check = seg.is_valid(EPS_COLLAPSED);
        if !check {
            continue;
        }
        let orig = if bulge < ZERO { seg.a } else { seg.b };
        let off = OffsetRaw {
            arc: seg,
            orig: orig,
            g: bulge,
        };
        offs.push(off);
    }

    offs
}

pub fn arcs_to_raws(arcss: &Vec<Arcline>) -> Vec<Vec<OffsetRaw>> {
    let mut varcs: Vec<Vec<OffsetRaw>> = Vec::new();
    for arcs in arcss {
        varcs.push(arcs_to_raws_single(arcs));
    }
    varcs
}

pub fn arcs_to_raws_single(arcs: &Arcline) -> Vec<OffsetRaw> {
    let mut offs = Vec::with_capacity(arcs.len());
    let n = arcs.len();

    // Cyclic loop: for each arc i, process it (arcs are already connected in sequence)
    for i in 0..n {
        let seg = arcs[i];
        let check = seg.is_valid(EPS_COLLAPSED);
        if !check {
            continue;
        }

        if seg.is_seg() {
            let off = OffsetRaw {
                arc: seg,
                orig: seg.b,
                g: ZERO,  // Line segments have no bulge
            };
            offs.push(off);
            continue;
        }

        // Determine bulge sign from connectivity with next arc
        // All arcs in togo are CCW, so bulge_from_arc() always returns positive
        // We need to check the direction based on how the arc connects to the next arc
        let next_i = (i + 1) % n;
        let next_seg = arcs[next_i];

        // try to get arc orientation by checking the connection to the next arc
        let seg_b_to_next_a = seg.b == next_seg.a;
        let seg_b_to_next_b = seg.b == next_seg.b;

        // Determine bulge sign (just ±1, not actual magnitude)
        // to identify which endpoint connects to next arc
        let bulge = if seg_b_to_next_a || seg_b_to_next_b {
            // seg.b connects to next arc -> seg is normal (positive bulge)
            1.0
        } else {
            // seg.a connects to next arc -> seg is reversed (negative bulge)
            -1.0
        };

        let orig = if bulge < ZERO { seg.a } else { seg.b };
        let off = OffsetRaw {
            arc: seg,
            orig: orig,
            g: bulge,
        };
        offs.push(off);
    }

    offs
}

#[cfg(test)]
mod test_offset_polyline_raw {
    use togo::prelude::*;

    use crate::offsetraw::offsetraw;

    use super::*;

    #[test]
    fn test_arc_offset_collapsed_arc() {
        // let arc0 = arc();
        // let res = arc_offset(
        //     pvertex(point(0.0, 0.0), -1.0),
        //     pvertex(point(1.0, 0.0), 0.0),
        //     1.0,
        // );
        // assert_eq!(
        //     res,
        //     OffsetRaw {
        //         arc: arcline(point(1.0, 0.0), point(0.0, 0.0)),
        //         orig: point(1.0, 0.0),
        //         g: 0.0
        //     }
        // );
    }

    #[test]
    fn test_new() {
        let arc = arc_from_bulge(point(1.0, 2.0), point(3.0, 4.0), 3.3);
        let o0 = offsetraw(arc, point(5.0, 6.0), 3.3);
        let o1 = offsetraw(arc, point(5.0, 6.0), 3.3);
        assert_eq!(o0, o1);
    }

    #[test]
    fn test_display_01() {
        let arc = arc_from_bulge(point(0.0, 0.0), point(2.0, 2.0), 1.0);
        let o0 = offsetraw(arc, point(5.0, 6.0), 3.3);
        assert_eq!(
            "[[[0.00000000000000000000, 0.00000000000000000000], [2.00000000000000000000, 2.00000000000000000000], [1.00000000000000000000, 1.00000000000000000000], 1.41421356237309514547], [5.00000000000000000000, 6.00000000000000000000], 3.3]",
            format!("{}", o0)
        );
    }

    #[test]
    fn test_display_02() {
        let arc = arc_from_bulge(point(1.0, 2.0), point(3.0, 4.0), 3.3);
        let o0 = offsetraw(arc, point(5.0, 6.0), 3.3);
        assert_eq!(
            "[[[1.00000000000000000000, 2.00000000000000000000], [3.00000000000000000000, 4.00000000000000000000], [3.49848484848484808651, 1.50151515151515169144], 2.54772716009334887488], [5.00000000000000000000, 6.00000000000000000000], 3.3]",
            format!("{}", o0)
        );
    }

    #[test]
    fn test_line_offset_vertical() {
        // vertical segment
        // let seg = arcline(point(2.0, 1.0), point(2.0, 11.0));
        // let res = offsetraw(
        //     arcline(point(3.0, 1.0), point(3.0, 11.0)),
        //     point(2.0, 11.0),
        //     0.0,
        // );
        // assert_eq!(line_offset(&seg, 1.0), res);
    }
    #[test]
    fn test_line_offset_horizontal() {
        // horizontal segment
        // let seg = arcline(point(-2.0, 1.0), point(3.0, 1.0));
        // let res = offsetraw(
        //     arcline(point(-2.0, -1.0), point(3.0, -1.0)),
        //     point(3.0, 1.0),
        //     0.0,
        // );
        // assert_eq!(line_offset(&seg, 2.0), res);
    }
    #[test]
    fn test_line_offset_diagonal() {
        // diagonal segment
        // let seg = arcline(point(-1.0, 1.0), point(2.0, 2.0));
        // let res = offsetraw(
        //     arcline(point(0.0, 2.0), point(-1.0, 3.0)),
        //     point(-2.0, 2.0),
        //     0.0,
        // );
        // assert_eq!(line_offset(&seg, std::f64::consts::SQRT_2), res);
    }

    #[test]
    //#[ignore = "svg output"]
    fn test_offset_polyline_raw02() {
        // let pline = vec![
        //     pvertex(point(100.0, 100.0), 0.5),
        //     pvertex(point(200.0, 100.0), -0.5),
        //     pvertex(point(200.0, 200.0), 0.5),
        //     pvertex(point(100.0, 200.0), -0.5),
        // ];
        // let plines = vec![pline.clone()];
        // let mut svg = svg(400.0, 600.0);
        // //let pline = polyline_translate(&pline, point(0.0, 100.0));
        // svg.polyline(&pline, "red");

        // //let pline = polyline_reverse(&pline);
        // let off: f64 = 52.25;
        // let offset_raw = offset_polyline_raw(&plines, off);
        // svg.offset_raws(&offset_raw, "blue");
        // svg.write();
    }

    // #[test]
    // //#[ignore = "svg output"]
    // fn test_offset_polyline_raw03() {
    //     let plines = pline_01();
    //     let mut svg = svg(400.0, 600.0);
    //     let plines = vec![pline.clone()];
    //     svg.polyline(&pline, "red");

    //     //let pline = polyline_reverse(&pline);
    //     let off: f64 = 52.25;
    //     let offset_raw = offset_polyline_raw(&plines, off);
    //     svg.offset_raws(&offset_raw, "blue");
    //     svg.write();
    // }

    #[test]
    #[ignore = "svg output"]
    fn test_arc_from_bulge_plinearc_svg() {
        let arc0 = arc_from_bulge(
            point(-52.0, 250.0),
            point(-23.429621235520095, 204.88318696736243),
            -0.6068148963145962,
        );
        let mut svg = svg(400.0, 600.0);
        svg.arc(&arc0, "red");
        let circle0 = circle(point(arc0.c.x, arc0.c.y), 0.1);
        svg.circle(&circle0, "blue");

        let offsetraw = offset_segment(&arc0, point(-52.0, 250.0), -0.6068148963145962, 16.0);
        svg.arcsegment(&offsetraw.arc, "green");
        let circle1 = circle(point(offsetraw.arc.c.x, offsetraw.arc.c.y), 0.1);
        svg.circle(&circle1, "blue");

        svg.write();
    }

    // Unit tests for negative bulge arc offsetting bug
    #[test]
    fn test_arc_offset_positive_bulge_right_side() {
        // Create a simple arc with positive bulge (CCW, curving left from the direction of travel)
        // Center at (0, 0), radius 10
        // Arc from (10, 0) to (0, 10) - quarter circle in first quadrant
        let arc = arc(point(10.0, 0.0), point(0.0, 10.0), point(0.0, 0.0), 10.0);
        let bulge = 1.0; // positive bulge

        // Offset right by 2.0 units (positive offset)
        let offset_result = arc_offset(&arc, arc.a, bulge, 2.0);

        // For positive bulge, offset should expand the radius: 10 + 2 = 12
        let expected_radius = 12.0;
        assert!(
            (offset_result.arc.r - expected_radius).abs() < 0.01,
            "Positive bulge: expected radius {}, got {}",
            expected_radius,
            offset_result.arc.r
        );
    }

    #[test]
    fn test_arc_offset_negative_bulge_right_side() {
        // Create a simple arc with negative bulge (CW when unwound, curving right)
        // Center at (0, 0), radius 10
        // Arc from (10, 0) to (0, 10) - but bulge is negative
        let arc = arc(point(10.0, 0.0), point(0.0, 10.0), point(0.0, 0.0), 10.0);
        let bulge = -1.0; // negative bulge

        // Offset right by 2.0 units (positive offset)
        // For negative bulge, the offset is negated, so the arc shrinks
        // Radius becomes: 10 + (-2.0) = 8.0
        let offset_result = arc_offset(&arc, arc.a, bulge, 2.0);

        let expected_radius = 8.0;
        assert!(
            (offset_result.arc.r - expected_radius).abs() < 0.01,
            "Negative bulge: expected radius {}, got {}",
            expected_radius,
            offset_result.arc.r
        );
    }

    #[test]
    fn test_line_offset_simple_horizontal() {
        // Simple horizontal line segment from (0,0) to (10,0)
        let seg = arcseg(point(0.0, 0.0), point(10.0, 0.0));

        // Offset to the right by 2 units
        // For a horizontal line going right, right offset should move it downward
        let result = line_offset(&seg, point(0.0, 0.0), 2.0);

        // Expected: line from (0, -2) to (10, -2)
        let expected_a = point(0.0, -2.0);
        let expected_b = point(10.0, -2.0);
        assert!(
            result.arc.a.close_enough(expected_a, 0.01),
            "Expected a={:?}, got {:?}",
            expected_a,
            result.arc.a
        );
        assert!(
            result.arc.b.close_enough(expected_b, 0.01),
            "Expected b={:?}, got {:?}",
            expected_b,
            result.arc.b
        );
    }

    #[test]
    #[ignore = "synthetic test - real bulge comes from polyline/arcline connectivity, not manual parameter"]
    fn test_positive_vs_negative_bulge_arc_offset_direction() {
        // This test is not relevant: bulge parameter to arc_offset doesn't control geometry
        // Real bulge sign is determined by connectivity in poly_to_raws_single or arcs_to_raws_single
        // Keeping for reference but disabled
    }

    #[test]
    fn test_arc_offset_direction_consistency() {
        // This is the critical test: when offsetting with the SAME positive distance,
        // do different bulges offset in different directions?

        // Create a simple vertical line from (5, 0) to (5, 10)
        // Positive bulge: curves right (center at x > 5)
        // Negative bulge: curves left (center at x < 5)

        let seg = arcseg(point(5.0, 0.0), point(5.0, 10.0));

        // For a vertical segment going up:
        // - Right offset should move to x = 7 (increasing x)
        // - Left offset should move to x = 3 (decreasing x)

        let result_right = line_offset(&seg, point(5.0, 0.0), 2.0);

        // For vertical line going up, right offset should move x coordinate to 7
        assert!(
            (result_right.arc.a.x - 7.0).abs() < 0.01,
            "Expected right offset to x=7, got x={}",
            result_right.arc.a.x
        );
    }

    #[test]
    #[ignore = "synthetic test - real bulge comes from polyline/arcline connectivity, not manual parameter"]
    fn test_negative_bulge_offset_side_bug() {
        // This test is not relevant: bulge parameter to arc_offset doesn't control geometry
        // Real bulge sign is determined by connectivity in poly_to_raws_single or arcs_to_raws_single
        // Keeping for reference but disabled
    }

    #[test]
    fn test_arcline_bulge_always_positive() {
        // INSIGHT: All arcs in togo are CCW, so bulge_from_arc() always returns positive!
        // Negative bulge metadata is LOST when converting arc_from_bulge to Arc struct
        // This is why arcs_to_raws_single can never recover original bulge sign

        let start = point(0.0, 0.0);
        let end = point(10.0, 0.0);

        // Create arc with negative bulge
        let arc_neg = arc_from_bulge(start, end, -1.0);
        let recalc_neg = bulge_from_arc(arc_neg.a, arc_neg.b, arc_neg.c, arc_neg.r);

        // Create arc with positive bulge
        let arc_pos = arc_from_bulge(start, end, 1.0);
        let recalc_pos = bulge_from_arc(arc_pos.a, arc_pos.b, arc_pos.c, arc_pos.r);

        // Both recalculate to positive because all arcs in togo are CCW
        assert!(
            recalc_neg > 0.0,
            "Recalculated bulge from negative should still be positive (all togo arcs are CCW)"
        );
        assert!(
            recalc_pos > 0.0,
            "Recalculated bulge from positive should be positive"
        );

        // The arcs might be geometrically different, but bulge sign info is lost
        eprintln!(
            "Original negative bulge: -1.0, recalculated: {}",
            recalc_neg
        );
        eprintln!("Original positive bulge: 1.0, recalculated: {}", recalc_pos);
        eprintln!("Arc from neg: a={:?}, b={:?}", arc_neg.a, arc_neg.b);
        eprintln!("Arc from pos: a={:?}, b={:?}", arc_pos.a, arc_pos.b);
    }
}