sheets.rs

9.3 kB · rust · 339 lines

1use super::faces::Quad;2use crate::space::Vec3;3use std::f32::consts::TAU;45/// The working width of the sheet plane.6pub const WIDTH: f32 = 320.0;7/// The working height of the sheet plane.8pub const HEIGHT: f32 = 452.0;910const HALF: f32 = HEIGHT / 2.0;11const MARGIN: f32 = 16.0;12const MAX_LINE: usize = 48;13const MIN_SEGMENTS: usize = 8;14const MAX_SEGMENTS: usize = 24;15const MAX_FACETS: usize = 9600;16const MIN_THICK: f32 = 1.0;17const MAX_THICK: f32 = 80.0;18const BORE_MAX: f32 = 0.45;19const EPS: f32 = 1e-6;2021const UP: Vec3 = Vec3 {22    x: 0.0,23    y: 0.0,24    z: 1.0,25};26const DOWN: Vec3 = Vec3 {27    x: 0.0,28    y: 0.0,29    z: -1.0,30};3132// SHAPE3334fn fit(cols: usize, rows: usize, segments: usize) -> (usize, usize, usize) {35    let mut cols = cols.min(MAX_LINE);36    let mut rows = rows.min(MAX_LINE);37    let mut segments = segments.clamp(MIN_SEGMENTS, MAX_SEGMENTS);38    while cols * rows * segments > MAX_FACETS && segments > MIN_SEGMENTS {39        segments -= 1;40    }41    while cols * rows * segments > MAX_FACETS {42        if cols >= rows {43            cols -= 1;44        } else {45            rows -= 1;46        }47    }48    (cols, rows, segments)49}5051fn depth(thickness: f32) -> f32 {52    let held = if thickness.is_finite() {53        thickness54    } else {55        0.056    };57    held.clamp(MIN_THICK, MAX_THICK) / 2.058}5960fn point(x: f32, y: f32, z: f32) -> Vec3 {61    Vec3::new(x / HALF, y / HALF, z / HALF)62}6364fn reach(hx: f32, hy: f32, c: f32, s: f32) -> f32 {65    let tx = if c.abs() > EPS {66        hx / c.abs()67    } else {68        f32::MAX69    };70    let ty = if s.abs() > EPS {71        hy / s.abs()72    } else {73        f32::MAX74    };75    tx.min(ty)76}7778// PARTS7980fn plate(out: &mut Vec<Quad>, x0: f32, y0: f32, x1: f32, y1: f32, t: f32) {81    if x1 - x0 <= EPS || y1 - y0 <= EPS {82        return;83    }84    out.push(Quad {85        normal: UP,86        verts: [87            point(x0, y0, t),88            point(x1, y0, t),89            point(x1, y1, t),90            point(x0, y1, t),91        ],92    });93    out.push(Quad {94        normal: DOWN,95        verts: [96            point(x0, y0, -t),97            point(x0, y1, -t),98            point(x1, y1, -t),99            point(x1, y0, -t),100        ],101    });102}103104fn rim(out: &mut Vec<Quad>, w: f32, h: f32, t: f32) {105    out.push(Quad {106        normal: Vec3::new(1.0, 0.0, 0.0),107        verts: [108            point(w, -h, -t),109            point(w, h, -t),110            point(w, h, t),111            point(w, -h, t),112        ],113    });114    out.push(Quad {115        normal: Vec3::new(-1.0, 0.0, 0.0),116        verts: [117            point(-w, -h, -t),118            point(-w, -h, t),119            point(-w, h, t),120            point(-w, h, -t),121        ],122    });123    out.push(Quad {124        normal: Vec3::new(0.0, 1.0, 0.0),125        verts: [126            point(-w, h, -t),127            point(-w, h, t),128            point(w, h, t),129            point(w, h, -t),130        ],131    });132    out.push(Quad {133        normal: Vec3::new(0.0, -1.0, 0.0),134        verts: [135            point(-w, -h, -t),136            point(w, -h, -t),137            point(w, -h, t),138            point(-w, -h, t),139        ],140    });141}142143fn bored(out: &mut Vec<Quad>, at: [f32; 2], span: [f32; 2], bore: f32, t: f32, segments: usize) {144    let step = TAU / segments as f32;145    let ring: Vec<[f32; 2]> = (0..segments)146        .map(|s| {147            let a = step * s as f32;148            [a.cos(), a.sin()]149        })150        .collect();151    for s in 0..segments {152        let [c0, s0] = ring[s];153        let [c1, s1] = ring[(s + 1) % segments];154        let a = [at[0] + bore * c0, at[1] + bore * s0];155        let b = [at[0] + bore * c1, at[1] + bore * s1];156        let ra = reach(span[0], span[1], c0, s0);157        let rb = reach(span[0], span[1], c1, s1);158        let oa = [at[0] + ra * c0, at[1] + ra * s0];159        let ob = [at[0] + rb * c1, at[1] + rb * s1];160        out.push(Quad {161            normal: UP,162            verts: [163                point(oa[0], oa[1], t),164                point(ob[0], ob[1], t),165                point(b[0], b[1], t),166                point(a[0], a[1], t),167            ],168        });169        out.push(Quad {170            normal: DOWN,171            verts: [172                point(a[0], a[1], -t),173                point(b[0], b[1], -t),174                point(ob[0], ob[1], -t),175                point(oa[0], oa[1], -t),176            ],177        });178        let mid = step * (s as f32 + 0.5);179        out.push(Quad {180            normal: Vec3::new(-mid.cos(), -mid.sin(), 0.0),181            verts: [182                point(a[0], a[1], t),183                point(b[0], b[1], t),184                point(b[0], b[1], -t),185                point(a[0], a[1], -t),186            ],187        });188    }189}190191// SHEET192193/// Builds the quads of a rimmed plate bored with a cols-by-rows grid of holes.194pub fn sheet(195    cols: usize,196    rows: usize,197    diameter: f32,198    thickness: f32,199    segments: usize,200) -> Vec<Quad> {201    let (cols, rows, segments) = fit(cols, rows, segments);202    let t = depth(thickness);203    let (w, h) = (WIDTH / 2.0, HALF);204    let mut out = Vec::new();205    rim(&mut out, w, h, t);206    let pitch = [207        (WIDTH - 2.0 * MARGIN) / cols.max(1) as f32,208        (HEIGHT - 2.0 * MARGIN) / rows.max(1) as f32,209    ];210    let wide = if diameter.is_finite() { diameter } else { 0.0 };211    let bore = (wide / 2.0).clamp(0.0, BORE_MAX * pitch[0].min(pitch[1]));212    if cols == 0 || rows == 0 || bore <= EPS {213        plate(&mut out, -w, -h, w, h, t);214        return out;215    }216    plate(&mut out, -w, -h, w, -h + MARGIN, t);217    plate(&mut out, -w, h - MARGIN, w, h, t);218    plate(&mut out, -w, -h + MARGIN, -w + MARGIN, h - MARGIN, t);219    plate(&mut out, w - MARGIN, -h + MARGIN, w, h - MARGIN, t);220    let span = [pitch[0] / 2.0, pitch[1] / 2.0];221    for j in 0..rows {222        for i in 0..cols {223            let at = [224                -w + MARGIN + (i as f32 + 0.5) * pitch[0],225                -h + MARGIN + (j as f32 + 0.5) * pitch[1],226            ];227            bored(&mut out, at, span, bore, t, segments);228        }229    }230    out231}232233/// Returns the twelve wire edges of the sheet's box at the given thickness.234pub fn sheet_edges(thickness: f32) -> Vec<[Vec3; 2]> {235    let t = depth(thickness);236    let (w, h) = (WIDTH / 2.0, HALF);237    let mut out = Vec::new();238    for z in [-t, t] {239        let ring = [240            point(-w, -h, z),241            point(w, -h, z),242            point(w, h, z),243            point(-w, h, z),244        ];245        for k in 0..4 {246            out.push([ring[k], ring[(k + 1) % 4]]);247        }248    }249    for [x, y] in [[-w, -h], [w, -h], [w, h], [-w, h]] {250        out.push([point(x, y, -t), point(x, y, t)]);251    }252    out253}254255#[cfg(test)]256mod tests {257    use super::*;258    use crate::space::Pack;259260    fn buffer(quads: &[Quad]) -> Vec<f32> {261        let mut pack = Pack::new();262        for quad in quads {263            pack.quad(quad.verts, quad.normal);264        }265        pack.buffer()266    }267268    #[test]269    fn the_buffer_holds_its_header() {270        let quads = sheet(6, 8, 24.0, 8.0, 12);271        let buf = buffer(&quads);272        assert_eq!(buf[0] as usize, quads.len() * 36);273        assert_eq!(buf[1], 0.0);274        assert_eq!(buf.len(), 2 + quads.len() * 36);275        assert_eq!(buf[0] as usize % 18, 0);276        assert!(quads.len() > 6 * 8 * 12);277    }278    #[test]279    fn the_plate_keeps_the_sheet_law() {280        let quads = sheet(4, 6, 20.0, 6.0, 8);281        let mut wide = 0.0f32;282        let mut tall = 0.0f32;283        for quad in &quads {284            for v in quad.verts {285                wide = wide.max(v.x.abs());286                tall = tall.max(v.y.abs());287                assert!(v.z.abs() <= 1.0);288            }289        }290        assert!((tall - 1.0).abs() < 1e-6);291        assert!((wide - WIDTH / HEIGHT).abs() < 1e-6);292    }293    #[test]294    fn the_normals_stay_flat_and_true() {295        for quad in sheet(3, 4, 30.0, 10.0, 8) {296            let n = quad.normal;297            assert!((n.dot(n) - 1.0).abs() < 1e-5);298            for k in 0..3 {299                let e = quad.verts[k + 1] - quad.verts[0];300                assert!(n.dot(e).abs() < 1e-5);301            }302        }303    }304    #[test]305    fn the_holes_open_through_the_plate() {306        let solid = sheet(0, 0, 0.0, 8.0, 12);307        let holed = sheet(5, 7, 24.0, 8.0, 12);308        assert_eq!(solid.len(), 6);309        assert!(holed.len() > solid.len());310    }311    #[test]312    fn the_params_clamp_to_the_budget() {313        let (cols, rows, segments) = fit(4000, 4000, 999);314        assert!(cols <= MAX_LINE && rows <= MAX_LINE);315        assert!((MIN_SEGMENTS..=MAX_SEGMENTS).contains(&segments));316        assert!(cols * rows * segments <= MAX_FACETS);317        let buf = buffer(&sheet(4000, 4000, 999.0, 999.0, 999));318        assert!(buf.len() * 4 < 5_000_000);319        let (_, _, tiny) = fit(2, 2, 0);320        assert_eq!(tiny, MIN_SEGMENTS);321    }322    #[test]323    fn the_degenerate_sheets_hold_together() {324        assert_eq!(sheet(0, 8, 20.0, 8.0, 12).len(), 6);325        assert_eq!(sheet(8, 0, 20.0, 8.0, 12).len(), 6);326        assert_eq!(sheet(8, 8, 0.0, 8.0, 12).len(), 6);327        assert!(!sheet(8, 8, 4000.0, 0.0, 12).is_empty());328        assert!(!sheet(8, 8, -4.0, -4.0, 12).is_empty());329        assert!(!sheet(1, 1, f32::NAN, f32::INFINITY, 12).is_empty());330    }331    #[test]332    fn the_frame_wires_the_box() {333        let wires = sheet_edges(8.0);334        assert_eq!(wires.len(), 12);335        for [a, b] in wires {336            assert!(a != b);337        }338    }339}