Skip to main content

ai_usagebar/tray/
strip.rs

1//! Menu-bar strip: starred metrics, OpenUsage bar geometry, and a template
2//! RGBA glyph. Pure JSON in, pixels out — no AppKit, no HWND.
3
4use std::collections::BTreeMap;
5
6use serde_json::Value;
7
8/// At most two starred metrics per provider, matching OpenUsage.
9pub const MAX_STARS_PER_PROVIDER: usize = 2;
10/// Compact Bars glyph shows at most four bounded metrics.
11pub const MAX_BARS: usize = 4;
12/// Logical size of the Bars glyph, matching NSStatusItem's 18 pt slot.
13pub const BARS_POINT_SIDE: u32 = 18;
14/// Pixel size of the Bars glyph at 2× (18 pt). Used by hosts that can only
15/// ship one raster; macOS paints 1×/2×/3× from [`BARS_POINT_SIDE`].
16pub const BARS_PIXEL_SIDE: u32 = BARS_POINT_SIDE * 2;
17/// Backing scales packed into the macOS template image so a 1× external
18/// and a 2×/3× Retina each get a native raster instead of a downsample.
19#[cfg_attr(not(test), allow(dead_code))]
20pub const BARS_SCALES: [u32; 3] = [1, 2, 3];
21
22/// How the status item renders starred metrics.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StripStyle {
25    Text,
26    Bars,
27}
28
29impl StripStyle {
30    pub fn parse(name: &str) -> Self {
31        if name.eq_ignore_ascii_case("text") {
32            Self::Text
33        } else {
34            Self::Bars
35        }
36    }
37
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Self::Text => "text",
41            Self::Bars => "bars",
42        }
43    }
44}
45
46/// Stars the popover persists: provider id → metric keys, in star order.
47pub type Stars = BTreeMap<String, Vec<String>>;
48
49/// One resolved starred metric, ready to draw.
50#[derive(Debug, Clone, PartialEq)]
51pub struct StripMetric {
52    pub provider_id: String,
53    pub provider_name: String,
54    pub key: String,
55    pub label: String,
56    pub value: String,
57    /// 0..=1 fill for bounded metrics (used fraction).
58    pub fraction: f64,
59    pub bounded: bool,
60}
61
62/// Resolved strip contents. `groups` drives Text; `bars` drives Bars.
63#[derive(Debug, Clone, PartialEq)]
64pub struct StripContent {
65    pub groups: Vec<(String, String, Vec<StripMetric>)>,
66    pub bars: Vec<StripMetric>,
67}
68
69impl StripContent {
70    pub fn is_empty(&self) -> bool {
71        self.groups.is_empty()
72    }
73
74    /// One-line Text fallback when we cannot paint stacked type (Windows
75    /// NotifyIcon, or a host that only has `set_title`).
76    pub fn title_line(&self) -> String {
77        let mut parts = Vec::new();
78        for (_, name, metrics) in &self.groups {
79            let values: Vec<&str> = metrics.iter().map(|m| m.value.as_str()).collect();
80            if values.is_empty() {
81                continue;
82            }
83            parts.push(format!("{name} {}", values.join(" ")));
84        }
85        parts.join("   ")
86    }
87}
88
89/// Fill geometry for one bar of the compact glyph. A 1:1 port of OpenUsage
90/// `MenuBarBarGeometry`.
91#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct BarFill {
93    pub fill_w: f64,
94    pub remainder_w: f64,
95    pub divider_x: Option<f64>,
96}
97
98/// Quantize near-full (0.7–1.0) bars by remainder in 15% steps, so a
99/// nearly-full bar still leaves a visible tail instead of reading as 100%.
100pub fn visual_fraction(fraction: f64) -> f64 {
101    if !fraction.is_finite() {
102        return 0.0;
103    }
104    let clamped = fraction.clamp(0.0, 1.0);
105    if clamped > 0.7 && clamped < 1.0 {
106        let remainder = 1.0 - clamped;
107        let quantized = ((remainder / 0.15).ceil() * 0.15).min(1.0);
108        (1.0 - quantized).max(0.0)
109    } else {
110        clamped
111    }
112}
113
114pub fn bar_fill(track_w: f64, fraction: f64) -> BarFill {
115    if !fraction.is_finite() || fraction <= 0.0 {
116        return BarFill {
117            fill_w: 0.0,
118            remainder_w: 0.0,
119            divider_x: None,
120        };
121    }
122    let visual = visual_fraction(fraction);
123    if visual >= 1.0 {
124        return BarFill {
125            fill_w: track_w,
126            remainder_w: 0.0,
127            divider_x: None,
128        };
129    }
130    let min_visible = (track_w * 0.2).round().max(4.0);
131    let max_fill_w = (track_w - min_visible).max(1.0);
132    let fill_w = (track_w * visual).round().clamp(1.0, max_fill_w);
133    let true_remainder = track_w - fill_w;
134    let remainder_w = true_remainder.max(min_visible).min(track_w - 1.0);
135    BarFill {
136        fill_w,
137        remainder_w,
138        divider_x: Some(track_w - remainder_w),
139    }
140}
141
142/// Parse `{style, stars, order}` from the popover's `strip` IPC.
143/// Icon style is locked to Bars; `style` is ignored if present.
144pub fn parse_strip_ipc(value: &Value) -> (StripStyle, Stars, Vec<String>) {
145    let style = StripStyle::Bars;
146    let mut stars = Stars::new();
147    if let Some(map) = value.get("stars").and_then(Value::as_object) {
148        for (id, keys) in map {
149            let id = id.trim();
150            if id.is_empty() {
151                continue;
152            }
153            let mut list = Vec::new();
154            if let Some(arr) = keys.as_array() {
155                for key in arr {
156                    let Some(key) = key.as_str() else { continue };
157                    let key = key.trim();
158                    if key.is_empty() || list.iter().any(|k| k == key) {
159                        continue;
160                    }
161                    list.push(key.to_string());
162                    if list.len() == MAX_STARS_PER_PROVIDER {
163                        break;
164                    }
165                }
166            }
167            if !list.is_empty() {
168                stars.insert(id.to_string(), list);
169            }
170        }
171    }
172    let mut order = Vec::new();
173    if let Some(arr) = value.get("order").and_then(Value::as_array) {
174        for id in arr {
175            let Some(id) = id.as_str() else { continue };
176            let id = id.trim();
177            if id.is_empty() || order.iter().any(|existing| existing == id) {
178                continue;
179            }
180            order.push(id.to_string());
181        }
182    }
183    (style, stars, order)
184}
185
186/// Resolve starred metrics from a host payload. `order` is the popover's
187/// visible card order; empty means payload order. Missing stars fall back to
188/// the first two bounded metrics of each ready entry so the glyph has
189/// something to draw before the WebView sends its layout.
190pub fn content_from_payload(payload: &Value, stars: &Stars, order: &[String]) -> StripContent {
191    let Some(entries) = payload.get("entries").and_then(Value::as_array) else {
192        return StripContent {
193            groups: Vec::new(),
194            bars: Vec::new(),
195        };
196    };
197    let mut by_id = std::collections::HashMap::new();
198    let mut payload_ids = Vec::new();
199    for entry in entries {
200        let Some(id) = entry.get("id").and_then(Value::as_str) else {
201            continue;
202        };
203        payload_ids.push(id.to_string());
204        by_id.insert(id.to_string(), entry);
205    }
206    let mut walk = Vec::new();
207    let mut seen = std::collections::HashSet::new();
208    for id in order {
209        if by_id.contains_key(id) && seen.insert(id.clone()) {
210            walk.push(id.clone());
211        }
212    }
213    for id in payload_ids {
214        if seen.insert(id.clone()) {
215            walk.push(id);
216        }
217    }
218    let mut groups = Vec::new();
219    for id in walk {
220        let Some(entry) = by_id.get(&id) else {
221            continue;
222        };
223        if entry.get("status").and_then(Value::as_str) == Some("error") {
224            continue;
225        }
226        let name = entry
227            .get("display_name")
228            .or_else(|| entry.get("name"))
229            .and_then(Value::as_str)
230            .unwrap_or(id.as_str())
231            .to_string();
232        let metrics = metrics_for_entry(entry, &id, &name);
233        let wanted = if stars.is_empty() {
234            metrics
235                .iter()
236                .filter(|m| m.bounded)
237                .take(MAX_STARS_PER_PROVIDER)
238                .map(|m| m.key.clone())
239                .collect()
240        } else {
241            stars.get(&id).cloned().unwrap_or_default()
242        };
243        if wanted.is_empty() {
244            continue;
245        }
246        let mut picked = Vec::new();
247        for key in wanted {
248            if let Some(metric) = metrics.iter().find(|m| m.key == key) {
249                picked.push(metric.clone());
250            }
251        }
252        if picked.is_empty() {
253            continue;
254        }
255        groups.push((id, name, picked));
256    }
257    let bars: Vec<StripMetric> = groups
258        .iter()
259        .flat_map(|(_, _, metrics)| metrics.iter().cloned())
260        .filter(|m| m.bounded)
261        .take(MAX_BARS)
262        .collect();
263    StripContent { groups, bars }
264}
265
266fn metrics_for_entry(entry: &Value, id: &str, name: &str) -> Vec<StripMetric> {
267    let Some(sections) = entry.get("sections").and_then(Value::as_array) else {
268        return Vec::new();
269    };
270    let mut group = String::new();
271    let mut rows = Vec::new();
272    let mut seen: BTreeMap<String, u32> = BTreeMap::new();
273    for section in sections {
274        let kind = section.get("type").and_then(Value::as_str).unwrap_or("");
275        if kind == "text" {
276            let label = section.get("label").and_then(Value::as_str).unwrap_or("");
277            let value = section.get("value").and_then(Value::as_str).unwrap_or("");
278            if !label.is_empty() && value.is_empty() {
279                group = label.to_string();
280            }
281            continue;
282        }
283        if kind != "metric" {
284            continue;
285        }
286        let raw_label = section.get("label").and_then(Value::as_str).unwrap_or("");
287        let label = metric_label(id, raw_label);
288        let label = if group.is_empty() {
289            label
290        } else {
291            format!("{label} ({group})")
292        };
293        let percent = section
294            .get("percent")
295            .and_then(Value::as_f64)
296            .unwrap_or(0.0);
297        let value = section
298            .get("value")
299            .and_then(Value::as_str)
300            .map(str::to_string)
301            .unwrap_or_else(|| format!("{}%", percent.round() as i64));
302        let mut key = format!("metric:{label}");
303        let count = seen.entry(key.clone()).or_insert(0);
304        *count += 1;
305        if *count > 1 {
306            key = format!("{key} #{count}");
307        }
308        rows.push(StripMetric {
309            provider_id: id.to_string(),
310            provider_name: name.to_string(),
311            key,
312            label,
313            value,
314            fraction: (percent / 100.0).clamp(0.0, 1.0),
315            bounded: true,
316        });
317    }
318    rows
319}
320
321fn metric_label(entry_id: &str, label: &str) -> String {
322    let slug = entry_id
323        .split('@')
324        .next()
325        .unwrap_or(entry_id)
326        .to_ascii_lowercase();
327    if slug == "supergrok" {
328        let trimmed = regex_strip_build_credits(label);
329        return trimmed;
330    }
331    label.to_string()
332}
333
334fn regex_strip_build_credits(label: &str) -> String {
335    const SUFFIX: &str = " build credits";
336    let lower = label.to_ascii_lowercase();
337    if let Some(idx) = lower.rfind(SUFFIX)
338        && idx + SUFFIX.len() == lower.len()
339    {
340        return label[..idx].to_string();
341    }
342    label.to_string()
343}
344
345/// Layout of the compact Bars glyph in the given square, OpenUsage `MenuBarBars`.
346#[derive(Debug, Clone, Copy)]
347pub struct BarsLayout {
348    pub n: usize,
349    pub track_x: f64,
350    pub track_w: f64,
351    pub track_h: f64,
352    pub rx: f64,
353    pub y_offset: f64,
354    pub gap: f64,
355}
356
357pub fn bars_layout(count: usize, side: f64) -> BarsLayout {
358    let n = count.clamp(1, MAX_BARS);
359    let pad = (side * 0.08).round().max(1.0);
360    let gap = (side * 0.03).round().max(1.0);
361    let track_x = pad;
362    let track_w = side - 2.0 * pad;
363    let layout_n = n.max(2) as f64;
364    let track_h = ((side - 2.0 * pad - (layout_n - 1.0) * gap) / layout_n)
365        .floor()
366        .max(1.0);
367    let rx = (track_h / 3.0).floor().max(1.0);
368    let total_h = n as f64 * track_h + (n as f64 - 1.0) * gap;
369    let y_offset = pad + ((side - 2.0 * pad - total_h) / 2.0).floor();
370    BarsLayout {
371        n,
372        track_x,
373        track_w,
374        track_h,
375        rx,
376        y_offset,
377        gap,
378    }
379}
380
381/// Template RGBA (black ink, alpha as coverage) for the compact Bars glyph.
382/// Empty fractions yield a fully transparent square.
383pub fn bars_rgba(fractions: &[f64], side: u32) -> Vec<u8> {
384    let side = side.max(8);
385    let mut buf = vec![0u8; side as usize * side as usize * 4];
386    if fractions.is_empty() {
387        return buf;
388    }
389    let size = f64::from(side);
390    let layout = bars_layout(fractions.len(), size);
391    let n = layout.n;
392
393    for (i, fraction) in fractions.iter().copied().take(n).enumerate() {
394        let y = layout.y_offset + i as f64 * (layout.track_h + layout.gap) + 1.0;
395        stamp_round_rect(
396            &mut buf,
397            side,
398            RoundBar {
399                x: layout.track_x,
400                y,
401                w: layout.track_w,
402                h: layout.track_h,
403                r_left: layout.rx,
404                r_right: layout.rx,
405            },
406            41, // 0.16 * 255
407        );
408        let fill = bar_fill(layout.track_w, fraction);
409        if fill.fill_w > 0.0 {
410            let trailing = if fill.fill_w >= layout.track_w {
411                layout.rx
412            } else {
413                (layout.rx * 0.35).floor().max(0.0)
414            };
415            stamp_round_rect(
416                &mut buf,
417                side,
418                RoundBar {
419                    x: layout.track_x,
420                    y,
421                    w: fill.fill_w,
422                    h: layout.track_h,
423                    r_left: layout.rx,
424                    r_right: trailing,
425                },
426                255,
427            );
428        }
429        if fill.fill_w > 0.0
430            && fill.remainder_w > 0.0
431            && let Some(divider_x) = fill.divider_x
432        {
433            stamp_round_rect(
434                &mut buf,
435                side,
436                RoundBar {
437                    x: layout.track_x + divider_x,
438                    y,
439                    w: fill.remainder_w,
440                    h: layout.track_h,
441                    r_left: (layout.rx * 0.2).floor().max(0.0),
442                    r_right: layout.rx,
443                },
444                61, // 0.24 * 255
445            );
446        }
447    }
448    buf
449}
450
451struct RoundBar {
452    x: f64,
453    y: f64,
454    w: f64,
455    h: f64,
456    r_left: f64,
457    r_right: f64,
458}
459
460fn stamp_round_rect(buf: &mut [u8], side: u32, bar: RoundBar, alpha: u8) {
461    if bar.w <= 0.0 || bar.h <= 0.0 {
462        return;
463    }
464    let side_i = side as i32;
465    let min_x = bar.x.floor().max(0.0) as i32;
466    let min_y = bar.y.floor().max(0.0) as i32;
467    let max_x = (bar.x + bar.w).ceil().min(f64::from(side)) as i32;
468    let max_y = (bar.y + bar.h).ceil().min(f64::from(side)) as i32;
469    let target = f64::from(alpha);
470    for py in min_y..max_y {
471        for px in min_x..max_x {
472            if px < 0 || py < 0 || px >= side_i || py >= side_i {
473                continue;
474            }
475            let cover = sample_round_rect(f64::from(px) + 0.5, f64::from(py) + 0.5, &bar);
476            if cover <= 0.0 {
477                continue;
478            }
479            let idx = ((py as u32 * side + px as u32) * 4) as usize;
480            let stamped = (target * cover).round().clamp(0.0, 255.0) as u8;
481            if stamped > buf[idx + 3] {
482                buf[idx] = 0;
483                buf[idx + 1] = 0;
484                buf[idx + 2] = 0;
485                buf[idx + 3] = stamped;
486            }
487        }
488    }
489}
490
491fn sample_round_rect(px: f64, py: f64, bar: &RoundBar) -> f64 {
492    (0.5 - sd_round_box(px, py, bar)).clamp(0.0, 1.0)
493}
494
495fn sd_round_box(px: f64, py: f64, bar: &RoundBar) -> f64 {
496    let r = if px < bar.x + bar.w * 0.5 {
497        bar.r_left
498    } else {
499        bar.r_right
500    };
501    let r = r.max(0.0).min(bar.h * 0.5).min(bar.w * 0.5);
502    let cx = bar.x + bar.w * 0.5;
503    let cy = bar.y + bar.h * 0.5;
504    let dx = (px - cx).abs() - (bar.w * 0.5 - r);
505    let dy = (py - cy).abs() - (bar.h * 0.5 - r);
506    let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
507    outside + dx.min(0.0).max(dy.min(0.0)) - r
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use serde_json::json;
514
515    #[test]
516    fn zero_or_negative_fraction_draws_nothing() {
517        assert_eq!(bar_fill(100.0, 0.0).fill_w, 0.0);
518        assert_eq!(bar_fill(100.0, -0.5).fill_w, 0.0);
519    }
520
521    #[test]
522    fn full_fraction_fills_track_with_no_remainder() {
523        let fill = bar_fill(100.0, 1.0);
524        assert_eq!(fill.fill_w, 100.0);
525        assert_eq!(fill.remainder_w, 0.0);
526        assert_eq!(fill.divider_x, None);
527    }
528
529    #[test]
530    fn near_full_keeps_a_visible_tail() {
531        let fill = bar_fill(100.0, 0.97);
532        assert!(fill.fill_w < 100.0);
533        assert!(fill.remainder_w >= 20.0);
534        assert_eq!(fill.divider_x, Some(fill.fill_w));
535    }
536
537    #[test]
538    fn visual_fraction_quantizes_near_full_in_fifteen_percent_steps() {
539        assert!((visual_fraction(0.97) - 0.85).abs() < 0.0001);
540        assert_eq!(visual_fraction(1.0), 1.0);
541        assert_eq!(visual_fraction(0.0), 0.0);
542        assert!((visual_fraction(0.5) - 0.5).abs() < 0.0001);
543    }
544
545    fn sample_payload() -> Value {
546        json!({
547            "entries": [
548                {
549                    "id": "anthropic",
550                    "display_name": "Claude",
551                    "status": "ready",
552                    "sections": [
553                        {"type": "metric", "label": "Weekly", "percent": 19, "value": "19%"},
554                        {"type": "metric", "label": "Session", "percent": 41, "value": "41%"}
555                    ]
556                },
557                {
558                    "id": "openai",
559                    "display_name": "Codex",
560                    "status": "ready",
561                    "sections": [
562                        {"type": "metric", "label": "Codex weekly", "percent": 2, "value": "2%"}
563                    ]
564                },
565                {
566                    "id": "cursor",
567                    "display_name": "Cursor",
568                    "status": "error",
569                    "sections": [
570                        {"type": "metric", "label": "Cursor Models", "percent": 90, "value": "90%"}
571                    ]
572                }
573            ]
574        })
575    }
576
577    #[test]
578    fn empty_stars_fall_back_to_first_two_bounded_metrics() {
579        let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
580        assert_eq!(content.groups.len(), 2);
581        assert_eq!(content.groups[0].2.len(), 2);
582        assert_eq!(content.groups[0].2[0].key, "metric:Weekly");
583        assert_eq!(content.groups[0].2[1].key, "metric:Session");
584        assert_eq!(content.groups[1].2[0].key, "metric:Codex weekly");
585        assert_eq!(content.bars.len(), 3);
586        assert!(!content.title_line().is_empty());
587    }
588
589    #[test]
590    fn stars_select_named_metrics_and_skip_errors() {
591        let mut stars = Stars::new();
592        stars.insert("anthropic".into(), vec!["metric:Session".into()]);
593        stars.insert("cursor".into(), vec!["metric:Cursor Models".into()]);
594        let content = content_from_payload(&sample_payload(), &stars, &[]);
595        assert_eq!(content.groups.len(), 1);
596        assert_eq!(content.groups[0].2[0].key, "metric:Session");
597        assert_eq!(content.groups[0].2[0].fraction, 0.41);
598        assert_eq!(content.bars.len(), 1);
599    }
600
601    #[test]
602    fn grouped_metric_keys_match_the_popover() {
603        let payload = json!({
604            "entries": [{
605                "id": "antigravity",
606                "display_name": "Antigravity",
607                "status": "ready",
608                "sections": [
609                    {"type": "text", "label": "Session", "value": ""},
610                    {"type": "metric", "label": "Gemini", "percent": 4, "value": "4%"},
611                    {"type": "text", "label": "Weekly", "value": ""},
612                    {"type": "metric", "label": "Gemini", "percent": 11, "value": "11%"}
613                ]
614            }]
615        });
616        let content = content_from_payload(&payload, &Stars::new(), &[]);
617        assert_eq!(content.groups[0].2[0].key, "metric:Gemini (Session)");
618        assert_eq!(content.groups[0].2[1].key, "metric:Gemini (Weekly)");
619    }
620
621    #[test]
622    fn parse_strip_ipc_caps_stars_at_two_per_provider() {
623        let value = json!({
624            "style": "text",
625            "stars": {
626                "anthropic": ["metric:Weekly", "metric:Session", "metric:Extra"],
627                "": ["x"],
628                "openai": ["metric:Codex weekly"]
629            }
630        });
631        let (style, stars, order) = parse_strip_ipc(&value);
632        assert_eq!(style, StripStyle::Bars);
633        assert_eq!(stars["anthropic"].len(), 2);
634        assert_eq!(stars["openai"], vec!["metric:Codex weekly".to_string()]);
635        assert!(!stars.contains_key(""));
636        assert!(order.is_empty());
637    }
638
639    #[test]
640    fn strip_follows_popover_card_order() {
641        let mut stars = Stars::new();
642        stars.insert("anthropic".into(), vec!["metric:Weekly".into()]);
643        stars.insert("openai".into(), vec!["metric:Codex weekly".into()]);
644        let content = content_from_payload(
645            &sample_payload(),
646            &stars,
647            &["openai".into(), "anthropic".into()],
648        );
649        assert_eq!(content.groups[0].0, "openai");
650        assert_eq!(content.groups[1].0, "anthropic");
651        assert_eq!(content.bars[0].provider_id, "openai");
652        assert_eq!(content.bars[1].provider_id, "anthropic");
653    }
654
655    #[test]
656    fn bars_rgba_is_square_black_and_anti_aliased() {
657        let bytes = bars_rgba(&[0.4, 0.97], BARS_PIXEL_SIDE);
658        assert_eq!(
659            bytes.len(),
660            (BARS_PIXEL_SIDE * BARS_PIXEL_SIDE * 4) as usize
661        );
662        assert_eq!(bytes[3], 0, "top-left stays transparent");
663        let last = bytes.len() - 1;
664        assert_eq!(bytes[last], 0, "bottom-right stays transparent");
665        let mut inked = 0usize;
666        let mut soft = 0usize;
667        for pixel in bytes.as_chunks::<4>().0 {
668            if pixel[3] > 0 {
669                inked += 1;
670                assert_eq!(&pixel[..3], &[0, 0, 0]);
671            }
672            if pixel[3] > 0 && pixel[3] < 255 {
673                soft += 1;
674            }
675        }
676        assert!(inked > 40, "glyph too sparse: {inked}");
677        assert!(soft > 0, "expected anti-aliased edges");
678    }
679
680    #[test]
681    fn empty_fractions_are_fully_transparent() {
682        let bytes = bars_rgba(&[], BARS_PIXEL_SIDE);
683        assert!(bytes.as_chunks::<4>().0.iter().all(|p| p[3] == 0));
684    }
685
686    #[test]
687    fn bars_rgba_at_each_status_item_scale() {
688        for scale in BARS_SCALES {
689            let side = BARS_POINT_SIDE * scale;
690            let bytes = bars_rgba(&[0.4, 0.97], side);
691            assert_eq!(bytes.len(), (side * side * 4) as usize, "scale {scale}");
692            assert!(
693                bytes.as_chunks::<4>().0.iter().any(|p| p[3] > 0),
694                "scale {scale} produced an empty glyph"
695            );
696        }
697    }
698}