Skip to main content

ling_ui/
holo.rs

1//! Holographic vector UI primitives — a stroke font, sci-fi frame geometry, and
2//! immediate-mode hit-testing. Everything is returned as line segments
3//! `[x0,y0,x1,y1]` in a local box so a renderer can scale, glow and depth-shift
4//! them however it likes.
5
6/// Point-in-rect test (immediate-mode hit testing).
7pub fn hit_rect(px: f32, py: f32, x: f32, y: f32, w: f32, h: f32) -> bool {
8    px >= x && px <= x + w && py >= y && py <= y + h
9}
10
11// ── Stroke font ─────────────────────────────────────────────────────────────
12// Glyphs are line segments in a unit em-box: x∈[0,1] right, y∈[0,1] down.
13
14const L: f32 = 0.12; const M: f32 = 0.5; const R: f32 = 0.88;
15const T: f32 = 0.0;  const U: f32 = 0.25; const C: f32 = 0.5; const D: f32 = 0.75; const B: f32 = 1.0;
16
17/// Stroke segments for a single character (uppercased for letters).
18/// Unknown / non-ASCII glyphs render as a small box placeholder.
19pub fn glyph(c: char) -> Vec<[f32; 4]> {
20    let c = c.to_ascii_uppercase();
21    let s: &[[f32; 4]] = match c {
22        ' ' => &[],
23        'A' => &[[L,B,M,T],[M,T,R,B],[0.26,C,0.74,C]],
24        'B' => &[[L,T,L,B],[L,T,R,U],[R,U,L,C],[L,C,R,D],[R,D,L,B]],
25        'C' => &[[R,T,L,T],[L,T,L,B],[L,B,R,B]],
26        'D' => &[[L,T,L,B],[L,T,R,U],[R,U,R,D],[R,D,L,B]],
27        'E' => &[[R,T,L,T],[L,T,L,B],[L,B,R,B],[L,C,0.7,C]],
28        'F' => &[[R,T,L,T],[L,T,L,B],[L,C,0.7,C]],
29        'G' => &[[R,T,L,T],[L,T,L,B],[L,B,R,B],[R,B,R,C],[R,C,M,C]],
30        'H' => &[[L,T,L,B],[R,T,R,B],[L,C,R,C]],
31        'I' => &[[M,T,M,B],[L,T,R,T],[L,B,R,B]],
32        'J' => &[[R,T,R,B],[R,B,L,B],[L,B,L,D]],
33        'K' => &[[L,T,L,B],[R,T,L,C],[L,C,R,B]],
34        'L' => &[[L,T,L,B],[L,B,R,B]],
35        'M' => &[[L,B,L,T],[L,T,M,C],[M,C,R,T],[R,T,R,B]],
36        'N' => &[[L,B,L,T],[L,T,R,B],[R,B,R,T]],
37        'O' => &[[L,T,R,T],[R,T,R,B],[R,B,L,B],[L,B,L,T]],
38        'P' => &[[L,B,L,T],[L,T,R,U],[R,U,L,C]],
39        'Q' => &[[L,T,R,T],[R,T,R,B],[R,B,L,B],[L,B,L,T],[M,D,R,B]],
40        'R' => &[[L,B,L,T],[L,T,R,U],[R,U,L,C],[L,C,R,B]],
41        'S' => &[[R,T,L,T],[L,T,L,C],[L,C,R,C],[R,C,R,B],[R,B,L,B]],
42        'T' => &[[L,T,R,T],[M,T,M,B]],
43        'U' => &[[L,T,L,B],[L,B,R,B],[R,B,R,T]],
44        'V' => &[[L,T,M,B],[M,B,R,T]],
45        'W' => &[[L,T,0.3,B],[0.3,B,M,C],[M,C,0.7,B],[0.7,B,R,T]],
46        'X' => &[[L,T,R,B],[R,T,L,B]],
47        'Y' => &[[L,T,M,C],[R,T,M,C],[M,C,M,B]],
48        'Z' => &[[L,T,R,T],[R,T,L,B],[L,B,R,B]],
49        '0' => &[[L,T,R,T],[R,T,R,B],[R,B,L,B],[L,B,L,T],[L,B,R,T]],
50        '1' => &[[M,T,M,B],[L,U,M,T],[L,B,R,B]],
51        '2' => &[[L,U,M,T],[M,T,R,U],[R,U,L,B],[L,B,R,B]],
52        '3' => &[[L,T,R,T],[R,T,R,C],[R,C,M,C],[R,C,R,B],[R,B,L,B]],
53        '4' => &[[R,B,R,T],[R,T,L,C],[L,C,R,C]],
54        '5' => &[[R,T,L,T],[L,T,L,C],[L,C,R,C],[R,C,R,B],[R,B,L,B]],
55        '6' => &[[R,T,L,C],[L,C,L,B],[L,B,R,B],[R,B,R,C],[R,C,L,C]],
56        '7' => &[[L,T,R,T],[R,T,M,B]],
57        '8' => &[[L,T,R,T],[R,T,R,B],[R,B,L,B],[L,B,L,T],[L,C,R,C]],
58        '9' => &[[R,C,L,C],[L,C,L,T],[L,T,R,T],[R,T,R,B],[R,B,L,B]],
59        '-' => &[[L,C,R,C]],
60        '_' => &[[L,B,R,B]],
61        '.' => &[[0.44,0.92,0.56,0.92]],
62        ',' => &[[0.5,0.8,0.4,0.98]],
63        '!' => &[[M,T,M,0.66],[M,0.86,M,B]],
64        '?' => &[[L,U,M,T],[M,T,R,U],[R,U,M,C],[M,C,M,D],[M,0.92,M,B]],
65        ':' => &[[0.44,0.34,0.56,0.34],[0.44,0.66,0.56,0.66]],
66        '/' => &[[L,B,R,T]],
67        '+' => &[[L,C,R,C],[M,U,M,D]],
68        '*' => &[[L,C,R,C],[M,U,M,D],[0.24,0.3,0.76,0.7],[0.76,0.3,0.24,0.7]],
69        '=' => &[[L,0.38,R,0.38],[L,0.62,R,0.62]],
70        '>' => &[[L,U,R,C],[R,C,L,D]],
71        '<' => &[[R,U,L,C],[L,C,R,D]],
72        _   => &[[L,U,R,U],[R,U,R,D],[R,D,L,D],[L,D,L,U]], // placeholder box
73    };
74    s.to_vec()
75}
76
77/// Lay out a string as line segments. Each glyph occupies `gw`×`gh`, advancing by
78/// `gw + spacing`. Returns segments in absolute coords starting at `(x, y)`.
79pub fn text_lines(text: &str, x: f32, y: f32, gw: f32, gh: f32, spacing: f32) -> Vec<[f32; 4]> {
80    let mut out = Vec::new();
81    let mut cx = x;
82    for ch in text.chars() {
83        for seg in glyph(ch) {
84            out.push([cx + seg[0]*gw, y + seg[1]*gh, cx + seg[2]*gw, y + seg[3]*gh]);
85        }
86        cx += gw + spacing;
87    }
88    out
89}
90
91/// Width a string would occupy with the given glyph width + spacing.
92pub fn text_width(text: &str, gw: f32, spacing: f32) -> f32 {
93    let n = text.chars().count() as f32;
94    if n == 0.0 { 0.0 } else { n * gw + (n - 1.0) * spacing }
95}
96
97// ── Holographic frame geometry ──────────────────────────────────────────────
98
99/// Sci-fi corner brackets for a rect — eight short segments at the four corners.
100pub fn corner_brackets(x: f32, y: f32, w: f32, h: f32, len: f32) -> Vec<[f32; 4]> {
101    let l = len.min(w * 0.5).min(h * 0.5);
102    let (x1, y1) = (x + w, y + h);
103    vec![
104        [x, y, x + l, y], [x, y, x, y + l],             // top-left
105        [x1, y, x1 - l, y], [x1, y, x1, y + l],         // top-right
106        [x, y1, x + l, y1], [x, y1, x, y1 - l],         // bottom-left
107        [x1, y1, x1 - l, y1], [x1, y1, x1, y1 - l],     // bottom-right
108    ]
109}
110
111/// A clipped/beveled rectangle outline (corners cut → holographic "panel" look).
112pub fn beveled_rect(x: f32, y: f32, w: f32, h: f32, bevel: f32) -> Vec<[f32; 4]> {
113    let b = bevel.min(w * 0.5).min(h * 0.5);
114    let (x1, y1) = (x + w, y + h);
115    vec![
116        [x + b, y, x1 - b, y],       // top
117        [x1 - b, y, x1, y + b],      // top-right cut
118        [x1, y + b, x1, y1 - b],     // right
119        [x1, y1 - b, x1 - b, y1],    // bottom-right cut
120        [x1 - b, y1, x + b, y1],     // bottom
121        [x + b, y1, x, y1 - b],      // bottom-left cut
122        [x, y1 - b, x, y + b],       // left
123        [x, y + b, x + b, y],        // top-left cut
124    ]
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn hit_testing() {
133        assert!(hit_rect(15.0, 15.0, 10.0, 10.0, 20.0, 20.0));
134        assert!(!hit_rect(5.0, 5.0, 10.0, 10.0, 20.0, 20.0));
135    }
136
137    #[test]
138    fn font_covers_alphanumerics_and_lays_out() {
139        // Every A–Z/0–9 has at least one stroke.
140        for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".chars() {
141            assert!(!glyph(c).is_empty(), "glyph {c} empty");
142        }
143        assert!(glyph(' ').is_empty());
144        // Unknown → placeholder box (4 segs).
145        assert_eq!(glyph('灵').len(), 4);
146        let segs = text_lines("HI 7", 0.0, 0.0, 10.0, 16.0, 2.0);
147        assert!(!segs.is_empty());
148        assert!((text_width("ABCD", 10.0, 2.0) - (4.0*10.0 + 3.0*2.0)).abs() < 1e-3);
149    }
150
151    #[test]
152    fn frame_geometry_segment_counts() {
153        assert_eq!(corner_brackets(0.0,0.0,100.0,50.0,10.0).len(), 8);
154        assert_eq!(beveled_rect(0.0,0.0,100.0,50.0,8.0).len(), 8);
155    }
156}