BREP_app 0.3.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! The inline-icon catalog: every `assets/glyphs/*.svg` as SVG source, keyed by
//! the character it draws.
//!
//! This is the lookup half of the SVG-icon feature; [`crate::icon_text`] is the
//! drawing half. The table itself is generated by `build.rs` at compile time
//! from the glyph SVGs, so an icon is catalogued by existing, with no list here
//! to fall out of step. See `build.rs` for the two transforms it applies (the
//! `#111` ink sentinel, and the viewBox-derived aspect ratio).
//!
//! Why both a font and an SVG catalog: the font is what egui lays out inside
//! ordinary text, and it stays the fallback for every glyph that is drawn as
//! text (toolbar buttons, `ui.label`s not yet converted). The catalog is what
//! lets the same character be drawn as a real image — which is what makes
//! genuine multi-colour icons possible, since a TrueType glyph can only be one
//! colour. The stacked private-use layers in
//! [`crate::panels::toolbar_button`] exist only to work around that limit.
//!
//! Three icons already take that route — the bug (U+1F41E), the book
//! (U+1F4DA) and the pencil (U+270D). Each was composed from the very layers
//! `toolbar_button` stacks by hand, in the same palette, so they are the same
//! artwork expressed once instead of assembled at paint time. Their glyph SVGs
//! mark the ink layer `brep:font-outline="1"`, so the TTF is byte-for-byte what
//! it was and a text use of those characters still renders exactly as before —
//! the colour is additive, visible only through this catalog.

// The generated `pub static ICONS: &[Icon]`, sorted by codepoint.
include!(concat!(env!("OUT_DIR"), "/icon_catalog.rs"));

/// One catalogued icon: the SVG source for a character, plus what the renderer
/// needs to place and colour it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Icon {
    /// The character this icon draws — what a string must contain to select it.
    pub ch: char,
    /// PostScript glyph name (e.g. `icon_2699`), for debugging and docs.
    pub name: &'static str,
    /// A stable, per-icon `bytes://…svg` URI. egui caches the decoded texture
    /// under it, so it must be constant across frames — and it must end in
    /// `.svg`, which is how `egui_extras`' loader recognises the format.
    pub uri: &'static str,
    /// The SVG source, with `mono` artwork already rewritten to white.
    pub svg: &'static str,
    /// width / height of the viewBox — the proportion the font draws it at.
    pub aspect: f32,
    /// SVG cropped to painted bounds, for standalone artwork in icon tiles.
    pub artwork_svg: &'static str,
    /// Aspect ratio of the painted artwork, excluding canvas padding.
    pub artwork_aspect: f32,
    /// Monochrome artwork that should take the colour of the surrounding text.
    /// `false` means the SVG carries its own colours and must not be tinted.
    pub mono: bool,
}

/// The icon for `ch`, or `None` if the catalog has none — in which case the
/// character stays ordinary text and the font draws it.
pub fn lookup(ch: char) -> Option<&'static Icon> {
    let i = ICONS.binary_search_by_key(&ch, |icon| icon.ch).ok()?;
    Some(&ICONS[i])
}

/// Whether `ch` has an icon. Cheaper to read at a call site than `lookup().is_some()`.
pub fn has(ch: char) -> bool {
    lookup(ch).is_some()
}

/// The catalogued artwork for a label that is exactly one character.
///
/// This is the test every icon-drawing site makes — toolbar buttons, tree rows,
/// palette rows — so they all agree on what is an icon. `None` for a
/// multi-character label (a composite, not an icon) and for an uncatalogued
/// character; both are drawn as ordinary text.
///
/// MONOCHROME artwork is included. It used to be excluded, on the grounds that
/// the icon font already drew it in the live text colour for free — but that
/// exclusion was the last thing keeping a font in the binary. Callers tint a
/// `mono` icon to their text colour (it is white in the catalog, so a multiply
/// lands it exactly) and leave a colour one alone.
pub fn artwork(glyph: &str) -> Option<&'static Icon> {
    let mut chars = glyph.chars();
    let icon = lookup(chars.next()?)?;
    chars.next().is_none().then_some(icon)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tile_artwork_fills_the_requested_medium_and_large_icon_sizes() {
        // The cube's original font canvas is 2384 units tall, but its ink is
        // only 955 units tall. Check painted pixels, not the enclosing widget.
        let cube = lookup('\u{E032}').unwrap();
        let painted_height = |svg: &str, height: u32| {
            let image = egui_extras::image::load_svg_bytes_with_size(
                svg.as_bytes(), egui::load::SizeHint::Height(height), &Default::default(),
            ).unwrap();
            let occupied: Vec<_> = image.pixels.chunks(image.width()).enumerate()
                .filter(|(_, row)| row.iter().any(|pixel| pixel.a() > 127))
                .map(|(y, _)| y).collect();
            occupied.last().unwrap() - occupied.first().unwrap() + 1
        };
        let medium = painted_height(cube.artwork_svg, 36);
        let large = painted_height(cube.artwork_svg, 56);
        assert!(medium >= 34, "medium ink height: {medium}");
        assert!(large >= 54, "large ink height: {large}");
        assert!(large > medium);
        assert!(painted_height(cube.svg, 56) < 28, "fixture must retain font padding");
        for icon in ICONS {
            egui_extras::image::load_svg_bytes_with_size(
                icon.artwork_svg.as_bytes(), egui::load::SizeHint::Height(56), &Default::default(),
            ).unwrap_or_else(|error| panic!("{}: {error}", icon.name));
        }
    }

    /// The colour icons OUTSIDE the feature block — each composed by hand from
    /// the layered private-use artwork the toolbar used to stack. Used by the
    /// tests below from both directions, so adding a colour icon without saying
    /// so here fails loudly rather than silently widening what is allowed.
    /// [`is_colour`] is what the tests actually ask; the feature block is a
    /// range rather than 42 more lines.
    const COLOUR: &[char] = &[
        '\u{270D}',  // pencil    — freehand sketch
        '\u{1F41E}', // bug       — Submit Bug
        '\u{1F4DA}', // book      — step.parts library
        '\u{E010}',  // document  — New
        '\u{E011}',  // folder    — Open
        '\u{E012}',  // disk      — Save
        '\u{E013}',  // disk+     — Save As
        '\u{E014}',  // in-tray   — Import
        '\u{E015}',  // out-tray  — Export
        '\u{E016}',  // arrow     — Undo
        '\u{E017}',  // arrow     — Redo
        '\u{E018}',  // camera    — Projection
    ];

    /// Every kernel feature, U+E030-E059, is colour artwork — see
    /// [`FEATURE_PALETTE`]. They are a range rather than 42 entries in
    /// [`COLOUR`] because the rule is the block, not the individual icon.
    const FEATURES: std::ops::RangeInclusive<char> = '\u{E030}'..='\u{E059}';

    /// Whether an icon is expected to carry its own colours.
    fn is_colour(ch: char) -> bool {
        COLOUR.contains(&ch) || FEATURES.contains(&ch)
    }

    /// Every `fill="…"` value in an SVG. `build.rs` has the same three lines and
    /// decides `mono` with them; this is the test side of the same question.
    fn fills(svg: &str) -> impl Iterator<Item = &str> {
        svg.match_indices("fill=\"").filter_map(|(i, m)| {
            let rest = &svg[i + m.len()..];
            rest.find('"').map(|end| &rest[..end])
        })
    }

    #[test]
    fn catalog_is_populated_and_sorted() {
        // A floor, not the exact count: it catches `build.rs` emitting nothing
        // or a half-scanned directory, without breaking every time a glyph is
        // added or (as the colour conversions do) several are merged away.
        assert!(ICONS.len() >= 50, "catalog looks truncated: {}", ICONS.len());
        assert!(
            ICONS.windows(2).all(|w| w[0].ch < w[1].ch),
            "ICONS must be sorted by codepoint for `lookup` to binary-search it"
        );
    }

    #[test]
    fn lookup_finds_every_entry() {
        for icon in ICONS {
            assert_eq!(lookup(icon.ch), Some(icon), "lookup missed {}", icon.name);
        }
    }

    #[test]
    fn notdef_is_not_catalogued() {
        // `.notdef` has no codepoint, so nothing in a string can select it.
        assert!(ICONS.iter().all(|i| i.name != ".notdef"));
    }

    #[test]
    fn uncatalogued_characters_stay_text() {
        for ch in ['a', ' ', '1', '\n', 'é'] {
            assert!(!has(ch), "{ch:?} must not be an icon");
        }
    }

    /// The ink sentinel, both ways round: a monochrome glyph must have been
    /// rewritten to white (so the widget's tint lands exactly), and a glyph with
    /// authored colour must have been left completely alone (tinting it would
    /// wash it out).
    #[test]
    fn the_ink_sentinel_rewrites_mono_glyphs_and_spares_coloured_ones() {
        for icon in ICONS {
            if icon.mono {
                assert!(!icon.svg.contains("#111"), "{}: ink sentinel not rewritten", icon.name);
                assert!(icon.svg.contains("#fff"), "{}: no white fill after rewrite", icon.name);
            } else {
                assert!(
                    icon.svg.contains("fill=\""),
                    "{}: a non-mono glyph must carry its own fills",
                    icon.name
                );
            }
        }
    }

    /// The colour lane is live, not just possible. Each of these was composed
    /// from the layered private-use artwork the toolbar used to paint by hand,
    /// so it carries that exact palette; a regression that flattened one back to
    /// ink would otherwise pass every other test.
    #[test]
    fn the_composed_colour_icons_are_catalogued_in_colour() {
        for (ch, fills) in [
            ('\u{1F41E}', &["#e1544a", "#5e1a14"][..]), // bug        — Submit Bug
            ('\u{1F4DA}', &["#2e9e8f", "#124840"][..]), // book       — step.parts
            ('\u{270D}', &["#f2b33d", "#6e4a12"][..]),  // pencil     — freehand
            ('\u{E010}', &["#70b7f6", "#1c5384"][..]),  // document   — New
            ('\u{E011}', &["#cd8b19", "#fac437"][..]),  // folder     — Open
            ('\u{E012}', &["#4597e5", "#13416c"][..]),  // disk       — Save
            ('\u{E014}', &["#2fa469", "#7be8ae"][..]),  // in-tray    — Import
            ('\u{E015}', &["#da762a", "#ffbb69"][..]),  // out-tray   — Export
            ('\u{E018}', &["#40abda", "#d3f5ff"][..]),  // camera     — Projection
        ] {
            let icon = lookup(ch).unwrap_or_else(|| panic!("U+{:04X} is not catalogued", ch as u32));
            assert!(!icon.mono, "{} must not be tinted — it has authored colour", icon.name);
            for fill in fills {
                assert!(icon.svg.contains(fill), "{}: lost its {fill} layer", icon.name);
            }
        }
    }

    /// The COLOUR list is exactly right, both ways round: every icon on it is
    /// colour, and no icon off it is. That is what catches a stray fill edit
    /// making an icon un-tintable, and equally a conversion that forgot to be
    /// declared here.
    #[test]
    fn the_colour_set_is_exactly_the_declared_one() {
        for ch in COLOUR.iter().copied().chain(FEATURES) {
            let icon = lookup(ch).unwrap_or_else(|| panic!("U+{:04X} missing", ch as u32));
            assert!(!icon.mono, "{} is declared colour but catalogued as ink", icon.name);
        }
        for icon in ICONS.iter().filter(|i| !is_colour(i.ch)) {
            assert!(icon.mono, "{} carries colour but is not declared colour", icon.name);
        }
    }

    /// Every feature the kernel catalogues must have real ARTWORK here, not just
    /// a codepoint mapping.
    ///
    /// `brep_render` already tests that `feature_icon` returns a glyph for every
    /// catalogue entry — and that test passed for a long time while NOTHING was
    /// drawn: all 42 feature codepoints (U+E030-E059) mapped to a private-use
    /// slot that had no glyph behind it, so every feature in the palette and the
    /// history tree rendered as tofu. A mapping test cannot catch that; only
    /// checking the catalog can.
    /// The shared palette the 42 feature icons are drawn from, documented in
    /// `BREP_app/assets/glyphs/README.md`. Hue says what a thing IS — gold solid
    /// material, blue reference and placement, green a curve or added material,
    /// steel sheet metal, violet a second instance, red material removed.
    ///
    /// `_HI`/`_LO` tones are bodies that sit UNDER ink and pair with the deep
    /// `_INK`; `_MID` is ink drawn on NO body, dark enough to read on a light
    /// panel and light enough on a dark one, which a deep tone is not.
    const FEATURE_PALETTE: &[&str] = &[
        "#ffd977", "#f0b840", "#d4901f", "#c9871a", "#7a4d10", // gold   — solid material
        "#a8d4f7", "#3d86c4", "#1c5384", // blue   — reference & placement
        "#a5e8c4", "#2fa469", "#125236", // green  — curves & added material
        "#bcd7e8", "#5c86a6", "#2b4c63", // steel  — sheet metal
        "#c9b6f0", "#8b6fd4", "#3d2a6b", // violet — a second instance
        "#d94438",                       // red    — material removed
    ];

    /// The 42 feature icons are ONE set, not 42 drawings: every fill in every one
    /// of them comes from [`FEATURE_PALETTE`].
    ///
    /// This is what keeps them consistent as they are edited. A glyph hand-tuned
    /// in an SVG editor picks up whatever colour the editor's swatch had, and
    /// nothing else here would notice — `mono` only asks whether a fill is the
    /// `#111` sentinel, so any off-system colour passes every other test in this
    /// file while quietly breaking the set.
    #[test]
    fn every_feature_icon_is_colour_from_the_shared_palette() {
        for ch in FEATURES {
            let icon =
                lookup(ch).unwrap_or_else(|| panic!("U+{:04X} is not catalogued", ch as u32));
            assert!(!icon.mono, "{}: feature artwork must carry its own colours", icon.name);
            for fill in fills(icon.svg) {
                assert!(
                    FEATURE_PALETTE.contains(&fill),
                    "{}: {fill} is not in the feature palette",
                    icon.name
                );
            }
        }
    }

    #[test]
    fn every_feature_type_has_catalogued_artwork() {
        let catalogue = brep_render::features::feature_catalogue();
        let features = catalogue["features"].as_array().expect("catalogue features");
        assert!(!features.is_empty(), "empty feature catalogue");

        let missing: Vec<String> = features
            .iter()
            .filter_map(|feature| {
                let ty = feature["type"].as_str()?;
                match brep_render::features::feature_icon(ty) {
                    Some(ch) if has(ch) => None,
                    Some(ch) => Some(format!("{ty}: U+{:04X} has no glyph", ch as u32)),
                    None => Some(format!("{ty}: no icon mapping at all")),
                }
            })
            .collect();

        assert!(missing.is_empty(), "features without artwork: {missing:#?}");
    }

    #[test]
    fn aspects_are_sane() {
        for icon in ICONS {
            assert!(
                icon.aspect > 0.05 && icon.aspect < 5.0,
                "{}: implausible aspect {}",
                icon.name,
                icon.aspect
            );
        }
    }

    #[test]
    fn uris_are_unique_and_svg_suffixed() {
        let mut seen = std::collections::HashSet::new();
        for icon in ICONS {
            assert!(icon.uri.ends_with(".svg"), "{}: loader needs a .svg URI", icon.name);
            assert!(seen.insert(icon.uri), "{}: duplicate URI {}", icon.name, icon.uri);
        }
    }
}