Skip to main content

calib_targets_print/
render.rs

1use crate::model::{
2    validate_charuco_spec, validate_inner_corner_grid, validate_marker_board_spec,
3    validate_puzzleboard_spec, CharucoTargetSpec, MarkerBoardTargetSpec, PrintableTargetDocument,
4    PrintableTargetError, PuzzleBoardTargetSpec, RenderOptions, ResolvedTargetLayout, TargetSpec,
5};
6use calib_targets_charuco::CharucoBoard;
7use calib_targets_marker::CirclePolarity;
8use calib_targets_puzzleboard::code_maps;
9use png::{BitDepth, ColorType, Encoder, PixelDimensions, Unit};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub(crate) enum Fill {
13    White,
14    Black,
15    Accent,
16    Guide,
17}
18
19impl Fill {
20    fn gray(self) -> u8 {
21        match self {
22            Self::White => 255,
23            Self::Black => 0,
24            Self::Accent => 96,
25            Self::Guide => 180,
26        }
27    }
28
29    fn svg(self) -> &'static str {
30        match self {
31            Self::White => "#ffffff",
32            Self::Black => "#000000",
33            Self::Accent => "#d22f27",
34            Self::Guide => "#4a90e2",
35        }
36    }
37}
38
39#[derive(Clone, Debug)]
40pub(crate) enum Primitive {
41    Rect {
42        x_mm: f64,
43        y_mm: f64,
44        width_mm: f64,
45        height_mm: f64,
46        fill: Fill,
47    },
48    Circle {
49        cx_mm: f64,
50        cy_mm: f64,
51        radius_mm: f64,
52        fill: Fill,
53    },
54}
55
56#[derive(Clone, Debug)]
57pub(crate) struct Scene {
58    pub(crate) width_mm: f64,
59    pub(crate) height_mm: f64,
60    pub(crate) primitives: Vec<Primitive>,
61}
62
63impl Scene {
64    pub(crate) fn new(width_mm: f64, height_mm: f64) -> Self {
65        Self {
66            width_mm,
67            height_mm,
68            primitives: Vec::new(),
69        }
70    }
71}
72
73/// A rendered printable-target bundle: the JSON description plus the
74/// SVG, PNG, and DXF renderings, all held in memory.
75///
76/// Marked `#[non_exhaustive]` because the rendered-formats list is
77/// expected to keep growing (e.g. PDF, Gerber); new fields are
78/// therefore not a breaking change for downstream consumers, who must
79/// construct instances via [`GeneratedTargetBundle::new`].
80#[non_exhaustive]
81#[derive(Clone, Debug)]
82pub struct GeneratedTargetBundle {
83    /// The target description serialized as JSON.
84    pub json_text: String,
85    /// The target rendered as an SVG document.
86    pub svg_text: String,
87    /// The target rendered as PNG image bytes.
88    pub png_bytes: Vec<u8>,
89    /// The target rendered as a DXF document — chrome-on-glass
90    /// photolithography handoff. Carries the `Fill::Black` regions
91    /// only (single layer `PATTERN`), Y-flipped into DXF cartesian.
92    pub dxf_text: String,
93}
94
95impl GeneratedTargetBundle {
96    /// Construct a bundle from its rendered formats.
97    pub fn new(json_text: String, svg_text: String, png_bytes: Vec<u8>, dxf_text: String) -> Self {
98        Self {
99            json_text,
100            svg_text,
101            png_bytes,
102            dxf_text,
103        }
104    }
105}
106
107/// Render a printable-target document into an in-memory JSON / SVG /
108/// PNG / DXF bundle.
109pub fn render_target_bundle(
110    document: &PrintableTargetDocument,
111) -> Result<GeneratedTargetBundle, PrintableTargetError> {
112    let layout = document.resolve_layout()?;
113    let mut scene = Scene::new(layout.page_width_mm, layout.page_height_mm);
114    scene.primitives.push(Primitive::Rect {
115        x_mm: 0.0,
116        y_mm: 0.0,
117        width_mm: layout.page_width_mm,
118        height_mm: layout.page_height_mm,
119        fill: Fill::White,
120    });
121    build_board_scene(&mut scene, document, &layout)?;
122    // DXF must never carry debug annotations — render it from the
123    // pre-debug scene snapshot so a hardware handoff file is always
124    // pattern-only, even when the SVG/PNG render is annotated.
125    let dxf_text = crate::render_dxf::render_dxf(&scene);
126    if document.render.debug_annotations {
127        add_debug_primitives(&mut scene, document, &layout);
128    }
129    Ok(GeneratedTargetBundle::new(
130        document.to_json_pretty()?,
131        render_svg(&scene),
132        render_png(&scene, &document.render)?,
133        dxf_text,
134    ))
135}
136
137fn build_board_scene(
138    scene: &mut Scene,
139    document: &PrintableTargetDocument,
140    layout: &ResolvedTargetLayout,
141) -> Result<(), PrintableTargetError> {
142    match &document.target {
143        TargetSpec::Chessboard(spec) => build_chessboard(scene, spec, layout),
144        TargetSpec::Charuco(spec) => build_charuco(scene, spec, layout),
145        TargetSpec::MarkerBoard(spec) => build_marker_board(scene, spec, layout),
146        TargetSpec::PuzzleBoard(spec) => build_puzzleboard(scene, spec, layout),
147    }
148}
149
150fn build_chessboard(
151    scene: &mut Scene,
152    spec: &crate::model::ChessboardTargetSpec,
153    layout: &ResolvedTargetLayout,
154) -> Result<(), PrintableTargetError> {
155    validate_inner_corner_grid(spec.inner_rows, spec.inner_cols, spec.square_size_mm)?;
156    let squares_x = spec.inner_cols + 1;
157    let squares_y = spec.inner_rows + 1;
158    for sy in 0..squares_y {
159        for sx in 0..squares_x {
160            let fill = if (sx + sy) % 2 == 0 {
161                Fill::Black
162            } else {
163                Fill::White
164            };
165            scene.primitives.push(Primitive::Rect {
166                x_mm: layout.board_origin_mm[0] + sx as f64 * spec.square_size_mm,
167                y_mm: layout.board_origin_mm[1] + sy as f64 * spec.square_size_mm,
168                width_mm: spec.square_size_mm,
169                height_mm: spec.square_size_mm,
170                fill,
171            });
172        }
173    }
174    Ok(())
175}
176
177fn build_charuco(
178    scene: &mut Scene,
179    spec: &CharucoTargetSpec,
180    layout: &ResolvedTargetLayout,
181) -> Result<(), PrintableTargetError> {
182    validate_charuco_spec(spec)?;
183    for sy in 0..spec.rows {
184        for sx in 0..spec.cols {
185            let fill = if (sx + sy) % 2 == 0 {
186                Fill::Black
187            } else {
188                Fill::White
189            };
190            scene.primitives.push(Primitive::Rect {
191                x_mm: layout.board_origin_mm[0] + sx as f64 * spec.square_size_mm,
192                y_mm: layout.board_origin_mm[1] + sy as f64 * spec.square_size_mm,
193                width_mm: spec.square_size_mm,
194                height_mm: spec.square_size_mm,
195                fill,
196            });
197        }
198    }
199
200    let board = CharucoBoard::new(spec.to_board_spec())?;
201    let marker_side_mm = spec.square_size_mm * spec.marker_size_rel;
202    let marker_offset_mm = 0.5 * (spec.square_size_mm - marker_side_mm);
203    let bits = spec.dictionary.marker_size();
204    let total_cells = bits + 2 * spec.border_bits;
205    let bit_cell_mm = marker_side_mm / total_cells as f64;
206
207    for marker_id in 0..board.marker_count() {
208        let cell = board
209            .marker_position(marker_id as u32)
210            .expect("validated marker position");
211        let origin_x =
212            layout.board_origin_mm[0] + cell.u as f64 * spec.square_size_mm + marker_offset_mm;
213        let origin_y =
214            layout.board_origin_mm[1] + cell.v as f64 * spec.square_size_mm + marker_offset_mm;
215        let code = spec.dictionary.codes()[marker_id];
216        for cy in 0..total_cells {
217            for cx in 0..total_cells {
218                let is_black = if cx < spec.border_bits
219                    || cy < spec.border_bits
220                    || cx >= spec.border_bits + bits
221                    || cy >= spec.border_bits + bits
222                {
223                    true
224                } else {
225                    let bx = cx - spec.border_bits;
226                    let by = cy - spec.border_bits;
227                    let idx = by * bits + bx;
228                    ((code >> idx) & 1) == 1
229                };
230                scene.primitives.push(Primitive::Rect {
231                    x_mm: origin_x + cx as f64 * bit_cell_mm,
232                    y_mm: origin_y + cy as f64 * bit_cell_mm,
233                    width_mm: bit_cell_mm,
234                    height_mm: bit_cell_mm,
235                    fill: if is_black { Fill::Black } else { Fill::White },
236                });
237            }
238        }
239    }
240
241    Ok(())
242}
243
244fn build_puzzleboard(
245    scene: &mut Scene,
246    spec: &PuzzleBoardTargetSpec,
247    layout: &ResolvedTargetLayout,
248) -> Result<(), PrintableTargetError> {
249    validate_puzzleboard_spec(spec)?;
250    let origin_x = layout.board_origin_mm[0];
251    let origin_y = layout.board_origin_mm[1];
252
253    // 1) Checkerboard squares. Convention: top-left square (local (0, 0))
254    //    is **black** iff `(origin_row + origin_col) % 2 == 0`, so the
255    //    master checkerboard tiling is consistent across sub-rectangles.
256    for sy in 0..spec.rows {
257        for sx in 0..spec.cols {
258            let master_r = spec.origin_row + sy;
259            let master_c = spec.origin_col + sx;
260            let fill = if (master_r + master_c).is_multiple_of(2) {
261                Fill::Black
262            } else {
263                Fill::White
264            };
265            scene.primitives.push(Primitive::Rect {
266                x_mm: origin_x + sx as f64 * spec.square_size_mm,
267                y_mm: origin_y + sy as f64 * spec.square_size_mm,
268                width_mm: spec.square_size_mm,
269                height_mm: spec.square_size_mm,
270                fill,
271            });
272        }
273    }
274
275    // 2) Dots at every interior edge midpoint. Dot colour encodes the bit:
276    //    bit=0 → black dot, bit=1 → white dot  (Stelldinger 2024 convention).
277    let dot_radius_mm = 0.5 * spec.dot_diameter_rel * spec.square_size_mm;
278
279    // Horizontal interior edges: between rows `r` and `r+1` at column `c`.
280    // There are `rows - 1` such rows × `cols` columns in the board.
281    for r in 0..spec.rows.saturating_sub(1) {
282        for c in 0..spec.cols {
283            let master_r = (spec.origin_row + r) as i32;
284            let master_c = (spec.origin_col + c) as i32;
285            let bit = code_maps::horizontal_edge_bit(master_r, master_c);
286            let fill = if bit == 1 { Fill::White } else { Fill::Black };
287            let cx = origin_x + (c as f64 + 0.5) * spec.square_size_mm;
288            let cy = origin_y + (r as f64 + 1.0) * spec.square_size_mm;
289            scene.primitives.push(Primitive::Circle {
290                cx_mm: cx,
291                cy_mm: cy,
292                radius_mm: dot_radius_mm,
293                fill,
294            });
295        }
296    }
297
298    // Vertical interior edges: between cols `c` and `c+1` at row `r`.
299    // `rows` rows × `cols - 1` columns.
300    for r in 0..spec.rows {
301        for c in 0..spec.cols.saturating_sub(1) {
302            let master_r = (spec.origin_row + r) as i32;
303            let master_c = (spec.origin_col + c) as i32;
304            let bit = code_maps::vertical_edge_bit(master_r, master_c);
305            let fill = if bit == 1 { Fill::White } else { Fill::Black };
306            let cx = origin_x + (c as f64 + 1.0) * spec.square_size_mm;
307            let cy = origin_y + (r as f64 + 0.5) * spec.square_size_mm;
308            scene.primitives.push(Primitive::Circle {
309                cx_mm: cx,
310                cy_mm: cy,
311                radius_mm: dot_radius_mm,
312                fill,
313            });
314        }
315    }
316
317    Ok(())
318}
319
320fn build_marker_board(
321    scene: &mut Scene,
322    spec: &MarkerBoardTargetSpec,
323    layout: &ResolvedTargetLayout,
324) -> Result<(), PrintableTargetError> {
325    validate_marker_board_spec(spec)?;
326    build_chessboard(
327        scene,
328        &crate::model::ChessboardTargetSpec {
329            inner_rows: spec.inner_rows,
330            inner_cols: spec.inner_cols,
331            square_size_mm: spec.square_size_mm,
332        },
333        layout,
334    )?;
335    let radius_mm = 0.5 * spec.circle_diameter_rel * spec.square_size_mm;
336    for circle in spec.circles {
337        scene.primitives.push(Primitive::Circle {
338            cx_mm: layout.board_origin_mm[0] + (circle.i as f64 + 0.5) * spec.square_size_mm,
339            cy_mm: layout.board_origin_mm[1] + (circle.j as f64 + 0.5) * spec.square_size_mm,
340            radius_mm,
341            // NOTE: update this adapter when new CirclePolarity variants are added upstream.
342            fill: match circle.polarity {
343                CirclePolarity::White => Fill::White,
344                CirclePolarity::Black => Fill::Black,
345                _ => unreachable!("unhandled CirclePolarity variant — update render_marker_board"),
346            },
347        });
348    }
349    Ok(())
350}
351
352fn add_debug_primitives(
353    scene: &mut Scene,
354    document: &PrintableTargetDocument,
355    layout: &ResolvedTargetLayout,
356) {
357    let margin = document.page.margin_mm;
358    let printable_width_mm = layout.page_width_mm - 2.0 * margin;
359    let printable_height_mm = layout.page_height_mm - 2.0 * margin;
360    add_outline_rect(
361        scene,
362        margin,
363        margin,
364        printable_width_mm,
365        printable_height_mm,
366        0.5,
367        Fill::Guide,
368    );
369    add_outline_rect(
370        scene,
371        layout.board_origin_mm[0],
372        layout.board_origin_mm[1],
373        layout.board_width_mm,
374        layout.board_height_mm,
375        0.7,
376        Fill::Accent,
377    );
378    for point in &layout.points {
379        scene.primitives.push(Primitive::Circle {
380            cx_mm: layout.board_origin_mm[0] + point.position_mm[0],
381            cy_mm: layout.board_origin_mm[1] + point.position_mm[1],
382            radius_mm: 0.8,
383            fill: Fill::Accent,
384        });
385    }
386}
387
388fn add_outline_rect(
389    scene: &mut Scene,
390    x_mm: f64,
391    y_mm: f64,
392    width_mm: f64,
393    height_mm: f64,
394    thickness_mm: f64,
395    fill: Fill,
396) {
397    scene.primitives.push(Primitive::Rect {
398        x_mm,
399        y_mm,
400        width_mm,
401        height_mm: thickness_mm,
402        fill,
403    });
404    scene.primitives.push(Primitive::Rect {
405        x_mm,
406        y_mm: y_mm + height_mm - thickness_mm,
407        width_mm,
408        height_mm: thickness_mm,
409        fill,
410    });
411    scene.primitives.push(Primitive::Rect {
412        x_mm,
413        y_mm,
414        width_mm: thickness_mm,
415        height_mm,
416        fill,
417    });
418    scene.primitives.push(Primitive::Rect {
419        x_mm: x_mm + width_mm - thickness_mm,
420        y_mm,
421        width_mm: thickness_mm,
422        height_mm,
423        fill,
424    });
425}
426
427fn render_svg(scene: &Scene) -> String {
428    let mut out = String::new();
429    out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
430    out.push('\n');
431    out.push_str(&format!(
432        r#"<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{}mm" height="{}mm" viewBox="0 0 {} {}">"#,
433        fmt_mm(scene.width_mm),
434        fmt_mm(scene.height_mm),
435        fmt_mm(scene.width_mm),
436        fmt_mm(scene.height_mm),
437    ));
438    out.push('\n');
439    for primitive in &scene.primitives {
440        match primitive {
441            Primitive::Rect {
442                x_mm,
443                y_mm,
444                width_mm,
445                height_mm,
446                fill,
447            } => {
448                out.push_str(&format!(
449                    r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
450                    fmt_mm(*x_mm),
451                    fmt_mm(*y_mm),
452                    fmt_mm(*width_mm),
453                    fmt_mm(*height_mm),
454                    fill.svg(),
455                ));
456            }
457            Primitive::Circle {
458                cx_mm,
459                cy_mm,
460                radius_mm,
461                fill,
462            } => {
463                out.push_str(&format!(
464                    r#"<circle cx="{}" cy="{}" r="{}" fill="{}"/>"#,
465                    fmt_mm(*cx_mm),
466                    fmt_mm(*cy_mm),
467                    fmt_mm(*radius_mm),
468                    fill.svg(),
469                ));
470            }
471        }
472        out.push('\n');
473    }
474    out.push_str("</svg>\n");
475    out
476}
477
478fn render_png(scene: &Scene, options: &RenderOptions) -> Result<Vec<u8>, PrintableTargetError> {
479    let px_per_mm = options.png_dpi as f64 / 25.4;
480    let width_px = (scene.width_mm * px_per_mm).round().max(1.0) as usize;
481    let height_px = (scene.height_mm * px_per_mm).round().max(1.0) as usize;
482    let mut data = vec![255u8; width_px * height_px];
483    let mut canvas = RasterCanvas {
484        data: &mut data,
485        width_px,
486        height_px,
487        px_per_mm,
488    };
489
490    for primitive in &scene.primitives {
491        match primitive {
492            Primitive::Rect {
493                x_mm,
494                y_mm,
495                width_mm,
496                height_mm,
497                fill,
498            } => fill_rect(
499                &mut canvas,
500                *x_mm,
501                *y_mm,
502                [*width_mm, *height_mm],
503                fill.gray(),
504            ),
505            Primitive::Circle {
506                cx_mm,
507                cy_mm,
508                radius_mm,
509                fill,
510            } => fill_circle(&mut canvas, [*cx_mm, *cy_mm], *radius_mm, fill.gray()),
511        }
512    }
513
514    let mut bytes = Vec::new();
515    let mut encoder = Encoder::new(&mut bytes, width_px as u32, height_px as u32);
516    encoder.set_color(ColorType::Grayscale);
517    encoder.set_depth(BitDepth::Eight);
518    encoder.set_pixel_dims(Some(PixelDimensions {
519        xppu: (options.png_dpi as f64 / 25.4 * 1000.0).round() as u32,
520        yppu: (options.png_dpi as f64 / 25.4 * 1000.0).round() as u32,
521        unit: Unit::Meter,
522    }));
523    let mut writer = encoder.write_header()?;
524    writer.write_image_data(&data)?;
525    writer.finish()?;
526    Ok(bytes)
527}
528
529struct RasterCanvas<'a> {
530    data: &'a mut [u8],
531    width_px: usize,
532    height_px: usize,
533    px_per_mm: f64,
534}
535
536fn fill_rect(canvas: &mut RasterCanvas<'_>, x_mm: f64, y_mm: f64, size_mm: [f64; 2], gray: u8) {
537    let x0 = (x_mm * canvas.px_per_mm).round().max(0.0) as i32;
538    let y0 = (y_mm * canvas.px_per_mm).round().max(0.0) as i32;
539    let x1 = ((x_mm + size_mm[0]) * canvas.px_per_mm)
540        .round()
541        .min(canvas.width_px as f64) as i32;
542    let y1 = ((y_mm + size_mm[1]) * canvas.px_per_mm)
543        .round()
544        .min(canvas.height_px as f64) as i32;
545    for y in y0.max(0)..y1.max(0) {
546        let y = y as usize;
547        if y >= canvas.height_px {
548            continue;
549        }
550        let row = y * canvas.width_px;
551        for x in x0.max(0)..x1.max(0) {
552            let x = x as usize;
553            if x < canvas.width_px {
554                canvas.data[row + x] = gray;
555            }
556        }
557    }
558}
559
560fn fill_circle(canvas: &mut RasterCanvas<'_>, center_mm: [f64; 2], radius_mm: f64, gray: u8) {
561    let cx_px = center_mm[0] * canvas.px_per_mm;
562    let cy_px = center_mm[1] * canvas.px_per_mm;
563    let radius_px = radius_mm * canvas.px_per_mm;
564    let x0 = (cx_px - radius_px).floor().max(0.0) as i32;
565    let y0 = (cy_px - radius_px).floor().max(0.0) as i32;
566    let x1 = (cx_px + radius_px).ceil().min(canvas.width_px as f64) as i32;
567    let y1 = (cy_px + radius_px).ceil().min(canvas.height_px as f64) as i32;
568    let radius_sq = radius_px * radius_px;
569    for y in y0..y1 {
570        let y_usize = y as usize;
571        if y_usize >= canvas.height_px {
572            continue;
573        }
574        let py = y as f64 + 0.5;
575        let row = y_usize * canvas.width_px;
576        for x in x0..x1 {
577            let x_usize = x as usize;
578            if x_usize >= canvas.width_px {
579                continue;
580            }
581            let px = x as f64 + 0.5;
582            let dx = px - cx_px;
583            let dy = py - cy_px;
584            if dx * dx + dy * dy <= radius_sq {
585                canvas.data[row + x_usize] = gray;
586            }
587        }
588    }
589}
590
591fn fmt_mm(value: f64) -> String {
592    let mut text = format!("{value:.4}");
593    while text.contains('.') && text.ends_with('0') {
594        text.pop();
595    }
596    if text.ends_with('.') {
597        text.pop();
598    }
599    text
600}
601
602impl From<png::EncodingError> for PrintableTargetError {
603    fn from(value: png::EncodingError) -> Self {
604        PrintableTargetError::Io(std::io::Error::other(value.to_string()))
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611    use crate::model::{
612        CharucoTargetSpec, ChessboardTargetSpec, MarkerBoardTargetSpec, MarkerCircleSpec, PageSize,
613        PrintableTargetDocument, TargetSpec,
614    };
615    use calib_targets_aruco::builtins;
616    use calib_targets_charuco::MarkerLayout;
617
618    #[test]
619    fn svg_and_png_follow_page_dimensions() {
620        let mut doc = PrintableTargetDocument::new(TargetSpec::Chessboard(ChessboardTargetSpec {
621            inner_rows: 6,
622            inner_cols: 8,
623            square_size_mm: 20.0,
624        }));
625        doc.page.size = PageSize::Custom {
626            width_mm: 250.0,
627            height_mm: 180.0,
628        };
629        let bundle = render_target_bundle(&doc).expect("bundle");
630        assert!(bundle.svg_text.contains(r#"width="250mm""#));
631        assert!(bundle.svg_text.contains(r#"height="180mm""#));
632        assert!(!bundle.png_bytes.is_empty());
633    }
634
635    #[test]
636    fn debug_annotations_add_outline_primitives() {
637        let mut doc =
638            PrintableTargetDocument::new(TargetSpec::MarkerBoard(MarkerBoardTargetSpec {
639                inner_rows: 6,
640                inner_cols: 8,
641                square_size_mm: 20.0,
642                circles: [
643                    MarkerCircleSpec {
644                        i: 3,
645                        j: 2,
646                        polarity: CirclePolarity::White,
647                    },
648                    MarkerCircleSpec {
649                        i: 4,
650                        j: 2,
651                        polarity: CirclePolarity::Black,
652                    },
653                    MarkerCircleSpec {
654                        i: 4,
655                        j: 3,
656                        polarity: CirclePolarity::White,
657                    },
658                ],
659                circle_diameter_rel: 0.5,
660            }));
661        doc.render.debug_annotations = true;
662        let bundle = render_target_bundle(&doc).expect("bundle");
663        assert!(bundle.svg_text.contains("#d22f27"));
664        assert!(bundle.svg_text.contains("#4a90e2"));
665    }
666
667    #[test]
668    fn charuco_svg_contains_marker_cells() {
669        let doc = PrintableTargetDocument::new(TargetSpec::Charuco(CharucoTargetSpec {
670            rows: 5,
671            cols: 7,
672            square_size_mm: 15.0,
673            marker_size_rel: 0.75,
674            dictionary: builtins::builtin_dictionary("DICT_4X4_50").expect("dict"),
675            marker_layout: MarkerLayout::OpenCvCharuco,
676            border_bits: 1,
677        }));
678        let bundle = render_target_bundle(&doc).expect("bundle");
679        let rect_count = bundle.svg_text.matches("<rect ").count();
680        assert!(rect_count > 35);
681    }
682}