Skip to main content

r/interpreter/builtins/
grid.rs

1//! Grid graphics builtins — R-facing functions for the grid graphics system.
2//!
3//! Grid objects are represented as R lists with S3 class attributes:
4//! - Unit: `list(value=..., units=...)` with class "unit"
5//! - Gpar: `list(col=..., fill=..., ...)` with class "gpar"
6//! - Viewport: `list(x=..., y=..., width=..., ...)` with class "viewport"
7//! - Grobs: `list(x=..., y=..., gp=..., ...)` with class c("<type>", "grob")
8
9use super::CallArgs;
10use crate::interpreter::grid;
11use crate::interpreter::value::*;
12use crate::interpreter::BuiltinContext;
13use minir_macros::interpreter_builtin;
14
15// region: Helpers
16
17/// Create an RValue::List with the given named entries and set its class attribute.
18fn make_grid_object(entries: Vec<(String, RValue)>, classes: &[&str]) -> RValue {
19    let values: Vec<(Option<String>, RValue)> =
20        entries.into_iter().map(|(k, v)| (Some(k), v)).collect();
21    let mut list = RList::new(values);
22    let class_vec: Vec<Option<String>> = classes.iter().map(|c| Some(c.to_string())).collect();
23    list.set_attr(
24        "class".to_string(),
25        RValue::vec(Vector::Character(class_vec.into())),
26    );
27    RValue::List(list)
28}
29
30/// Extract an optional RValue from args by name or position, returning NULL if absent.
31fn opt_value(args: &CallArgs, name: &str, pos: usize) -> RValue {
32    args.value(name, pos).cloned().unwrap_or(RValue::Null)
33}
34
35/// Normalize the `just` parameter: convert string names like "centre", "left",
36/// "top" to numeric c(hjust, vjust) pairs matching R's grid convention.
37fn normalize_just(ca: &CallArgs, name: &str, pos: usize) -> RValue {
38    let val = ca.value(name, pos).cloned().unwrap_or(RValue::Null);
39    if let RValue::Vector(ref rv) = val {
40        if let Some(s) = rv.inner.as_character_scalar() {
41            let (h, v) = match s.as_str() {
42                "left" => (0.0, 0.5),
43                "right" => (1.0, 0.5),
44                "top" => (0.5, 1.0),
45                "bottom" => (0.5, 0.0),
46                "centre" | "center" => (0.5, 0.5),
47                "bottom.left" | "bottomleft" => (0.0, 0.0),
48                "bottom.right" | "bottomright" => (1.0, 0.0),
49                "top.left" | "topleft" => (0.0, 1.0),
50                "top.right" | "topright" => (1.0, 1.0),
51                _ => return val, // unknown string, pass through
52            };
53            return RValue::vec(Vector::Double(vec![Some(h), Some(v)].into()));
54        }
55    }
56    val
57}
58
59/// Generate a unique grob name with the given prefix and a counter.
60fn auto_grob_name(prefix: &str, ctx: &BuiltinContext) -> String {
61    // Use the display list length as a simple counter for unique names
62    let n = ctx.interpreter().grid_display_list.borrow().len();
63    format!("{prefix}.{n}")
64}
65
66/// Record a grob on the grid display list.
67fn record_on_display_list(grob: &RValue, ctx: &BuiltinContext) {
68    ctx.interpreter()
69        .grid_display_list
70        .borrow_mut()
71        .push(grob.clone());
72}
73
74/// Wrap a unit value: if x is already a unit object, return it; otherwise
75/// create `unit(x, default_units)`.
76fn ensure_unit(value: &RValue, default_units: &str) -> RValue {
77    // Check if it's already a unit (list with class "unit")
78    if let RValue::List(list) = value {
79        if let Some(classes) = list.class() {
80            if classes.iter().any(|c| c == "unit") {
81                return value.clone();
82            }
83        }
84    }
85    // Otherwise wrap in a unit
86    make_grid_object(
87        vec![
88            ("value".to_string(), value.clone()),
89            (
90                "units".to_string(),
91                RValue::vec(Vector::Character(
92                    vec![Some(default_units.to_string())].into(),
93                )),
94            ),
95        ],
96        &["unit"],
97    )
98}
99
100/// Default NPC unit value (0.5 npc).
101fn default_npc(val: f64) -> RValue {
102    make_grid_object(
103        vec![
104            (
105                "value".to_string(),
106                RValue::vec(Vector::Double(vec![Some(val)].into())),
107            ),
108            (
109                "units".to_string(),
110                RValue::vec(Vector::Character(vec![Some("npc".to_string())].into())),
111            ),
112        ],
113        &["unit"],
114    )
115}
116
117/// Default unit of 0 npc.
118fn default_npc_zero() -> RValue {
119    default_npc(0.0)
120}
121
122/// Default unit of 1 npc for width/height.
123fn default_npc_one() -> RValue {
124    default_npc(1.0)
125}
126
127/// Default unit of 0.5 npc for x/y.
128fn default_npc_half() -> RValue {
129    default_npc(0.5)
130}
131
132/// Find a viewport by name in the viewport stack.
133/// Returns the index in the stack if found.
134fn find_viewport_by_name(name: &str, ctx: &BuiltinContext) -> Option<usize> {
135    let stack = ctx.interpreter().grid_viewport_stack.borrow();
136    for (i, vp) in stack.iter().enumerate() {
137        if let RValue::List(list) = vp {
138            for (key, val) in &list.values {
139                if key.as_deref() == Some("name") {
140                    if let RValue::Vector(rv) = val {
141                        if rv.inner.as_character_scalar().as_deref() == Some(name) {
142                            return Some(i);
143                        }
144                    }
145                }
146            }
147        }
148    }
149    None
150}
151
152// endregion
153
154// region: R-to-Rust conversion helpers
155
156/// Extract a Rust `Unit` from an R value representing a unit object.
157///
158/// Handles both:
159/// - A unit object (list with class "unit", fields "value" and "units")
160/// - A bare numeric vector (treated as NPC by default)
161fn extract_unit_from_rvalue(val: &RValue) -> grid::units::Unit {
162    if let RValue::List(list) = val {
163        // Look for "value" and "units" entries
164        let mut values_opt = None;
165        let mut units_opt = None;
166        for (key, v) in &list.values {
167            match key.as_deref() {
168                Some("value") => values_opt = Some(v),
169                Some("units") => units_opt = Some(v),
170                _ => {}
171            }
172        }
173
174        if let (Some(values_val), Some(units_val)) = (values_opt, units_opt) {
175            let nums: Vec<f64> = if let Some(rv) = values_val.as_vector() {
176                rv.to_doubles()
177                    .into_iter()
178                    .map(|v| v.unwrap_or(0.0))
179                    .collect()
180            } else {
181                vec![0.0]
182            };
183
184            let unit_strs: Vec<String> = if let Some(rv) = units_val.as_vector() {
185                rv.to_characters()
186                    .into_iter()
187                    .map(|v: Option<String>| v.unwrap_or_else(|| "npc".to_string()))
188                    .collect()
189            } else {
190                vec!["npc".to_string()]
191            };
192
193            let mut unit_types = Vec::with_capacity(unit_strs.len());
194            for s in &unit_strs {
195                unit_types.push(parse_unit_type(s));
196            }
197
198            // Recycle to match lengths
199            let n = nums.len().max(unit_types.len());
200            let values: Vec<f64> = (0..n).map(|i| nums[i % nums.len()]).collect();
201            let units: Vec<grid::units::UnitType> = (0..n)
202                .map(|i| unit_types[i % unit_types.len()].clone())
203                .collect();
204
205            return grid::units::Unit { values, units };
206        }
207    }
208
209    // Bare numeric — treat as NPC
210    if let Some(rv) = val.as_vector() {
211        let nums: Vec<f64> = rv
212            .to_doubles()
213            .into_iter()
214            .map(|v| v.unwrap_or(0.0))
215            .collect();
216        if !nums.is_empty() {
217            return grid::units::Unit {
218                values: nums.clone(),
219                units: nums.iter().map(|_| grid::units::UnitType::Npc).collect(),
220            };
221        }
222    }
223
224    // Default: 0.5 npc
225    grid::units::Unit::npc(0.5)
226}
227
228/// Parse a unit type string into a `UnitType`.
229fn parse_unit_type(s: &str) -> grid::units::UnitType {
230    match s {
231        "npc" => grid::units::UnitType::Npc,
232        "cm" => grid::units::UnitType::Cm,
233        "inches" | "in" => grid::units::UnitType::Inches,
234        "mm" => grid::units::UnitType::Mm,
235        "points" | "pt" | "bigpts" | "picas" | "dida" | "cicero" | "scaledpts" => {
236            grid::units::UnitType::Points
237        }
238        "lines" => grid::units::UnitType::Lines,
239        "char" => grid::units::UnitType::Char,
240        "native" => grid::units::UnitType::Native,
241        "null" => grid::units::UnitType::Null,
242        "snpc" => grid::units::UnitType::Snpc,
243        "strwidth" => grid::units::UnitType::StrWidth(String::new()),
244        "strheight" => grid::units::UnitType::StrHeight(String::new()),
245        "grobwidth" => grid::units::UnitType::GrobWidth(String::new()),
246        "grobheight" => grid::units::UnitType::GrobHeight(String::new()),
247        _ => grid::units::UnitType::Npc,
248    }
249}
250
251/// Parse an R color value to RGBA.
252///
253/// Supports: color name strings, hex strings (#RRGGBB / #RRGGBBAA),
254/// and integer indices (treated as palette lookups, default black).
255fn parse_grid_color(value: &RValue) -> Option<[u8; 4]> {
256    match value {
257        RValue::Vector(rv) => match &rv.inner {
258            Vector::Character(c) => {
259                if let Some(Some(s)) = c.first() {
260                    Some(parse_color_string(s))
261                } else {
262                    None
263                }
264            }
265            Vector::Integer(iv) => {
266                // Basic palette: 0=white, 1=black, 2=red, 3=green, 4=blue, ...
267                iv.first_opt().map(|i| match i {
268                    0 => [255, 255, 255, 0],   // transparent
269                    1 => [0, 0, 0, 255],       // black
270                    2 => [255, 0, 0, 255],     // red
271                    3 => [0, 128, 0, 255],     // green
272                    4 => [0, 0, 255, 255],     // blue
273                    5 => [0, 255, 255, 255],   // cyan
274                    6 => [255, 0, 255, 255],   // magenta
275                    7 => [255, 255, 0, 255],   // yellow
276                    8 => [128, 128, 128, 255], // gray
277                    _ => [0, 0, 0, 255],       // default black
278                })
279            }
280            _ => None,
281        },
282        RValue::Null => None,
283        _ => None,
284    }
285}
286
287/// Parse a hex color string to RGBA.
288fn parse_color_string(s: &str) -> [u8; 4] {
289    // Named colors
290    match s.to_lowercase().as_str() {
291        "black" => return [0, 0, 0, 255],
292        "white" => return [255, 255, 255, 255],
293        "red" => return [255, 0, 0, 255],
294        "green" => return [0, 128, 0, 255],
295        "blue" => return [0, 0, 255, 255],
296        "cyan" => return [0, 255, 255, 255],
297        "magenta" => return [255, 0, 255, 255],
298        "yellow" => return [255, 255, 0, 255],
299        "gray" | "grey" => return [190, 190, 190, 255],
300        "orange" => return [255, 165, 0, 255],
301        "purple" => return [128, 0, 128, 255],
302        "brown" => return [165, 42, 42, 255],
303        "pink" => return [255, 192, 203, 255],
304        "transparent" | "na" => return [0, 0, 0, 0],
305        _ => {}
306    }
307
308    // Hex format
309    if let Some(hex) = s.strip_prefix('#') {
310        match hex.len() {
311            6 => {
312                let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
313                let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
314                let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
315                return [r, g, b, 255];
316            }
317            8 => {
318                let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
319                let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
320                let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
321                let a = u8::from_str_radix(&hex[6..8], 16).unwrap_or(255);
322                return [r, g, b, a];
323            }
324            _ => {}
325        }
326    }
327
328    [0, 0, 0, 255] // default black
329}
330
331/// Extract a Rust `Gpar` from an R gpar list object.
332fn extract_gpar_from_rvalue(val: &RValue) -> grid::gpar::Gpar {
333    let mut gp = grid::gpar::Gpar::new();
334
335    if let RValue::List(list) = val {
336        for (key, v) in &list.values {
337            match key.as_deref() {
338                Some("col") => {
339                    gp.col = parse_grid_color(v);
340                }
341                Some("fill") => {
342                    gp.fill = parse_grid_color(v);
343                }
344                Some("lwd") => {
345                    if let Some(rv) = v.as_vector() {
346                        gp.lwd = rv.as_double_scalar();
347                    }
348                }
349                Some("fontsize") => {
350                    if let Some(rv) = v.as_vector() {
351                        gp.fontsize = rv.as_double_scalar();
352                    }
353                }
354                Some("lineheight") => {
355                    if let Some(rv) = v.as_vector() {
356                        gp.lineheight = rv.as_double_scalar();
357                    }
358                }
359                Some("cex") => {
360                    if let Some(rv) = v.as_vector() {
361                        gp.cex = rv.as_double_scalar();
362                    }
363                }
364                Some("alpha") => {
365                    if let Some(rv) = v.as_vector() {
366                        gp.alpha = rv.as_double_scalar();
367                    }
368                }
369                _ => {}
370            }
371        }
372    }
373
374    gp
375}
376
377/// Extract justification from an R value.
378///
379/// Accepts: a character string ("centre", "left", etc.), a numeric vector
380/// c(hjust, vjust), or NULL (returns default centre/centre).
381fn extract_justification(
382    val: &RValue,
383) -> (grid::viewport::Justification, grid::viewport::Justification) {
384    use grid::viewport::Justification;
385
386    match val {
387        RValue::Vector(rv) => match &rv.inner {
388            Vector::Character(c) => {
389                if c.len() >= 2 {
390                    let h = c[0]
391                        .as_deref()
392                        .and_then(Justification::parse)
393                        .unwrap_or(Justification::Centre);
394                    let v_j = c[1]
395                        .as_deref()
396                        .and_then(Justification::parse)
397                        .unwrap_or(Justification::Centre);
398                    (h, v_j)
399                } else if let Some(Some(s)) = c.first() {
400                    let j = Justification::parse(s).unwrap_or(Justification::Centre);
401                    (j, j)
402                } else {
403                    (Justification::Centre, Justification::Centre)
404                }
405            }
406            Vector::Double(d) => {
407                let h = d.first_opt().unwrap_or(0.5);
408                let v_val = d.get_opt(1).unwrap_or(h);
409                (num_to_just(h), num_to_just(v_val))
410            }
411            _ => (Justification::Centre, Justification::Centre),
412        },
413        RValue::Null => (Justification::Centre, Justification::Centre),
414        _ => (Justification::Centre, Justification::Centre),
415    }
416}
417
418/// Convert a numeric justification (0.0, 0.5, 1.0) to a `Justification`.
419fn num_to_just(v: f64) -> grid::viewport::Justification {
420    use grid::viewport::Justification;
421    if v <= 0.25 {
422        Justification::Left
423    } else if v >= 0.75 {
424        Justification::Right
425    } else {
426        Justification::Centre
427    }
428}
429
430/// Extract a Rust `Viewport` from an R viewport list object.
431fn extract_viewport_from_rvalue(val: &RValue) -> grid::viewport::Viewport {
432    let mut vp = grid::viewport::Viewport::new();
433
434    if let RValue::List(list) = val {
435        for (key, v) in &list.values {
436            match key.as_deref() {
437                Some("x") => vp.x = extract_unit_from_rvalue(v),
438                Some("y") => vp.y = extract_unit_from_rvalue(v),
439                Some("width") => vp.width = extract_unit_from_rvalue(v),
440                Some("height") => vp.height = extract_unit_from_rvalue(v),
441                Some("just") => {
442                    let (h, v_just) = extract_justification(v);
443                    vp.just = (h, v_just);
444                }
445                Some("xscale") => {
446                    if let Some(rv) = v.as_vector() {
447                        let doubles = rv.to_doubles();
448                        if doubles.len() >= 2 {
449                            vp.xscale = (doubles[0].unwrap_or(0.0), doubles[1].unwrap_or(1.0));
450                        }
451                    }
452                }
453                Some("yscale") => {
454                    if let Some(rv) = v.as_vector() {
455                        let doubles = rv.to_doubles();
456                        if doubles.len() >= 2 {
457                            vp.yscale = (doubles[0].unwrap_or(0.0), doubles[1].unwrap_or(1.0));
458                        }
459                    }
460                }
461                Some("angle") => {
462                    if let Some(rv) = v.as_vector() {
463                        vp.angle = rv.as_double_scalar().unwrap_or(0.0);
464                    }
465                }
466                Some("clip") => {
467                    if let Some(rv) = v.as_vector() {
468                        if let Some(s) = rv.as_character_scalar() {
469                            vp.clip = s == "on";
470                        }
471                    }
472                }
473                Some("gp") => {
474                    vp.gp = extract_gpar_from_rvalue(v);
475                }
476                Some("name") => {
477                    if let Some(rv) = v.as_vector() {
478                        vp.name = rv.as_character_scalar();
479                    }
480                }
481                _ => {}
482            }
483        }
484    }
485
486    vp
487}
488
489/// Record a Rust grob on the Rust display list.
490///
491/// Creates the Rust `Grob`, adds it to the `GrobStore`, and records a
492/// `DisplayItem::Draw` on the Rust `DisplayList`.
493fn record_rust_grob(grob: grid::grob::Grob, ctx: &BuiltinContext) {
494    let grob_id = ctx.interpreter().grid_grob_store.borrow_mut().add(grob);
495    ctx.interpreter()
496        .grid_rust_display_list
497        .borrow_mut()
498        .record(grid::display::DisplayItem::Draw(grob_id));
499}
500
501/// Extract a vector of label strings from an R value.
502fn extract_labels(val: &RValue) -> Vec<String> {
503    match val {
504        RValue::Vector(rv) => rv
505            .inner
506            .to_characters()
507            .into_iter()
508            .map(|v| v.unwrap_or_default())
509            .collect(),
510        _ => vec![],
511    }
512}
513
514/// Extract rotation angle from an R value.
515fn extract_rot(val: &RValue) -> f64 {
516    if let Some(rv) = val.as_vector() {
517        rv.as_double_scalar().unwrap_or(0.0)
518    } else {
519        0.0
520    }
521}
522
523/// Extract pch (plotting character) from an R value.
524fn extract_pch(val: &RValue) -> u8 {
525    if let Some(rv) = val.as_vector() {
526        rv.as_integer_scalar()
527            .and_then(|i| u8::try_from(i).ok())
528            .unwrap_or(1)
529    } else {
530        1
531    }
532}
533
534// endregion
535
536// region: Page management
537
538/// Clear the grid display list and viewport stack, starting a new page.
539///
540/// @return NULL (invisibly)
541#[interpreter_builtin(name = "grid.newpage", namespace = "grid")]
542fn interp_grid_newpage(
543    _args: &[RValue],
544    _named: &[(String, RValue)],
545    context: &BuiltinContext,
546) -> Result<RValue, RError> {
547    // Flush any existing grid content before clearing
548    flush_grid_to_plot(context);
549
550    // Clear R-level state
551    context.interpreter().grid_display_list.borrow_mut().clear();
552    context
553        .interpreter()
554        .grid_viewport_stack
555        .borrow_mut()
556        .clear();
557
558    // Clear Rust-level state
559    context
560        .interpreter()
561        .grid_rust_display_list
562        .borrow_mut()
563        .clear();
564    *context.interpreter().grid_grob_store.borrow_mut() = grid::grob::GrobStore::new();
565    *context.interpreter().grid_rust_viewport_stack.borrow_mut() =
566        grid::viewport::ViewportStack::new(17.78, 17.78);
567
568    context.interpreter().set_invisible();
569    Ok(RValue::Null)
570}
571
572/// Record a grob on the grid display list for later rendering.
573///
574/// @param grob a grob object to draw
575/// @return the grob (invisibly)
576#[interpreter_builtin(name = "grid.draw", namespace = "grid", min_args = 1)]
577fn interp_grid_draw(
578    args: &[RValue],
579    _named: &[(String, RValue)],
580    context: &BuiltinContext,
581) -> Result<RValue, RError> {
582    let grob = args[0].clone();
583    record_on_display_list(&grob, context);
584
585    // Also record on the Rust display list if we can determine the grob type
586    if let RValue::List(list) = &grob {
587        if let Some(classes) = list.class() {
588            // The first class before "grob" is the type class
589            let type_class = classes
590                .iter()
591                .find(|c| *c != "grob")
592                .cloned()
593                .unwrap_or_default();
594
595            // Build entries from the list
596            let entries: Vec<(String, RValue)> = list
597                .values
598                .iter()
599                .filter_map(|(k, v)| k.as_ref().map(|k| (k.clone(), v.clone())))
600                .collect();
601
602            if let Some(rust_grob) = build_rust_grob(&type_class, &entries) {
603                record_rust_grob(rust_grob, context);
604            }
605        }
606    }
607
608    context.interpreter().set_invisible();
609    Ok(grob)
610}
611
612// endregion
613
614// region: Unit constructor
615
616/// Create a unit object representing a measurement with given units.
617///
618/// Supported units include: "npc", "cm", "inches", "mm", "points", "lines",
619/// "native", "null", "char", "grobwidth", "grobheight", "strwidth", "strheight".
620///
621/// @param x numeric value(s) for the unit
622/// @param units character string specifying the unit type
623/// @param data optional data for special units (e.g., grob for "grobwidth")
624/// @return a unit object (list with class "unit")
625#[interpreter_builtin(namespace = "grid", min_args = 2)]
626fn interp_unit(
627    args: &[RValue],
628    named: &[(String, RValue)],
629    _context: &BuiltinContext,
630) -> Result<RValue, RError> {
631    let ca = CallArgs::new(args, named);
632
633    let x = ca.value("x", 0).ok_or_else(|| {
634        RError::new(
635            RErrorKind::Argument,
636            "unit() requires an 'x' argument".to_string(),
637        )
638    })?;
639    let units_str = ca.string("units", 1)?;
640    let data = opt_value(&ca, "data", 2);
641
642    // Validate units
643    let valid_units = [
644        "npc",
645        "cm",
646        "inches",
647        "mm",
648        "points",
649        "lines",
650        "native",
651        "null",
652        "char",
653        "grobwidth",
654        "grobheight",
655        "strwidth",
656        "strheight",
657        "picas",
658        "bigpts",
659        "dida",
660        "cicero",
661        "scaledpts",
662    ];
663    if !valid_units.contains(&units_str.as_str()) {
664        return Err(RError::new(
665            RErrorKind::Argument,
666            format!(
667                "invalid unit '{}'. Valid units are: {}",
668                units_str,
669                valid_units.join(", ")
670            ),
671        ));
672    }
673
674    // Vectorize: if x is a vector, replicate the units string to match
675    let units_val = if let Some(rv) = x.as_vector() {
676        let n = rv.len();
677        let units_vec: Vec<Option<String>> = (0..n).map(|_| Some(units_str.clone())).collect();
678        RValue::vec(Vector::Character(units_vec.into()))
679    } else {
680        RValue::vec(Vector::Character(vec![Some(units_str.clone())].into()))
681    };
682
683    let mut entries = vec![
684        ("value".to_string(), x.clone()),
685        ("units".to_string(), units_val),
686    ];
687    if !matches!(data, RValue::Null) {
688        entries.push(("data".to_string(), data));
689    }
690
691    Ok(make_grid_object(entries, &["unit"]))
692}
693
694// endregion
695
696// region: Gpar constructor
697
698/// Create a graphical parameter object (gpar) for grid graphics.
699///
700/// @param col line color
701/// @param fill fill color
702/// @param lwd line width
703/// @param lty line type
704/// @param fontsize font size in points
705/// @param font font face (1=plain, 2=bold, 3=italic, 4=bold-italic)
706/// @param fontfamily font family name
707/// @param lineheight line height multiplier
708/// @param cex character expansion factor
709/// @param alpha alpha transparency
710/// @return a gpar object (list with class "gpar")
711#[interpreter_builtin(namespace = "grid")]
712fn interp_gpar(
713    _args: &[RValue],
714    named: &[(String, RValue)],
715    _context: &BuiltinContext,
716) -> Result<RValue, RError> {
717    let mut entries: Vec<(String, RValue)> = Vec::new();
718
719    let param_names = [
720        "col",
721        "fill",
722        "lwd",
723        "lty",
724        "fontsize",
725        "font",
726        "fontfamily",
727        "lineheight",
728        "cex",
729        "alpha",
730        "lex",
731        "lineend",
732        "linejoin",
733        "linemitre",
734    ];
735
736    for &name in &param_names {
737        if let Some((_, val)) = named.iter().find(|(k, _)| k == name) {
738            entries.push((name.to_string(), val.clone()));
739        }
740    }
741
742    Ok(make_grid_object(entries, &["gpar"]))
743}
744
745// endregion
746
747// region: Viewport functions
748
749/// Create a viewport object for grid graphics.
750///
751/// @param x horizontal position (default: unit(0.5, "npc"))
752/// @param y vertical position (default: unit(0.5, "npc"))
753/// @param width viewport width (default: unit(1, "npc"))
754/// @param height viewport height (default: unit(1, "npc"))
755/// @param just justification ("centre", "left", "right", "top", "bottom")
756/// @param xscale numeric c(min, max) for x-axis scale
757/// @param yscale numeric c(min, max) for y-axis scale
758/// @param angle rotation angle in degrees
759/// @param clip clipping ("on", "off", "inherit")
760/// @param gp graphical parameters (gpar object)
761/// @param layout a grid layout
762/// @param name viewport name for navigation
763/// @return a viewport object (list with class "viewport")
764#[interpreter_builtin(namespace = "grid")]
765fn interp_viewport(
766    _args: &[RValue],
767    named: &[(String, RValue)],
768    _context: &BuiltinContext,
769) -> Result<RValue, RError> {
770    let get = |name: &str| -> Option<RValue> {
771        named
772            .iter()
773            .find(|(k, _)| k == name)
774            .map(|(_, v)| v.clone())
775    };
776
777    let x = get("x").unwrap_or_else(default_npc_half);
778    let y = get("y").unwrap_or_else(default_npc_half);
779    let width = get("width").unwrap_or_else(default_npc_one);
780    let height = get("height").unwrap_or_else(default_npc_one);
781    let just = get("just")
782        .unwrap_or_else(|| RValue::vec(Vector::Character(vec![Some("centre".to_string())].into())));
783    let xscale = get("xscale")
784        .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into())));
785    let yscale = get("yscale")
786        .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into())));
787    let angle = get("angle").unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0)].into())));
788    let clip = get("clip").unwrap_or_else(|| {
789        RValue::vec(Vector::Character(vec![Some("inherit".to_string())].into()))
790    });
791    let gp = get("gp").unwrap_or(RValue::Null);
792    let layout = get("layout").unwrap_or(RValue::Null);
793    let name = get("name").unwrap_or(RValue::Null);
794    let layout_pos_row = get("layout.pos.row").unwrap_or(RValue::Null);
795    let layout_pos_col = get("layout.pos.col").unwrap_or(RValue::Null);
796
797    let entries = vec![
798        ("x".to_string(), x),
799        ("y".to_string(), y),
800        ("width".to_string(), width),
801        ("height".to_string(), height),
802        ("just".to_string(), just),
803        ("xscale".to_string(), xscale),
804        ("yscale".to_string(), yscale),
805        ("angle".to_string(), angle),
806        ("clip".to_string(), clip),
807        ("gp".to_string(), gp),
808        ("layout".to_string(), layout),
809        ("name".to_string(), name),
810        ("layout.pos.row".to_string(), layout_pos_row),
811        ("layout.pos.col".to_string(), layout_pos_col),
812    ];
813
814    Ok(make_grid_object(entries, &["viewport"]))
815}
816
817/// Push a viewport onto the viewport stack.
818///
819/// @param vp a viewport object
820/// @return NULL (invisibly)
821#[interpreter_builtin(name = "pushViewport", namespace = "grid", min_args = 1)]
822fn interp_push_viewport(
823    args: &[RValue],
824    _named: &[(String, RValue)],
825    context: &BuiltinContext,
826) -> Result<RValue, RError> {
827    let vp = args[0].clone();
828
829    // Record the push on the R-level display list
830    let push_record = make_grid_object(
831        vec![
832            (
833                "type".to_string(),
834                RValue::vec(Vector::Character(
835                    vec![Some("pushViewport".to_string())].into(),
836                )),
837            ),
838            ("viewport".to_string(), vp.clone()),
839        ],
840        &["vpOperation"],
841    );
842    record_on_display_list(&push_record, context);
843
844    // Also push on the Rust-level viewport stack and display list
845    let rust_vp = extract_viewport_from_rvalue(&vp);
846    context
847        .interpreter()
848        .grid_rust_viewport_stack
849        .borrow_mut()
850        .push(rust_vp.clone());
851    context
852        .interpreter()
853        .grid_rust_display_list
854        .borrow_mut()
855        .record(grid::display::DisplayItem::PushViewport(Box::new(rust_vp)));
856
857    context
858        .interpreter()
859        .grid_viewport_stack
860        .borrow_mut()
861        .push(vp);
862    context.interpreter().set_invisible();
863    Ok(RValue::Null)
864}
865
866/// Pop viewports from the viewport stack.
867///
868/// @param n number of viewports to pop (default 1)
869/// @return NULL (invisibly)
870#[interpreter_builtin(name = "popViewport", namespace = "grid")]
871fn interp_pop_viewport(
872    args: &[RValue],
873    named: &[(String, RValue)],
874    context: &BuiltinContext,
875) -> Result<RValue, RError> {
876    let ca = CallArgs::new(args, named);
877    let n = ca.integer_or("n", 0, 1);
878    let n = usize::try_from(n).unwrap_or(0);
879
880    let mut stack = context.interpreter().grid_viewport_stack.borrow_mut();
881    for _ in 0..n {
882        if stack.is_empty() {
883            return Err(RError::new(
884                RErrorKind::Other,
885                "cannot pop the top-level viewport".to_string(),
886            ));
887        }
888        stack.pop();
889    }
890
891    // Also pop from the Rust-level viewport stack and record on Rust display list
892    drop(stack);
893    {
894        let mut rust_stack = context.interpreter().grid_rust_viewport_stack.borrow_mut();
895        let mut rust_dl = context.interpreter().grid_rust_display_list.borrow_mut();
896        for _ in 0..n {
897            rust_stack.pop();
898            rust_dl.record(grid::display::DisplayItem::PopViewport);
899        }
900    }
901
902    // Record the pop on the R-level display list
903    let pop_record = make_grid_object(
904        vec![
905            (
906                "type".to_string(),
907                RValue::vec(Vector::Character(
908                    vec![Some("popViewport".to_string())].into(),
909                )),
910            ),
911            (
912                "n".to_string(),
913                RValue::vec(Vector::Integer(vec![Some(i64::from(n as i32))].into())),
914            ),
915        ],
916        &["vpOperation"],
917    );
918    record_on_display_list(&pop_record, context);
919
920    context.interpreter().set_invisible();
921    Ok(RValue::Null)
922}
923
924/// Return the current (topmost) viewport.
925///
926/// @return the current viewport object, or a default root viewport
927#[interpreter_builtin(name = "current.viewport", namespace = "grid")]
928fn interp_current_viewport(
929    _args: &[RValue],
930    _named: &[(String, RValue)],
931    context: &BuiltinContext,
932) -> Result<RValue, RError> {
933    let stack = context.interpreter().grid_viewport_stack.borrow();
934    match stack.last() {
935        Some(vp) => Ok(vp.clone()),
936        None => {
937            // Return a default root viewport
938            Ok(make_grid_object(
939                vec![
940                    ("x".to_string(), default_npc_half()),
941                    ("y".to_string(), default_npc_half()),
942                    ("width".to_string(), default_npc_one()),
943                    ("height".to_string(), default_npc_one()),
944                    (
945                        "name".to_string(),
946                        RValue::vec(Vector::Character(vec![Some("ROOT".to_string())].into())),
947                    ),
948                ],
949                &["viewport"],
950            ))
951        }
952    }
953}
954
955/// Navigate up the viewport stack without popping.
956///
957/// @param n number of levels to navigate up (default 1)
958/// @return NULL (invisibly)
959#[interpreter_builtin(name = "upViewport", namespace = "grid")]
960fn interp_up_viewport(
961    args: &[RValue],
962    named: &[(String, RValue)],
963    context: &BuiltinContext,
964) -> Result<RValue, RError> {
965    let ca = CallArgs::new(args, named);
966    let n = ca.integer_or("n", 0, 1);
967    let n = usize::try_from(n).unwrap_or(0);
968
969    let stack_len = context.interpreter().grid_viewport_stack.borrow().len();
970    if n > stack_len {
971        return Err(RError::new(
972            RErrorKind::Other,
973            format!("cannot navigate up {n} viewport(s) — only {stack_len} on the stack"),
974        ));
975    }
976
977    // upViewport navigates without popping — in our simplified model,
978    // we record the operation but keep the stack intact for query purposes.
979    let up_record = make_grid_object(
980        vec![
981            (
982                "type".to_string(),
983                RValue::vec(Vector::Character(
984                    vec![Some("upViewport".to_string())].into(),
985                )),
986            ),
987            (
988                "n".to_string(),
989                RValue::vec(Vector::Integer(vec![Some(i64::from(n as i32))].into())),
990            ),
991        ],
992        &["vpOperation"],
993    );
994    record_on_display_list(&up_record, context);
995
996    context.interpreter().set_invisible();
997    Ok(RValue::Null)
998}
999
1000/// Navigate down to a named viewport in the stack.
1001///
1002/// @param name the name of the viewport to navigate to
1003/// @return the depth navigated (integer), or error if not found
1004#[interpreter_builtin(name = "downViewport", namespace = "grid", min_args = 1)]
1005fn interp_down_viewport(
1006    args: &[RValue],
1007    named: &[(String, RValue)],
1008    context: &BuiltinContext,
1009) -> Result<RValue, RError> {
1010    let ca = CallArgs::new(args, named);
1011    let name = ca.string("name", 0)?;
1012
1013    match find_viewport_by_name(&name, context) {
1014        Some(idx) => {
1015            let stack_len = context.interpreter().grid_viewport_stack.borrow().len();
1016            let depth = stack_len.saturating_sub(idx + 1);
1017            Ok(RValue::vec(Vector::Integer(
1018                vec![Some(i64::try_from(depth).unwrap_or(0))].into(),
1019            )))
1020        }
1021        None => Err(RError::new(
1022            RErrorKind::Other,
1023            format!("viewport '{name}' was not found"),
1024        )),
1025    }
1026}
1027
1028/// Find and navigate to a named viewport anywhere in the viewport tree.
1029///
1030/// @param name the name of the viewport to seek
1031/// @return the depth navigated (integer), or error if not found
1032#[interpreter_builtin(name = "seekViewport", namespace = "grid", min_args = 1)]
1033fn interp_seek_viewport(
1034    args: &[RValue],
1035    named: &[(String, RValue)],
1036    context: &BuiltinContext,
1037) -> Result<RValue, RError> {
1038    let ca = CallArgs::new(args, named);
1039    let name = ca.string("name", 0)?;
1040
1041    match find_viewport_by_name(&name, context) {
1042        Some(idx) => {
1043            let stack_len = context.interpreter().grid_viewport_stack.borrow().len();
1044            let depth = stack_len.saturating_sub(idx + 1);
1045            Ok(RValue::vec(Vector::Integer(
1046                vec![Some(i64::try_from(depth).unwrap_or(0))].into(),
1047            )))
1048        }
1049        None => Err(RError::new(
1050            RErrorKind::Other,
1051            format!("viewport '{name}' was not found"),
1052        )),
1053    }
1054}
1055
1056/// Create a viewport with margins specified in lines of text.
1057///
1058/// This is a convenience wrapper around viewport() that converts margin
1059/// specifications (in lines of text) to appropriate offsets.
1060///
1061/// @param margins numeric vector c(bottom, left, top, right) in lines (default c(5.1, 4.1, 4.1, 2.1))
1062/// @return a viewport object
1063#[interpreter_builtin(name = "plotViewport", namespace = "grid")]
1064fn interp_plot_viewport(
1065    args: &[RValue],
1066    named: &[(String, RValue)],
1067    _context: &BuiltinContext,
1068) -> Result<RValue, RError> {
1069    let ca = CallArgs::new(args, named);
1070
1071    let margins = if let Some(val) = ca.value("margins", 0) {
1072        if let Some(rv) = val.as_vector() {
1073            rv.to_doubles()
1074        } else {
1075            vec![Some(5.1), Some(4.1), Some(4.1), Some(2.1)]
1076        }
1077    } else {
1078        vec![Some(5.1), Some(4.1), Some(4.1), Some(2.1)]
1079    };
1080
1081    // margins: c(bottom, left, top, right)
1082    let bottom = margins.first().copied().flatten().unwrap_or(5.1);
1083    let left = margins.get(1).copied().flatten().unwrap_or(4.1);
1084    let top = margins.get(2).copied().flatten().unwrap_or(4.1);
1085    let right = margins.get(3).copied().flatten().unwrap_or(2.1);
1086
1087    // Create viewport with margin-adjusted position and size
1088    let entries = vec![
1089        (
1090            "x".to_string(),
1091            make_grid_object(
1092                vec![
1093                    (
1094                        "value".to_string(),
1095                        RValue::vec(Vector::Double(vec![Some(0.5 * (left - right))].into())),
1096                    ),
1097                    (
1098                        "units".to_string(),
1099                        RValue::vec(Vector::Character(vec![Some("lines".to_string())].into())),
1100                    ),
1101                ],
1102                &["unit"],
1103            ),
1104        ),
1105        (
1106            "y".to_string(),
1107            make_grid_object(
1108                vec![
1109                    (
1110                        "value".to_string(),
1111                        RValue::vec(Vector::Double(vec![Some(0.5 * (bottom - top))].into())),
1112                    ),
1113                    (
1114                        "units".to_string(),
1115                        RValue::vec(Vector::Character(vec![Some("lines".to_string())].into())),
1116                    ),
1117                ],
1118                &["unit"],
1119            ),
1120        ),
1121        (
1122            "width".to_string(),
1123            make_grid_object(
1124                vec![
1125                    (
1126                        "value".to_string(),
1127                        RValue::vec(Vector::Double(vec![Some(-(left + right))].into())),
1128                    ),
1129                    (
1130                        "units".to_string(),
1131                        RValue::vec(Vector::Character(vec![Some("lines".to_string())].into())),
1132                    ),
1133                ],
1134                &["unit"],
1135            ),
1136        ),
1137        (
1138            "height".to_string(),
1139            make_grid_object(
1140                vec![
1141                    (
1142                        "value".to_string(),
1143                        RValue::vec(Vector::Double(vec![Some(-(bottom + top))].into())),
1144                    ),
1145                    (
1146                        "units".to_string(),
1147                        RValue::vec(Vector::Character(vec![Some("lines".to_string())].into())),
1148                    ),
1149                ],
1150                &["unit"],
1151            ),
1152        ),
1153        (
1154            "just".to_string(),
1155            RValue::vec(Vector::Character(vec![Some("centre".to_string())].into())),
1156        ),
1157    ];
1158
1159    Ok(make_grid_object(entries, &["viewport"]))
1160}
1161
1162/// Create a viewport with scales determined by data ranges.
1163///
1164/// @param xData numeric vector of x-axis data
1165/// @param yData numeric vector of y-axis data
1166/// @param extension fraction to extend scales beyond data range (default 0.05)
1167/// @return a viewport object with appropriate xscale/yscale
1168#[interpreter_builtin(name = "dataViewport", namespace = "grid")]
1169fn interp_data_viewport(
1170    args: &[RValue],
1171    named: &[(String, RValue)],
1172    _context: &BuiltinContext,
1173) -> Result<RValue, RError> {
1174    let ca = CallArgs::new(args, named);
1175
1176    let extension = ca
1177        .value("extension", 2)
1178        .and_then(|v| v.as_vector()?.as_double_scalar())
1179        .unwrap_or(0.05);
1180
1181    // Compute xscale from xData
1182    let xscale = if let Some(xdata) = ca.value("xData", 0) {
1183        if let Some(rv) = xdata.as_vector() {
1184            let doubles: Vec<f64> = rv.to_doubles().into_iter().flatten().collect();
1185            if doubles.is_empty() {
1186                vec![Some(0.0), Some(1.0)]
1187            } else {
1188                let min = doubles.iter().copied().fold(f64::INFINITY, f64::min);
1189                let max = doubles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1190                let range = max - min;
1191                vec![Some(min - range * extension), Some(max + range * extension)]
1192            }
1193        } else {
1194            vec![Some(0.0), Some(1.0)]
1195        }
1196    } else {
1197        vec![Some(0.0), Some(1.0)]
1198    };
1199
1200    // Compute yscale from yData
1201    let yscale = if let Some(ydata) = ca.value("yData", 1) {
1202        if let Some(rv) = ydata.as_vector() {
1203            let doubles: Vec<f64> = rv.to_doubles().into_iter().flatten().collect();
1204            if doubles.is_empty() {
1205                vec![Some(0.0), Some(1.0)]
1206            } else {
1207                let min = doubles.iter().copied().fold(f64::INFINITY, f64::min);
1208                let max = doubles.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1209                let range = max - min;
1210                vec![Some(min - range * extension), Some(max + range * extension)]
1211            }
1212        } else {
1213            vec![Some(0.0), Some(1.0)]
1214        }
1215    } else {
1216        vec![Some(0.0), Some(1.0)]
1217    };
1218
1219    let entries = vec![
1220        ("x".to_string(), default_npc_half()),
1221        ("y".to_string(), default_npc_half()),
1222        ("width".to_string(), default_npc_one()),
1223        ("height".to_string(), default_npc_one()),
1224        (
1225            "just".to_string(),
1226            RValue::vec(Vector::Character(vec![Some("centre".to_string())].into())),
1227        ),
1228        (
1229            "xscale".to_string(),
1230            RValue::vec(Vector::Double(xscale.into())),
1231        ),
1232        (
1233            "yscale".to_string(),
1234            RValue::vec(Vector::Double(yscale.into())),
1235        ),
1236    ];
1237
1238    Ok(make_grid_object(entries, &["viewport"]))
1239}
1240
1241// endregion
1242
1243// region: Grob primitives
1244
1245/// Helper to create a grob and optionally draw it.
1246///
1247/// 1. Creates the grob R object (list with class `c(type_class, "grob")`)
1248/// 2. If `draw=TRUE`, records it on the display list
1249/// 3. If `vp` is provided, pushes the viewport, records the grob, pops the viewport
1250/// 4. Returns the grob invisibly
1251fn make_grob(
1252    type_class: &str,
1253    entries: Vec<(String, RValue)>,
1254    draw: bool,
1255    vp: RValue,
1256    ctx: &BuiltinContext,
1257) -> Result<RValue, RError> {
1258    let grob = make_grid_object(entries.clone(), &[type_class, "grob"]);
1259
1260    if draw {
1261        // Build the Rust-level grob from the R entries
1262        let rust_grob = build_rust_grob(type_class, &entries);
1263
1264        match vp {
1265            RValue::Null => {
1266                record_on_display_list(&grob, ctx);
1267                if let Some(rg) = rust_grob {
1268                    record_rust_grob(rg, ctx);
1269                }
1270            }
1271            _ => {
1272                // Push viewport, draw, pop — both R-level and Rust-level
1273                let push_record = make_grid_object(
1274                    vec![
1275                        (
1276                            "type".to_string(),
1277                            RValue::vec(Vector::Character(
1278                                vec![Some("pushViewport".to_string())].into(),
1279                            )),
1280                        ),
1281                        ("viewport".to_string(), vp.clone()),
1282                    ],
1283                    &["vpOperation"],
1284                );
1285                record_on_display_list(&push_record, ctx);
1286
1287                // Rust-level viewport push
1288                let rust_vp = extract_viewport_from_rvalue(&vp);
1289                ctx.interpreter()
1290                    .grid_rust_viewport_stack
1291                    .borrow_mut()
1292                    .push(rust_vp.clone());
1293                ctx.interpreter()
1294                    .grid_rust_display_list
1295                    .borrow_mut()
1296                    .record(grid::display::DisplayItem::PushViewport(Box::new(rust_vp)));
1297
1298                ctx.interpreter().grid_viewport_stack.borrow_mut().push(vp);
1299
1300                record_on_display_list(&grob, ctx);
1301                if let Some(rg) = rust_grob {
1302                    record_rust_grob(rg, ctx);
1303                }
1304
1305                ctx.interpreter().grid_viewport_stack.borrow_mut().pop();
1306
1307                // Rust-level viewport pop
1308                ctx.interpreter()
1309                    .grid_rust_viewport_stack
1310                    .borrow_mut()
1311                    .pop();
1312                ctx.interpreter()
1313                    .grid_rust_display_list
1314                    .borrow_mut()
1315                    .record(grid::display::DisplayItem::PopViewport);
1316
1317                let pop_record = make_grid_object(
1318                    vec![
1319                        (
1320                            "type".to_string(),
1321                            RValue::vec(Vector::Character(
1322                                vec![Some("popViewport".to_string())].into(),
1323                            )),
1324                        ),
1325                        (
1326                            "n".to_string(),
1327                            RValue::vec(Vector::Integer(vec![Some(1)].into())),
1328                        ),
1329                    ],
1330                    &["vpOperation"],
1331                );
1332                record_on_display_list(&pop_record, ctx);
1333            }
1334        }
1335    }
1336
1337    ctx.interpreter().set_invisible();
1338    Ok(grob)
1339}
1340
1341/// Build a Rust-level `Grob` from a type class name and list entries.
1342///
1343/// Returns `None` for grob types we don't yet have Rust-level support for
1344/// (e.g. axes, which are composite grobs).
1345fn build_rust_grob(type_class: &str, entries: &[(String, RValue)]) -> Option<grid::grob::Grob> {
1346    // Helper to look up an entry by name
1347    let get =
1348        |name: &str| -> Option<&RValue> { entries.iter().find(|(k, _)| k == name).map(|(_, v)| v) };
1349
1350    match type_class {
1351        "lines" => {
1352            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1353            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1354            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1355            Some(grid::grob::Grob::Lines { x, y, gp })
1356        }
1357        "segments" => {
1358            let x0 = extract_unit_from_rvalue(get("x0").unwrap_or(&RValue::Null));
1359            let y0 = extract_unit_from_rvalue(get("y0").unwrap_or(&RValue::Null));
1360            let x1 = extract_unit_from_rvalue(get("x1").unwrap_or(&RValue::Null));
1361            let y1 = extract_unit_from_rvalue(get("y1").unwrap_or(&RValue::Null));
1362            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1363            Some(grid::grob::Grob::Segments { x0, y0, x1, y1, gp })
1364        }
1365        "points" => {
1366            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1367            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1368            let pch = extract_pch(get("pch").unwrap_or(&RValue::Null));
1369            let size = if let Some(sv) = get("size") {
1370                if matches!(sv, RValue::Null) {
1371                    grid::units::Unit::points(4.0)
1372                } else {
1373                    extract_unit_from_rvalue(sv)
1374                }
1375            } else {
1376                grid::units::Unit::points(4.0)
1377            };
1378            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1379            Some(grid::grob::Grob::Points {
1380                x,
1381                y,
1382                pch,
1383                size,
1384                gp,
1385            })
1386        }
1387        "rect" => {
1388            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1389            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1390            let width = extract_unit_from_rvalue(get("width").unwrap_or(&RValue::Null));
1391            let height = extract_unit_from_rvalue(get("height").unwrap_or(&RValue::Null));
1392            let just = extract_justification(get("just").unwrap_or(&RValue::Null));
1393            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1394            Some(grid::grob::Grob::Rect {
1395                x,
1396                y,
1397                width,
1398                height,
1399                just,
1400                gp,
1401            })
1402        }
1403        "circle" => {
1404            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1405            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1406            let r = extract_unit_from_rvalue(get("r").unwrap_or(&RValue::Null));
1407            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1408            Some(grid::grob::Grob::Circle { x, y, r, gp })
1409        }
1410        "polygon" => {
1411            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1412            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1413            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1414            Some(grid::grob::Grob::Polygon { x, y, gp })
1415        }
1416        "text" => {
1417            let label = extract_labels(get("label").unwrap_or(&RValue::Null));
1418            let x = extract_unit_from_rvalue(get("x").unwrap_or(&RValue::Null));
1419            let y = extract_unit_from_rvalue(get("y").unwrap_or(&RValue::Null));
1420            let just = extract_justification(get("just").unwrap_or(&RValue::Null));
1421            let rot = extract_rot(get("rot").unwrap_or(&RValue::Null));
1422            let gp = extract_gpar_from_rvalue(get("gp").unwrap_or(&RValue::Null));
1423            Some(grid::grob::Grob::Text {
1424                label,
1425                x,
1426                y,
1427                just,
1428                rot,
1429                gp,
1430            })
1431        }
1432        // Axes and other composite grobs don't have direct Rust Grob equivalents yet
1433        _ => None,
1434    }
1435}
1436
1437/// Draw line segments (polyline) on the grid graphics device.
1438///
1439/// @param x x-coordinates
1440/// @param y y-coordinates
1441/// @param default.units default unit type for coordinates
1442/// @param gp graphical parameters
1443/// @param vp viewport to use
1444/// @param name grob name
1445/// @param draw whether to draw immediately (default TRUE)
1446/// @return a lines grob (invisibly)
1447#[interpreter_builtin(name = "grid.lines", namespace = "grid")]
1448fn interp_grid_lines(
1449    args: &[RValue],
1450    named: &[(String, RValue)],
1451    context: &BuiltinContext,
1452) -> Result<RValue, RError> {
1453    let ca = CallArgs::new(args, named);
1454    let default_units = ca
1455        .optional_string("default.units", 2)
1456        .unwrap_or_else(|| "npc".to_string());
1457    let draw = ca.logical_flag("draw", 6, true);
1458    let vp = opt_value(&ca, "vp", 4);
1459    let name = ca
1460        .optional_string("name", 5)
1461        .unwrap_or_else(|| auto_grob_name("GRID.lines", context));
1462
1463    let x = ca
1464        .value("x", 0)
1465        .cloned()
1466        .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into())));
1467    let y = ca
1468        .value("y", 1)
1469        .cloned()
1470        .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into())));
1471
1472    let x = ensure_unit(&x, &default_units);
1473    let y = ensure_unit(&y, &default_units);
1474
1475    let gp = opt_value(&ca, "gp", 3);
1476
1477    let entries = vec![
1478        ("x".to_string(), x),
1479        ("y".to_string(), y),
1480        ("gp".to_string(), gp),
1481        (
1482            "name".to_string(),
1483            RValue::vec(Vector::Character(vec![Some(name)].into())),
1484        ),
1485    ];
1486
1487    make_grob("lines", entries, draw, vp, context)
1488}
1489
1490/// Draw line segments between pairs of points.
1491///
1492/// @param x0 x-coordinates of start points
1493/// @param y0 y-coordinates of start points
1494/// @param x1 x-coordinates of end points
1495/// @param y1 y-coordinates of end points
1496/// @param default.units default unit type for coordinates
1497/// @param gp graphical parameters
1498/// @param vp viewport to use
1499/// @param name grob name
1500/// @param draw whether to draw immediately (default TRUE)
1501/// @return a segments grob (invisibly)
1502#[interpreter_builtin(name = "grid.segments", namespace = "grid")]
1503fn interp_grid_segments(
1504    args: &[RValue],
1505    named: &[(String, RValue)],
1506    context: &BuiltinContext,
1507) -> Result<RValue, RError> {
1508    let ca = CallArgs::new(args, named);
1509    let default_units = ca
1510        .optional_string("default.units", 4)
1511        .unwrap_or_else(|| "npc".to_string());
1512    let draw = ca.logical_flag("draw", 8, true);
1513    let vp = opt_value(&ca, "vp", 6);
1514    let name = ca
1515        .optional_string("name", 7)
1516        .unwrap_or_else(|| auto_grob_name("GRID.segments", context));
1517
1518    let x0 = ensure_unit(
1519        &ca.value("x0", 0).cloned().unwrap_or_else(default_npc_half),
1520        &default_units,
1521    );
1522    let y0 = ensure_unit(
1523        &ca.value("y0", 1).cloned().unwrap_or_else(default_npc_half),
1524        &default_units,
1525    );
1526    let x1 = ensure_unit(
1527        &ca.value("x1", 2).cloned().unwrap_or_else(default_npc_half),
1528        &default_units,
1529    );
1530    let y1 = ensure_unit(
1531        &ca.value("y1", 3).cloned().unwrap_or_else(default_npc_half),
1532        &default_units,
1533    );
1534    let gp = opt_value(&ca, "gp", 5);
1535
1536    let entries = vec![
1537        ("x0".to_string(), x0),
1538        ("y0".to_string(), y0),
1539        ("x1".to_string(), x1),
1540        ("y1".to_string(), y1),
1541        ("gp".to_string(), gp),
1542        (
1543            "name".to_string(),
1544            RValue::vec(Vector::Character(vec![Some(name)].into())),
1545        ),
1546    ];
1547
1548    make_grob("segments", entries, draw, vp, context)
1549}
1550
1551/// Draw points on the grid graphics device.
1552///
1553/// @param x x-coordinates
1554/// @param y y-coordinates
1555/// @param pch plotting character (default 1)
1556/// @param size point size
1557/// @param default.units default unit type for coordinates
1558/// @param gp graphical parameters
1559/// @param vp viewport to use
1560/// @param name grob name
1561/// @param draw whether to draw immediately (default TRUE)
1562/// @return a points grob (invisibly)
1563#[interpreter_builtin(name = "grid.points", namespace = "grid")]
1564fn interp_grid_points(
1565    args: &[RValue],
1566    named: &[(String, RValue)],
1567    context: &BuiltinContext,
1568) -> Result<RValue, RError> {
1569    let ca = CallArgs::new(args, named);
1570    let default_units = ca
1571        .optional_string("default.units", 4)
1572        .unwrap_or_else(|| "npc".to_string());
1573    let draw = ca.logical_flag("draw", 8, true);
1574    let vp = opt_value(&ca, "vp", 6);
1575    let name = ca
1576        .optional_string("name", 7)
1577        .unwrap_or_else(|| auto_grob_name("GRID.points", context));
1578
1579    let x = ensure_unit(
1580        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
1581        &default_units,
1582    );
1583    let y = ensure_unit(
1584        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
1585        &default_units,
1586    );
1587    let pch = opt_value(&ca, "pch", 2);
1588    let size = opt_value(&ca, "size", 3);
1589    let gp = opt_value(&ca, "gp", 5);
1590
1591    let entries = vec![
1592        ("x".to_string(), x),
1593        ("y".to_string(), y),
1594        ("pch".to_string(), pch),
1595        ("size".to_string(), size),
1596        ("gp".to_string(), gp),
1597        (
1598            "name".to_string(),
1599            RValue::vec(Vector::Character(vec![Some(name)].into())),
1600        ),
1601    ];
1602
1603    make_grob("points", entries, draw, vp, context)
1604}
1605
1606/// Draw a rectangle on the grid graphics device.
1607///
1608/// @param x x-coordinate of center
1609/// @param y y-coordinate of center
1610/// @param width rectangle width
1611/// @param height rectangle height
1612/// @param just justification
1613/// @param default.units default unit type
1614/// @param gp graphical parameters
1615/// @param vp viewport to use
1616/// @param name grob name
1617/// @param draw whether to draw immediately (default TRUE)
1618/// @return a rect grob (invisibly)
1619#[interpreter_builtin(name = "grid.rect", namespace = "grid")]
1620fn interp_grid_rect(
1621    args: &[RValue],
1622    named: &[(String, RValue)],
1623    context: &BuiltinContext,
1624) -> Result<RValue, RError> {
1625    let ca = CallArgs::new(args, named);
1626    let default_units = ca
1627        .optional_string("default.units", 5)
1628        .unwrap_or_else(|| "npc".to_string());
1629    let draw = ca.logical_flag("draw", 9, true);
1630    let vp = opt_value(&ca, "vp", 7);
1631    let name = ca
1632        .optional_string("name", 8)
1633        .unwrap_or_else(|| auto_grob_name("GRID.rect", context));
1634
1635    let x = ensure_unit(
1636        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
1637        &default_units,
1638    );
1639    let y = ensure_unit(
1640        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
1641        &default_units,
1642    );
1643    let width = ensure_unit(
1644        &ca.value("width", 2)
1645            .cloned()
1646            .unwrap_or_else(default_npc_one),
1647        &default_units,
1648    );
1649    let height = ensure_unit(
1650        &ca.value("height", 3)
1651            .cloned()
1652            .unwrap_or_else(default_npc_one),
1653        &default_units,
1654    );
1655    let just = opt_value(&ca, "just", 4);
1656    let gp = opt_value(&ca, "gp", 6);
1657
1658    let entries = vec![
1659        ("x".to_string(), x),
1660        ("y".to_string(), y),
1661        ("width".to_string(), width),
1662        ("height".to_string(), height),
1663        ("just".to_string(), just),
1664        ("gp".to_string(), gp),
1665        (
1666            "name".to_string(),
1667            RValue::vec(Vector::Character(vec![Some(name)].into())),
1668        ),
1669    ];
1670
1671    make_grob("rect", entries, draw, vp, context)
1672}
1673
1674/// Draw a circle on the grid graphics device.
1675///
1676/// @param x x-coordinate of center
1677/// @param y y-coordinate of center
1678/// @param r radius
1679/// @param default.units default unit type
1680/// @param gp graphical parameters
1681/// @param vp viewport to use
1682/// @param name grob name
1683/// @param draw whether to draw immediately (default TRUE)
1684/// @return a circle grob (invisibly)
1685#[interpreter_builtin(name = "grid.circle", namespace = "grid")]
1686fn interp_grid_circle(
1687    args: &[RValue],
1688    named: &[(String, RValue)],
1689    context: &BuiltinContext,
1690) -> Result<RValue, RError> {
1691    let ca = CallArgs::new(args, named);
1692    let default_units = ca
1693        .optional_string("default.units", 3)
1694        .unwrap_or_else(|| "npc".to_string());
1695    let draw = ca.logical_flag("draw", 7, true);
1696    let vp = opt_value(&ca, "vp", 5);
1697    let name = ca
1698        .optional_string("name", 6)
1699        .unwrap_or_else(|| auto_grob_name("GRID.circle", context));
1700
1701    let x = ensure_unit(
1702        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
1703        &default_units,
1704    );
1705    let y = ensure_unit(
1706        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
1707        &default_units,
1708    );
1709    let r = ensure_unit(
1710        &ca.value("r", 2).cloned().unwrap_or_else(default_npc_half),
1711        &default_units,
1712    );
1713    let gp = opt_value(&ca, "gp", 4);
1714
1715    let entries = vec![
1716        ("x".to_string(), x),
1717        ("y".to_string(), y),
1718        ("r".to_string(), r),
1719        ("gp".to_string(), gp),
1720        (
1721            "name".to_string(),
1722            RValue::vec(Vector::Character(vec![Some(name)].into())),
1723        ),
1724    ];
1725
1726    make_grob("circle", entries, draw, vp, context)
1727}
1728
1729/// Draw a polygon on the grid graphics device.
1730///
1731/// @param x x-coordinates of vertices
1732/// @param y y-coordinates of vertices
1733/// @param default.units default unit type
1734/// @param gp graphical parameters
1735/// @param vp viewport to use
1736/// @param name grob name
1737/// @param draw whether to draw immediately (default TRUE)
1738/// @return a polygon grob (invisibly)
1739#[interpreter_builtin(name = "grid.polygon", namespace = "grid")]
1740fn interp_grid_polygon(
1741    args: &[RValue],
1742    named: &[(String, RValue)],
1743    context: &BuiltinContext,
1744) -> Result<RValue, RError> {
1745    let ca = CallArgs::new(args, named);
1746    let default_units = ca
1747        .optional_string("default.units", 2)
1748        .unwrap_or_else(|| "npc".to_string());
1749    let draw = ca.logical_flag("draw", 6, true);
1750    let vp = opt_value(&ca, "vp", 4);
1751    let name = ca
1752        .optional_string("name", 5)
1753        .unwrap_or_else(|| auto_grob_name("GRID.polygon", context));
1754
1755    let x = ensure_unit(
1756        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
1757        &default_units,
1758    );
1759    let y = ensure_unit(
1760        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
1761        &default_units,
1762    );
1763    let gp = opt_value(&ca, "gp", 3);
1764
1765    let entries = vec![
1766        ("x".to_string(), x),
1767        ("y".to_string(), y),
1768        ("gp".to_string(), gp),
1769        (
1770            "name".to_string(),
1771            RValue::vec(Vector::Character(vec![Some(name)].into())),
1772        ),
1773    ];
1774
1775    make_grob("polygon", entries, draw, vp, context)
1776}
1777
1778/// Draw text on the grid graphics device.
1779///
1780/// @param label character string(s) to draw
1781/// @param x x-coordinate(s)
1782/// @param y y-coordinate(s)
1783/// @param just justification
1784/// @param rot rotation angle in degrees
1785/// @param default.units default unit type
1786/// @param gp graphical parameters
1787/// @param vp viewport to use
1788/// @param name grob name
1789/// @param draw whether to draw immediately (default TRUE)
1790/// @return a text grob (invisibly)
1791#[interpreter_builtin(name = "grid.text", namespace = "grid")]
1792fn interp_grid_text(
1793    args: &[RValue],
1794    named: &[(String, RValue)],
1795    context: &BuiltinContext,
1796) -> Result<RValue, RError> {
1797    let ca = CallArgs::new(args, named);
1798    let default_units = ca
1799        .optional_string("default.units", 5)
1800        .unwrap_or_else(|| "npc".to_string());
1801    let draw = ca.logical_flag("draw", 9, true);
1802    let vp = opt_value(&ca, "vp", 7);
1803    let name = ca
1804        .optional_string("name", 8)
1805        .unwrap_or_else(|| auto_grob_name("GRID.text", context));
1806
1807    let label = opt_value(&ca, "label", 0);
1808    let x = ensure_unit(
1809        &ca.value("x", 1).cloned().unwrap_or_else(default_npc_half),
1810        &default_units,
1811    );
1812    let y = ensure_unit(
1813        &ca.value("y", 2).cloned().unwrap_or_else(default_npc_half),
1814        &default_units,
1815    );
1816    let just = opt_value(&ca, "just", 3);
1817    let rot = opt_value(&ca, "rot", 4);
1818    let gp = opt_value(&ca, "gp", 6);
1819
1820    let entries = vec![
1821        ("label".to_string(), label),
1822        ("x".to_string(), x),
1823        ("y".to_string(), y),
1824        ("just".to_string(), just),
1825        ("rot".to_string(), rot),
1826        ("gp".to_string(), gp),
1827        (
1828            "name".to_string(),
1829            RValue::vec(Vector::Character(vec![Some(name)].into())),
1830        ),
1831    ];
1832
1833    make_grob("text", entries, draw, vp, context)
1834}
1835
1836// endregion
1837
1838// region: Grob constructors (*Grob — create without drawing)
1839
1840/// Create a text grob object without drawing it.
1841///
1842/// Equivalent to `grid.text(..., draw=FALSE)`.
1843///
1844/// @param label text to display
1845/// @param x,y position (unit or numeric in default.units)
1846/// @param just justification
1847/// @param rot rotation angle in degrees
1848/// @param gp graphical parameters (gpar)
1849/// @param name unique grob name
1850/// @param vp viewport
1851/// @return a text grob object
1852/// @namespace grid
1853#[interpreter_builtin(name = "textGrob", namespace = "grid")]
1854fn interp_text_grob(
1855    args: &[RValue],
1856    named: &[(String, RValue)],
1857    context: &BuiltinContext,
1858) -> Result<RValue, RError> {
1859    let ca = CallArgs::new(args, named);
1860    let default_units = ca
1861        .optional_string("default.units", 5)
1862        .unwrap_or_else(|| "npc".to_string());
1863    let vp = opt_value(&ca, "vp", 7);
1864    let name = ca
1865        .optional_string("name", 8)
1866        .unwrap_or_else(|| auto_grob_name("GRID.text", context));
1867
1868    let label = opt_value(&ca, "label", 0);
1869    let x = ensure_unit(
1870        &ca.value("x", 1).cloned().unwrap_or_else(default_npc_half),
1871        &default_units,
1872    );
1873    let y = ensure_unit(
1874        &ca.value("y", 2).cloned().unwrap_or_else(default_npc_half),
1875        &default_units,
1876    );
1877    let just = normalize_just(&ca, "just", 3);
1878    let hjust = opt_value(&ca, "hjust", 9);
1879    let vjust = opt_value(&ca, "vjust", 10);
1880    let rot = opt_value(&ca, "rot", 4);
1881    let check_overlap = opt_value(&ca, "check.overlap", 11);
1882    let gp = opt_value(&ca, "gp", 6);
1883
1884    let entries = vec![
1885        ("label".to_string(), label),
1886        ("x".to_string(), x),
1887        ("y".to_string(), y),
1888        ("just".to_string(), just),
1889        ("hjust".to_string(), hjust),
1890        ("vjust".to_string(), vjust),
1891        ("rot".to_string(), rot),
1892        ("check.overlap".to_string(), check_overlap),
1893        ("gp".to_string(), gp),
1894        (
1895            "name".to_string(),
1896            RValue::vec(Vector::Character(vec![Some(name)].into())),
1897        ),
1898    ];
1899
1900    make_grob("text", entries, false, vp, context)
1901}
1902
1903/// Create a lines grob object without drawing it.
1904///
1905/// @param x,y positions (unit or numeric)
1906/// @param gp graphical parameters
1907/// @param name unique grob name
1908/// @param vp viewport
1909/// @return a lines grob object
1910/// @namespace grid
1911#[interpreter_builtin(name = "linesGrob", namespace = "grid")]
1912fn interp_lines_grob(
1913    args: &[RValue],
1914    named: &[(String, RValue)],
1915    context: &BuiltinContext,
1916) -> Result<RValue, RError> {
1917    let ca = CallArgs::new(args, named);
1918    let default_units = ca
1919        .optional_string("default.units", 4)
1920        .unwrap_or_else(|| "npc".to_string());
1921    let vp = opt_value(&ca, "vp", 5);
1922    let name = ca
1923        .optional_string("name", 3)
1924        .unwrap_or_else(|| auto_grob_name("GRID.lines", context));
1925
1926    let x = ensure_unit(
1927        &ca.value("x", 0)
1928            .cloned()
1929            .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into()))),
1930        &default_units,
1931    );
1932    let y = ensure_unit(
1933        &ca.value("y", 1)
1934            .cloned()
1935            .unwrap_or_else(|| RValue::vec(Vector::Double(vec![Some(0.0), Some(1.0)].into()))),
1936        &default_units,
1937    );
1938    let gp = opt_value(&ca, "gp", 2);
1939
1940    let entries = vec![
1941        ("x".to_string(), x),
1942        ("y".to_string(), y),
1943        ("gp".to_string(), gp),
1944        (
1945            "name".to_string(),
1946            RValue::vec(Vector::Character(vec![Some(name)].into())),
1947        ),
1948    ];
1949
1950    make_grob("lines", entries, false, vp, context)
1951}
1952
1953/// Create a points grob object without drawing it.
1954///
1955/// @param x,y positions
1956/// @param pch point character
1957/// @param size point size
1958/// @param gp graphical parameters
1959/// @param name unique grob name
1960/// @param vp viewport
1961/// @return a points grob object
1962/// @namespace grid
1963#[interpreter_builtin(name = "pointsGrob", namespace = "grid")]
1964fn interp_points_grob(
1965    args: &[RValue],
1966    named: &[(String, RValue)],
1967    context: &BuiltinContext,
1968) -> Result<RValue, RError> {
1969    let ca = CallArgs::new(args, named);
1970    let default_units = ca
1971        .optional_string("default.units", 5)
1972        .unwrap_or_else(|| "npc".to_string());
1973    let vp = opt_value(&ca, "vp", 6);
1974    let name = ca
1975        .optional_string("name", 4)
1976        .unwrap_or_else(|| auto_grob_name("GRID.points", context));
1977
1978    let x = ensure_unit(
1979        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
1980        &default_units,
1981    );
1982    let y = ensure_unit(
1983        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
1984        &default_units,
1985    );
1986    let pch = opt_value(&ca, "pch", 2);
1987    let size = opt_value(&ca, "size", 3);
1988    let gp = opt_value(&ca, "gp", 7);
1989
1990    let entries = vec![
1991        ("x".to_string(), x),
1992        ("y".to_string(), y),
1993        ("pch".to_string(), pch),
1994        ("size".to_string(), size),
1995        ("gp".to_string(), gp),
1996        (
1997            "name".to_string(),
1998            RValue::vec(Vector::Character(vec![Some(name)].into())),
1999        ),
2000    ];
2001
2002    make_grob("points", entries, false, vp, context)
2003}
2004
2005/// Create a rect grob object without drawing it.
2006///
2007/// @param x,y center position
2008/// @param width,height dimensions
2009/// @param just justification
2010/// @param gp graphical parameters
2011/// @param name unique grob name
2012/// @param vp viewport
2013/// @return a rect grob object
2014/// @namespace grid
2015#[interpreter_builtin(name = "rectGrob", namespace = "grid")]
2016fn interp_rect_grob(
2017    args: &[RValue],
2018    named: &[(String, RValue)],
2019    context: &BuiltinContext,
2020) -> Result<RValue, RError> {
2021    let ca = CallArgs::new(args, named);
2022    let default_units = ca
2023        .optional_string("default.units", 6)
2024        .unwrap_or_else(|| "npc".to_string());
2025    let vp = opt_value(&ca, "vp", 7);
2026    let name = ca
2027        .optional_string("name", 5)
2028        .unwrap_or_else(|| auto_grob_name("GRID.rect", context));
2029
2030    let x = ensure_unit(
2031        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
2032        &default_units,
2033    );
2034    let y = ensure_unit(
2035        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
2036        &default_units,
2037    );
2038    let width = opt_value(&ca, "width", 2);
2039    let height = opt_value(&ca, "height", 3);
2040    let just = normalize_just(&ca, "just", 4);
2041    let hjust = opt_value(&ca, "hjust", 9);
2042    let vjust = opt_value(&ca, "vjust", 10);
2043    let gp = opt_value(&ca, "gp", 8);
2044
2045    let entries = vec![
2046        ("x".to_string(), x),
2047        ("y".to_string(), y),
2048        ("width".to_string(), width),
2049        ("height".to_string(), height),
2050        ("just".to_string(), just),
2051        ("hjust".to_string(), hjust),
2052        ("vjust".to_string(), vjust),
2053        ("gp".to_string(), gp),
2054        (
2055            "name".to_string(),
2056            RValue::vec(Vector::Character(vec![Some(name)].into())),
2057        ),
2058    ];
2059
2060    make_grob("rect", entries, false, vp, context)
2061}
2062
2063/// Create a circle grob object without drawing it.
2064///
2065/// @param x,y center position
2066/// @param r radius
2067/// @param gp graphical parameters
2068/// @param name unique grob name
2069/// @param vp viewport
2070/// @return a circle grob object
2071/// @namespace grid
2072#[interpreter_builtin(name = "circleGrob", namespace = "grid")]
2073fn interp_circle_grob(
2074    args: &[RValue],
2075    named: &[(String, RValue)],
2076    context: &BuiltinContext,
2077) -> Result<RValue, RError> {
2078    let ca = CallArgs::new(args, named);
2079    let default_units = ca
2080        .optional_string("default.units", 4)
2081        .unwrap_or_else(|| "npc".to_string());
2082    let vp = opt_value(&ca, "vp", 5);
2083    let name = ca
2084        .optional_string("name", 3)
2085        .unwrap_or_else(|| auto_grob_name("GRID.circle", context));
2086
2087    let x = ensure_unit(
2088        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
2089        &default_units,
2090    );
2091    let y = ensure_unit(
2092        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
2093        &default_units,
2094    );
2095    let r = opt_value(&ca, "r", 2);
2096    let gp = opt_value(&ca, "gp", 6);
2097
2098    let entries = vec![
2099        ("x".to_string(), x),
2100        ("y".to_string(), y),
2101        ("r".to_string(), r),
2102        ("gp".to_string(), gp),
2103        (
2104            "name".to_string(),
2105            RValue::vec(Vector::Character(vec![Some(name)].into())),
2106        ),
2107    ];
2108
2109    make_grob("circle", entries, false, vp, context)
2110}
2111
2112/// Create a polygon grob object without drawing it.
2113///
2114/// @param x,y positions
2115/// @param id grouping indicator
2116/// @param gp graphical parameters
2117/// @param name unique grob name
2118/// @param vp viewport
2119/// @return a polygon grob object
2120/// @namespace grid
2121#[interpreter_builtin(name = "polygonGrob", namespace = "grid")]
2122fn interp_polygon_grob(
2123    args: &[RValue],
2124    named: &[(String, RValue)],
2125    context: &BuiltinContext,
2126) -> Result<RValue, RError> {
2127    let ca = CallArgs::new(args, named);
2128    let default_units = ca
2129        .optional_string("default.units", 4)
2130        .unwrap_or_else(|| "npc".to_string());
2131    let vp = opt_value(&ca, "vp", 5);
2132    let name = ca
2133        .optional_string("name", 3)
2134        .unwrap_or_else(|| auto_grob_name("GRID.polygon", context));
2135
2136    let x = ensure_unit(
2137        &ca.value("x", 0).cloned().unwrap_or_else(default_npc_half),
2138        &default_units,
2139    );
2140    let y = ensure_unit(
2141        &ca.value("y", 1).cloned().unwrap_or_else(default_npc_half),
2142        &default_units,
2143    );
2144    let id = opt_value(&ca, "id", 2);
2145    let gp = opt_value(&ca, "gp", 6);
2146
2147    let entries = vec![
2148        ("x".to_string(), x),
2149        ("y".to_string(), y),
2150        ("id".to_string(), id),
2151        ("gp".to_string(), gp),
2152        (
2153            "name".to_string(),
2154            RValue::vec(Vector::Character(vec![Some(name)].into())),
2155        ),
2156    ];
2157
2158    make_grob("polygon", entries, false, vp, context)
2159}
2160
2161/// Create a segments grob object without drawing it.
2162///
2163/// @param x0,y0 start positions
2164/// @param x1,y1 end positions
2165/// @param gp graphical parameters
2166/// @param name unique grob name
2167/// @param vp viewport
2168/// @return a segments grob object
2169/// @namespace grid
2170#[interpreter_builtin(name = "segmentsGrob", namespace = "grid")]
2171fn interp_segments_grob(
2172    args: &[RValue],
2173    named: &[(String, RValue)],
2174    context: &BuiltinContext,
2175) -> Result<RValue, RError> {
2176    let ca = CallArgs::new(args, named);
2177    let default_units = ca
2178        .optional_string("default.units", 6)
2179        .unwrap_or_else(|| "npc".to_string());
2180    let vp = opt_value(&ca, "vp", 7);
2181    let name = ca
2182        .optional_string("name", 5)
2183        .unwrap_or_else(|| auto_grob_name("GRID.segments", context));
2184
2185    let x0 = ensure_unit(
2186        &ca.value("x0", 0).cloned().unwrap_or_else(default_npc_zero),
2187        &default_units,
2188    );
2189    let y0 = ensure_unit(
2190        &ca.value("y0", 1).cloned().unwrap_or_else(default_npc_zero),
2191        &default_units,
2192    );
2193    let x1 = ensure_unit(
2194        &ca.value("x1", 2).cloned().unwrap_or_else(default_npc_one),
2195        &default_units,
2196    );
2197    let y1 = ensure_unit(
2198        &ca.value("y1", 3).cloned().unwrap_or_else(default_npc_one),
2199        &default_units,
2200    );
2201    let gp = opt_value(&ca, "gp", 4);
2202
2203    let entries = vec![
2204        ("x0".to_string(), x0),
2205        ("y0".to_string(), y0),
2206        ("x1".to_string(), x1),
2207        ("y1".to_string(), y1),
2208        ("gp".to_string(), gp),
2209        (
2210            "name".to_string(),
2211            RValue::vec(Vector::Character(vec![Some(name)].into())),
2212        ),
2213    ];
2214
2215    make_grob("segments", entries, false, vp, context)
2216}
2217
2218/// Create a null grob (empty placeholder).
2219///
2220/// @param name unique grob name
2221/// @param vp viewport
2222/// @return a null grob object
2223/// @namespace grid
2224#[interpreter_builtin(name = "nullGrob", namespace = "grid")]
2225fn interp_null_grob(
2226    args: &[RValue],
2227    named: &[(String, RValue)],
2228    context: &BuiltinContext,
2229) -> Result<RValue, RError> {
2230    let ca = CallArgs::new(args, named);
2231    let name = ca
2232        .optional_string("name", 0)
2233        .unwrap_or_else(|| auto_grob_name("GRID.null", context));
2234
2235    let entries = vec![(
2236        "name".to_string(),
2237        RValue::vec(Vector::Character(vec![Some(name)].into())),
2238    )];
2239
2240    make_grob("null", entries, false, RValue::Null, context)
2241}
2242
2243/// Create a gTree (group of grobs).
2244///
2245/// @param children list of grobs (gList)
2246/// @param name unique grob name
2247/// @param gp graphical parameters
2248/// @param vp viewport
2249/// @return a gTree grob object
2250/// @namespace grid
2251#[interpreter_builtin(name = "gTree", namespace = "grid")]
2252fn interp_gtree(
2253    args: &[RValue],
2254    named: &[(String, RValue)],
2255    context: &BuiltinContext,
2256) -> Result<RValue, RError> {
2257    let ca = CallArgs::new(args, named);
2258    let name = ca
2259        .optional_string("name", 1)
2260        .unwrap_or_else(|| auto_grob_name("GRID.gTree", context));
2261    let children = opt_value(&ca, "children", 0);
2262    let gp = opt_value(&ca, "gp", 2);
2263    let vp = opt_value(&ca, "vp", 3);
2264
2265    let entries = vec![
2266        ("children".to_string(), children),
2267        ("gp".to_string(), gp),
2268        (
2269            "name".to_string(),
2270            RValue::vec(Vector::Character(vec![Some(name)].into())),
2271        ),
2272    ];
2273
2274    make_grob("gTree", entries, false, vp, context)
2275}
2276
2277/// Create a gList (list of grobs).
2278///
2279/// @param ... grobs to combine
2280/// @return a gList object
2281/// @namespace grid
2282#[interpreter_builtin(name = "gList", namespace = "grid")]
2283fn interp_glist(
2284    args: &[RValue],
2285    _named: &[(String, RValue)],
2286    _context: &BuiltinContext,
2287) -> Result<RValue, RError> {
2288    let entries: Vec<(Option<String>, RValue)> = args.iter().map(|a| (None, a.clone())).collect();
2289    let mut list = RList::new(entries);
2290    list.set_attr(
2291        "class".to_string(),
2292        RValue::vec(Vector::Character(vec![Some("gList".to_string())].into())),
2293    );
2294    Ok(RValue::List(list))
2295}
2296
2297// endregion
2298
2299// region: Layout
2300
2301/// Create a grid layout object specifying rows and columns.
2302///
2303/// @param nrow number of rows (default 1)
2304/// @param ncol number of columns (default 1)
2305/// @param widths column widths (unit object)
2306/// @param heights row heights (unit object)
2307/// @param respect logical or matrix controlling aspect ratio respect
2308/// @return a layout object (list with class "layout")
2309#[interpreter_builtin(name = "grid.layout", namespace = "grid")]
2310fn interp_grid_layout(
2311    args: &[RValue],
2312    named: &[(String, RValue)],
2313    _context: &BuiltinContext,
2314) -> Result<RValue, RError> {
2315    let ca = CallArgs::new(args, named);
2316
2317    let nrow = ca.integer_or("nrow", 0, 1);
2318    let ncol = ca.integer_or("ncol", 1, 1);
2319    let widths = opt_value(&ca, "widths", 2);
2320    let heights = opt_value(&ca, "heights", 3);
2321    let respect = opt_value(&ca, "respect", 4);
2322
2323    // Default widths/heights: equal-sized units
2324    let widths = if matches!(widths, RValue::Null) {
2325        let vals: Vec<Option<f64>> = (0..ncol).map(|_| Some(1.0)).collect();
2326        make_grid_object(
2327            vec![
2328                (
2329                    "value".to_string(),
2330                    RValue::vec(Vector::Double(vals.into())),
2331                ),
2332                (
2333                    "units".to_string(),
2334                    RValue::vec(Vector::Character(
2335                        (0..ncol)
2336                            .map(|_| Some("null".to_string()))
2337                            .collect::<Vec<_>>()
2338                            .into(),
2339                    )),
2340                ),
2341            ],
2342            &["unit"],
2343        )
2344    } else {
2345        widths
2346    };
2347
2348    let heights = if matches!(heights, RValue::Null) {
2349        let vals: Vec<Option<f64>> = (0..nrow).map(|_| Some(1.0)).collect();
2350        make_grid_object(
2351            vec![
2352                (
2353                    "value".to_string(),
2354                    RValue::vec(Vector::Double(vals.into())),
2355                ),
2356                (
2357                    "units".to_string(),
2358                    RValue::vec(Vector::Character(
2359                        (0..nrow)
2360                            .map(|_| Some("null".to_string()))
2361                            .collect::<Vec<_>>()
2362                            .into(),
2363                    )),
2364                ),
2365            ],
2366            &["unit"],
2367        )
2368    } else {
2369        heights
2370    };
2371
2372    let entries = vec![
2373        (
2374            "nrow".to_string(),
2375            RValue::vec(Vector::Integer(vec![Some(nrow)].into())),
2376        ),
2377        (
2378            "ncol".to_string(),
2379            RValue::vec(Vector::Integer(vec![Some(ncol)].into())),
2380        ),
2381        ("widths".to_string(), widths),
2382        ("heights".to_string(), heights),
2383        ("respect".to_string(), respect),
2384    ];
2385
2386    Ok(make_grid_object(entries, &["layout"]))
2387}
2388
2389/// Visualize a grid layout by drawing labeled rectangles for each cell.
2390///
2391/// @param layout a layout object to visualize
2392/// @return NULL (invisibly)
2393#[interpreter_builtin(name = "grid.show.layout", namespace = "grid", min_args = 1)]
2394fn interp_grid_show_layout(
2395    args: &[RValue],
2396    _named: &[(String, RValue)],
2397    context: &BuiltinContext,
2398) -> Result<RValue, RError> {
2399    let layout = &args[0];
2400
2401    // Extract nrow, ncol from the layout object
2402    let (nrow, ncol) = if let RValue::List(list) = layout {
2403        let mut nr = 1i64;
2404        let mut nc = 1i64;
2405        for (key, val) in &list.values {
2406            match key.as_deref() {
2407                Some("nrow") => {
2408                    if let Some(rv) = val.as_vector() {
2409                        nr = rv.as_integer_scalar().unwrap_or(1);
2410                    }
2411                }
2412                Some("ncol") => {
2413                    if let Some(rv) = val.as_vector() {
2414                        nc = rv.as_integer_scalar().unwrap_or(1);
2415                    }
2416                }
2417                _ => {}
2418            }
2419        }
2420        (nr.max(1) as usize, nc.max(1) as usize)
2421    } else {
2422        (1, 1)
2423    };
2424
2425    // Draw a grid of rectangles with labels showing (row, col)
2426    let cell_width = 1.0 / ncol as f64;
2427    let cell_height = 1.0 / nrow as f64;
2428
2429    for row in 0..nrow {
2430        for col in 0..ncol {
2431            let x_center = (col as f64 + 0.5) * cell_width;
2432            let y_center = 1.0 - (row as f64 + 0.5) * cell_height;
2433
2434            // Draw the cell rectangle (Rust-level)
2435            let rect_gp = grid::gpar::Gpar {
2436                col: Some([0, 0, 0, 255]),
2437                fill: Some([255, 255, 255, 0]),
2438                lwd: Some(0.5),
2439                ..Default::default()
2440            };
2441            let rect_grob = grid::grob::Grob::Rect {
2442                x: grid::units::Unit::npc(x_center),
2443                y: grid::units::Unit::npc(y_center),
2444                width: grid::units::Unit::npc(cell_width),
2445                height: grid::units::Unit::npc(cell_height),
2446                just: (
2447                    grid::viewport::Justification::Centre,
2448                    grid::viewport::Justification::Centre,
2449                ),
2450                gp: rect_gp,
2451            };
2452            record_rust_grob(rect_grob, context);
2453
2454            // Draw the cell label
2455            let label = format!("({}, {})", row + 1, col + 1);
2456            let text_gp = grid::gpar::Gpar {
2457                col: Some([100, 100, 100, 255]),
2458                fontsize: Some(8.0),
2459                ..Default::default()
2460            };
2461            let text_grob = grid::grob::Grob::Text {
2462                label: vec![label],
2463                x: grid::units::Unit::npc(x_center),
2464                y: grid::units::Unit::npc(y_center),
2465                just: (
2466                    grid::viewport::Justification::Centre,
2467                    grid::viewport::Justification::Centre,
2468                ),
2469                rot: 0.0,
2470                gp: text_gp,
2471            };
2472            record_rust_grob(text_grob, context);
2473        }
2474    }
2475
2476    context.interpreter().set_invisible();
2477    Ok(RValue::Null)
2478}
2479
2480// endregion
2481
2482// region: Grob manipulation
2483
2484/// Retrieve a grob from the display list by name.
2485///
2486/// @param name character string naming the grob
2487/// @return the grob, or NULL if not found
2488#[interpreter_builtin(name = "grid.get", namespace = "grid", min_args = 1)]
2489fn interp_grid_get(
2490    args: &[RValue],
2491    named: &[(String, RValue)],
2492    context: &BuiltinContext,
2493) -> Result<RValue, RError> {
2494    let ca = CallArgs::new(args, named);
2495    let target_name = ca.string("name", 0)?;
2496
2497    let display_list = context.interpreter().grid_display_list.borrow();
2498    for item in display_list.iter() {
2499        if let RValue::List(list) = item {
2500            for (key, val) in &list.values {
2501                if key.as_deref() == Some("name") {
2502                    if let RValue::Vector(rv) = val {
2503                        if rv.inner.as_character_scalar().as_deref() == Some(target_name.as_str()) {
2504                            return Ok(item.clone());
2505                        }
2506                    }
2507                }
2508            }
2509        }
2510    }
2511
2512    Ok(RValue::Null)
2513}
2514
2515/// Modify properties of a grob on the display list.
2516///
2517/// @param name character string naming the grob
2518/// @param ... named arguments to update on the grob
2519/// @return NULL (invisibly)
2520#[interpreter_builtin(name = "grid.edit", namespace = "grid", min_args = 1)]
2521fn interp_grid_edit(
2522    args: &[RValue],
2523    named: &[(String, RValue)],
2524    context: &BuiltinContext,
2525) -> Result<RValue, RError> {
2526    let ca = CallArgs::new(args, named);
2527    let target_name = ca.string("name", 0)?;
2528
2529    let mut display_list = context.interpreter().grid_display_list.borrow_mut();
2530    for item in display_list.iter_mut() {
2531        if let RValue::List(list) = item {
2532            // Check if this grob has the target name
2533            let is_target = list.values.iter().any(|(key, val)| {
2534                key.as_deref() == Some("name")
2535                    && matches!(val, RValue::Vector(rv) if rv.inner.as_character_scalar().as_deref() == Some(target_name.as_str()))
2536            });
2537
2538            if is_target {
2539                // Update properties from named args (skip "name" since that's the lookup key)
2540                for (key, val) in named {
2541                    if key == "name" {
2542                        continue;
2543                    }
2544                    // Find and replace existing entry, or append
2545                    let mut found = false;
2546                    for (entry_key, entry_val) in list.values.iter_mut() {
2547                        if entry_key.as_deref() == Some(key.as_str()) {
2548                            *entry_val = val.clone();
2549                            found = true;
2550                            break;
2551                        }
2552                    }
2553                    if !found {
2554                        list.values.push((Some(key.clone()), val.clone()));
2555                    }
2556                }
2557                break;
2558            }
2559        }
2560    }
2561
2562    context.interpreter().set_invisible();
2563    Ok(RValue::Null)
2564}
2565
2566/// Remove a grob from the display list by name.
2567///
2568/// @param name character string naming the grob to remove
2569/// @return NULL (invisibly)
2570#[interpreter_builtin(name = "grid.remove", namespace = "grid", min_args = 1)]
2571fn interp_grid_remove(
2572    args: &[RValue],
2573    named: &[(String, RValue)],
2574    context: &BuiltinContext,
2575) -> Result<RValue, RError> {
2576    let ca = CallArgs::new(args, named);
2577    let target_name = ca.string("name", 0)?;
2578
2579    let mut display_list = context.interpreter().grid_display_list.borrow_mut();
2580    display_list.retain(|item| {
2581        if let RValue::List(list) = item {
2582            !list.values.iter().any(|(key, val)| {
2583                key.as_deref() == Some("name")
2584                    && matches!(val, RValue::Vector(rv) if rv.inner.as_character_scalar().as_deref() == Some(target_name.as_str()))
2585            })
2586        } else {
2587            true
2588        }
2589    });
2590
2591    context.interpreter().set_invisible();
2592    Ok(RValue::Null)
2593}
2594
2595// endregion
2596
2597// region: Axes
2598
2599/// Draw an x-axis on the grid graphics device.
2600///
2601/// @param at numeric vector of tick mark positions (in native coordinates)
2602/// @param label character vector of labels, or TRUE for automatic
2603/// @param main logical; if TRUE (default), draw below the viewport
2604/// @param gp graphical parameters
2605/// @param vp viewport to use
2606/// @param name grob name
2607/// @param draw whether to draw immediately (default TRUE)
2608/// @return an xaxis grob (invisibly)
2609#[interpreter_builtin(name = "grid.xaxis", namespace = "grid")]
2610fn interp_grid_xaxis(
2611    args: &[RValue],
2612    named: &[(String, RValue)],
2613    context: &BuiltinContext,
2614) -> Result<RValue, RError> {
2615    let ca = CallArgs::new(args, named);
2616    let draw = ca.logical_flag("draw", 6, true);
2617    let vp = opt_value(&ca, "vp", 4);
2618    let name = ca
2619        .optional_string("name", 5)
2620        .unwrap_or_else(|| auto_grob_name("GRID.xaxis", context));
2621
2622    let at = opt_value(&ca, "at", 0);
2623    let label = opt_value(&ca, "label", 1);
2624    let main = ca.logical_flag("main", 2, true);
2625    let gp = opt_value(&ca, "gp", 3);
2626
2627    let entries = vec![
2628        ("at".to_string(), at),
2629        ("label".to_string(), label),
2630        (
2631            "main".to_string(),
2632            RValue::vec(Vector::Logical(vec![Some(main)].into())),
2633        ),
2634        ("gp".to_string(), gp),
2635        (
2636            "name".to_string(),
2637            RValue::vec(Vector::Character(vec![Some(name)].into())),
2638        ),
2639    ];
2640
2641    make_grob("xaxis", entries, draw, vp, context)
2642}
2643
2644/// Draw a y-axis on the grid graphics device.
2645///
2646/// @param at numeric vector of tick mark positions (in native coordinates)
2647/// @param label character vector of labels, or TRUE for automatic
2648/// @param main logical; if TRUE (default), draw to the left of the viewport
2649/// @param gp graphical parameters
2650/// @param vp viewport to use
2651/// @param name grob name
2652/// @param draw whether to draw immediately (default TRUE)
2653/// @return a yaxis grob (invisibly)
2654#[interpreter_builtin(name = "grid.yaxis", namespace = "grid")]
2655fn interp_grid_yaxis(
2656    args: &[RValue],
2657    named: &[(String, RValue)],
2658    context: &BuiltinContext,
2659) -> Result<RValue, RError> {
2660    let ca = CallArgs::new(args, named);
2661    let draw = ca.logical_flag("draw", 6, true);
2662    let vp = opt_value(&ca, "vp", 4);
2663    let name = ca
2664        .optional_string("name", 5)
2665        .unwrap_or_else(|| auto_grob_name("GRID.yaxis", context));
2666
2667    let at = opt_value(&ca, "at", 0);
2668    let label = opt_value(&ca, "label", 1);
2669    let main = ca.logical_flag("main", 2, true);
2670    let gp = opt_value(&ca, "gp", 3);
2671
2672    let entries = vec![
2673        ("at".to_string(), at),
2674        ("label".to_string(), label),
2675        (
2676            "main".to_string(),
2677            RValue::vec(Vector::Logical(vec![Some(main)].into())),
2678        ),
2679        ("gp".to_string(), gp),
2680        (
2681            "name".to_string(),
2682            RValue::vec(Vector::Character(vec![Some(name)].into())),
2683        ),
2684    ];
2685
2686    make_grob("yaxis", entries, draw, vp, context)
2687}
2688
2689// endregion
2690
2691// region: Grid-to-PlotState rendering
2692
2693use crate::interpreter::graphics::plot_data::{PlotItem, PlotState};
2694
2695/// Device dimensions in centimeters (default ~7 inches square).
2696const DEVICE_WIDTH_CM: f64 = 17.78;
2697const DEVICE_HEIGHT_CM: f64 = 17.78;
2698
2699/// Convert the grid Rust display list into a `PlotState` for the existing
2700/// egui rendering pipeline.
2701///
2702/// This replays the display list through a `PlotStateRenderer` that converts
2703/// grid grobs (in cm coordinates) to `PlotItem`s in normalized [0,1] space.
2704fn grid_to_plot_state(
2705    display_list: &grid::display::DisplayList,
2706    grob_store: &grid::grob::GrobStore,
2707) -> PlotState {
2708    let mut renderer = PlotStateRenderer::new(DEVICE_WIDTH_CM, DEVICE_HEIGHT_CM);
2709    grid::render::replay(display_list, grob_store, &mut renderer);
2710    renderer.into_plot_state()
2711}
2712
2713/// A `GridRenderer` implementation that converts grid drawing operations
2714/// into `PlotItem`s for the existing egui_plot rendering pipeline.
2715///
2716/// Coordinates are mapped from cm to normalized [0, device_size] coordinates,
2717/// then to a PlotState with x_lim and y_lim set to the device extent.
2718struct PlotStateRenderer {
2719    items: Vec<PlotItem>,
2720    device_width_cm: f64,
2721    device_height_cm: f64,
2722}
2723
2724impl PlotStateRenderer {
2725    fn new(width_cm: f64, height_cm: f64) -> Self {
2726        PlotStateRenderer {
2727            items: Vec::new(),
2728            device_width_cm: width_cm,
2729            device_height_cm: height_cm,
2730        }
2731    }
2732
2733    fn into_plot_state(self) -> PlotState {
2734        PlotState {
2735            items: self.items,
2736            title: None,
2737            x_label: None,
2738            y_label: None,
2739            x_lim: Some((0.0, self.device_width_cm)),
2740            y_lim: Some((0.0, self.device_height_cm)),
2741            show_legend: false,
2742        }
2743    }
2744
2745    /// Convert RGBA with optional alpha to final RGBA.
2746    fn apply_alpha(rgba: [u8; 4], gp: &grid::gpar::Gpar) -> [u8; 4] {
2747        let alpha = gp.effective_alpha();
2748        if (alpha - 1.0).abs() < f64::EPSILON {
2749            rgba
2750        } else {
2751            let a = (f64::from(rgba[3]) * alpha) as u8;
2752            [rgba[0], rgba[1], rgba[2], a]
2753        }
2754    }
2755}
2756
2757impl grid::render::GridRenderer for PlotStateRenderer {
2758    fn line(&mut self, x0_cm: f64, y0_cm: f64, x1_cm: f64, y1_cm: f64, gp: &grid::gpar::Gpar) {
2759        let col = Self::apply_alpha(gp.effective_col(), gp);
2760        let width = gp.effective_lwd() as f32;
2761        self.items.push(PlotItem::Line {
2762            x: vec![x0_cm, x1_cm],
2763            y: vec![y0_cm, y1_cm],
2764            color: col,
2765            width,
2766            label: None,
2767        });
2768    }
2769
2770    fn polyline(&mut self, x_cm: &[f64], y_cm: &[f64], gp: &grid::gpar::Gpar) {
2771        let col = Self::apply_alpha(gp.effective_col(), gp);
2772        let width = gp.effective_lwd() as f32;
2773        self.items.push(PlotItem::Line {
2774            x: x_cm.to_vec(),
2775            y: y_cm.to_vec(),
2776            color: col,
2777            width,
2778            label: None,
2779        });
2780    }
2781
2782    fn rect(&mut self, x_cm: f64, y_cm: f64, w_cm: f64, h_cm: f64, gp: &grid::gpar::Gpar) {
2783        let fill = gp.effective_fill();
2784        let col = Self::apply_alpha(gp.effective_col(), gp);
2785        let width = gp.effective_lwd() as f32;
2786
2787        // Draw fill as a polygon if not transparent
2788        if fill[3] > 0 {
2789            // Use a line to represent the rectangle outline (4 corners + close)
2790            let fill_color = Self::apply_alpha(fill, gp);
2791            self.items.push(PlotItem::Line {
2792                x: vec![x_cm, x_cm + w_cm, x_cm + w_cm, x_cm, x_cm],
2793                y: vec![y_cm, y_cm, y_cm + h_cm, y_cm + h_cm, y_cm],
2794                color: fill_color,
2795                width: 0.5,
2796                label: None,
2797            });
2798        }
2799
2800        // Draw outline
2801        if col[3] > 0 {
2802            self.items.push(PlotItem::Line {
2803                x: vec![x_cm, x_cm + w_cm, x_cm + w_cm, x_cm, x_cm],
2804                y: vec![y_cm, y_cm, y_cm + h_cm, y_cm + h_cm, y_cm],
2805                color: col,
2806                width,
2807                label: None,
2808            });
2809        }
2810    }
2811
2812    fn circle(&mut self, x_cm: f64, y_cm: f64, r_cm: f64, gp: &grid::gpar::Gpar) {
2813        let col = Self::apply_alpha(gp.effective_col(), gp);
2814        // Approximate circle with a polygon (24 segments)
2815        let n = 24;
2816        let mut xs = Vec::with_capacity(n + 1);
2817        let mut ys = Vec::with_capacity(n + 1);
2818        for i in 0..=n {
2819            let theta = 2.0 * std::f64::consts::PI * (i as f64 / n as f64);
2820            xs.push(x_cm + r_cm * theta.cos());
2821            ys.push(y_cm + r_cm * theta.sin());
2822        }
2823        self.items.push(PlotItem::Line {
2824            x: xs,
2825            y: ys,
2826            color: col,
2827            width: gp.effective_lwd() as f32,
2828            label: None,
2829        });
2830    }
2831
2832    fn polygon(&mut self, x_cm: &[f64], y_cm: &[f64], gp: &grid::gpar::Gpar) {
2833        let col = Self::apply_alpha(gp.effective_col(), gp);
2834        let mut xs = x_cm.to_vec();
2835        let mut ys = y_cm.to_vec();
2836        // Close the polygon
2837        if let (Some(&first_x), Some(&first_y)) = (x_cm.first(), y_cm.first()) {
2838            xs.push(first_x);
2839            ys.push(first_y);
2840        }
2841        self.items.push(PlotItem::Line {
2842            x: xs,
2843            y: ys,
2844            color: col,
2845            width: gp.effective_lwd() as f32,
2846            label: None,
2847        });
2848    }
2849
2850    fn text(&mut self, x_cm: f64, y_cm: f64, label: &str, _rot: f64, gp: &grid::gpar::Gpar) {
2851        let col = Self::apply_alpha(gp.effective_col(), gp);
2852        self.items.push(PlotItem::Text {
2853            x: x_cm,
2854            y: y_cm,
2855            text: label.to_string(),
2856            color: col,
2857        });
2858    }
2859
2860    fn point(&mut self, x_cm: f64, y_cm: f64, pch: u8, size_cm: f64, gp: &grid::gpar::Gpar) {
2861        let col = Self::apply_alpha(gp.effective_col(), gp);
2862        self.items.push(PlotItem::Points {
2863            x: vec![x_cm],
2864            y: vec![y_cm],
2865            color: col,
2866            size: size_cm as f32 * 4.0, // scale up for visibility
2867            shape: pch,
2868            label: None,
2869        });
2870    }
2871
2872    fn clip(&mut self, _x_cm: f64, _y_cm: f64, _w_cm: f64, _h_cm: f64) {
2873        // Clipping is not supported in the PlotState model; ignore silently
2874    }
2875
2876    fn unclip(&mut self) {
2877        // No-op
2878    }
2879
2880    fn device_size_cm(&self) -> (f64, f64) {
2881        (self.device_width_cm, self.device_height_cm)
2882    }
2883}
2884
2885/// Internal: flush the grid display list to a PlotState and send it to the
2886/// plot channel (if the plot feature is enabled).
2887fn flush_grid_to_plot(ctx: &BuiltinContext) {
2888    let dl = ctx.interpreter().grid_rust_display_list.borrow();
2889    if dl.is_empty() {
2890        return;
2891    }
2892    let store = ctx.interpreter().grid_grob_store.borrow();
2893    let plot_state = grid_to_plot_state(&dl, &store);
2894    drop(dl);
2895    drop(store);
2896
2897    // Store it as the current_plot so existing flush_plot() picks it up
2898    *ctx.interpreter().current_plot.borrow_mut() = Some(plot_state);
2899}
2900
2901/// Public API: flush any accumulated grid graphics to the GUI thread.
2902///
2903/// Called by the REPL loop after each eval to auto-display grid graphics,
2904/// alongside `flush_plot()` for base graphics.
2905pub fn flush_grid(interp: &crate::interpreter::Interpreter) {
2906    let dl = interp.grid_rust_display_list.borrow();
2907    if dl.is_empty() {
2908        return;
2909    }
2910    let store = interp.grid_grob_store.borrow();
2911    let plot_state = grid_to_plot_state(&dl, &store);
2912    drop(dl);
2913    drop(store);
2914
2915    // If there's already a base-graphics plot, don't overwrite it — grid gets
2916    // its own turn only if no base plot is pending.
2917    if interp.current_plot.borrow().is_some() {
2918        return;
2919    }
2920
2921    *interp.current_plot.borrow_mut() = Some(plot_state);
2922    // flush_plot() (called immediately after this) will send it to the GUI
2923}
2924
2925// endregion