From 33f0b9e3cc1fe56354ea67bfc24f347461d08f10 Mon Sep 17 00:00:00 2001 From: NAX LAB Date: Sun, 11 Oct 2026 15:28:45 -0400 Subject: [PATCH 1/2] Updated Logo --- crates/ui-egui/src/toolbar.rs | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 3cbe3329..77054efe 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -45,10 +45,47 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { crate::titlebar::drag_area(ui, r); } let left = r.left() + if app.integrated_titlebar { crate::titlebar::TRAFFIC_LIGHTS } else { 0.0 }; - // Brand mark: a small solid block drawn in code. - let logo = Rect::from_center_size(pos2(left + 20.0, r.center().y), vec2(20.0, 20.0)); - icons::paint(ui.painter(), logo, "box", Color32::from_rgb(220, 226, 236), Color32::from_rgb(90, 160, 240), Color32::WHITE); - ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolveCraft", FontId::proportional(14.0), t.app_bar_text); + + // Brand mark: the SolveCraft logo, drawn in code from the SVG's own coordinates + // (viewBox 357 x 412). Same 20 px height and centre as the old block. + { + let c = pos2(left + 20.0, r.center().y); + let s = 20.0 / 412.217; + let m = |pts: &[(f32, f32)]| -> Vec { + pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() + }; + let p = ui.painter(); + // egui only fills convex shapes, so each face is a few overlapping convex pieces. + let face = |col: Color32, pts: &[(f32, f32)]| { + p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE)); + }; + let (light, mid, dark) = (Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197)); + face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]); + face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]); + face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]); + face(mid, &[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)]); + face(mid, &[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)]); + face(dark, &[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)]); + face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]); + face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]); + face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]); + // White edge lines. + let edge = Stroke::new(10.0 * s, Color32::WHITE); + let line = |pts: &[(f32, f32)]| { + p.add(egui::Shape::line(m(pts), edge)); + }; + line(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)]); + line(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)]); + line(&[(177.0, 206.1), (263.8, 156.0)]); + p.add(egui::Shape::closed_line( + m(&[ + (178.5, 5.8), (5.0, 105.9), (5.0, 206.1), (91.7, 256.2), (91.7, 356.4), + (178.5, 406.4), (352.0, 306.3), (352.0, 206.1), (265.2, 156.0), (265.2, 55.9), + ]), + edge, + )); + } + let mut x = left + 128.0; let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str| -> bool { let br = Rect::from_center_size(pos2(x, r.center().y), vec2(26.0, 26.0)); From 344e52e207788c94589bd81318c516f6641bb0a1 Mon Sep 17 00:00:00 2001 From: NAX LAB Date: Sun, 11 Oct 2026 15:50:09 -0400 Subject: [PATCH 2/2] App Icon --- apps/solvecraft-cli/src/main.rs | 119 ++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/apps/solvecraft-cli/src/main.rs b/apps/solvecraft-cli/src/main.rs index 5916504c..70ef1bc6 100644 --- a/apps/solvecraft-cli/src/main.rs +++ b/apps/solvecraft-cli/src/main.rs @@ -219,8 +219,8 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> { Ok(()) } -/// The app icon: a blue block with a bore, drawn by the CPU renderer on a transparent background -/// (so packages need no committed image). `.ico` holds the Windows sizes as PNGs. + /// The app icon: the SolveCraft logo, drawn in code on a transparent background + /// (so packages need no committed image). `.ico` holds the Windows sizes as PNGs. fn cmd_icon(args: &[String]) -> Result<(), String> { let out = flag(args, "--out").ok_or("icon needs --out FILE.png or FILE.ico")?; let size: usize = flag(args, "--size").and_then(|x| x.parse().ok()).unwrap_or(256).clamp(8, 2048); @@ -236,30 +236,105 @@ fn cmd_icon(args: &[String]) -> Result<(), String> { Ok(()) } +/// This block is the icon image data fn icon_png(size: usize) -> Result, String> { - use solvecraft_engine::render::{Rgb, Scene, SceneLine, SceneMesh, png, render_transparent}; - let mut s = Session::default(); - for (cmd, p) in [ - ("solid.box", json!({"length": 20, "width": 20, "height": 20, "center": [0, 0, 0]})), - ("solid.cylinder", json!({"diameter": 9, "height": 30, "base": [0, 0, -15], "operation": "cut"})), - ] { - s.execute(cmd, &p).map_err(|e| format!("icon: {e}"))?; - } - let mut cam = solvecraft_engine::view::home_camera(&s); - cam.distance *= 0.95; - let mut scene = Scene { radius: 60.0, ..Default::default() }; - let px = size as f64 / 256.0; - for body in &s.world_state().bodies { - let mesh = body.mesh(); - scene.meshes.push(SceneMesh { mesh: std::sync::Arc::clone(&mesh), color: Rgb(90, 160, 240), face_colors: Vec::new() }); - for (i, e) in mesh.edges.iter().enumerate() { - if mesh.shaded_edge_visible(i) { - scene.lines.push(SceneLine { points: e.clone(), color: Rgb(24, 52, 96), width: (3.0 * px).max(0.8), on_top: false }); + use solvecraft_engine::render::png; + type P = (f32, f32); + // Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end. + let ss = if size > 512 { 2 } else { 4 }; + let n = size * ss; + let mut buf = vec![[0u8; 4]; n * n]; + let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height + let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways + // Fill a convex polygon given in SVG units. + let mut fill = |pts: &[P], col: [u8; 3]| { + let v: Vec

= pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect(); + let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1))); + for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) { + let cy = y as f32 + 0.5; + let (mut lo, mut hi) = (f32::MAX, f32::MIN); + for i in 0..v.len() { + let (a, b) = (v[i], v[(i + 1) % v.len()]); + if (a.1 <= cy) != (b.1 <= cy) { + let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0); + lo = lo.min(x); + hi = hi.max(x); + } + } + if lo > hi { + continue; + } + for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) { + buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255]; + } + } + }; + // Faces: each is a few overlapping convex pieces (the filler only does convex shapes). + let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]); + fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light); + fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light); + fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid); + fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid); + fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid); + fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark); + fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark); + fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark); + fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark); + // White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner. + let white = [255, 255, 255]; + let mut stroke = |pts: &[P], closed: bool| { + let m = pts.len(); + for i in 0..(if closed { m } else { m - 1 }) { + let (a, b) = (pts[i], pts[(i + 1) % m]); + let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt(); + let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0); + fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white); + } + for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] { + let disc: Vec

= (0..16) + .map(|k| { + let t = k as f32 * std::f32::consts::TAU / 16.0; + (c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin()) + }) + .collect(); + fill(&disc, white); + } + }; + stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false); + stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false); + stroke(&[(177.0, 206.1), (263.8, 156.0)], false); + stroke( + &[ + (178.5, 5.8), (5.0, 105.9), (5.0, 206.1), (91.7, 256.2), (91.7, 356.4), + (178.5, 406.4), (352.0, 306.3), (352.0, 206.1), (265.2, 156.0), (265.2, 55.9), + ], + true, + ); + // Average each ss x ss block down to one pixel (straight alpha = covered share). + let mut rgba = Vec::with_capacity(size * size * 4); + for y in 0..size { + for x in 0..size { + let (mut sum, mut cnt) = ([0u32; 3], 0u32); + for j in 0..ss { + for i in 0..ss { + let p = buf[(y * ss + j) * n + x * ss + i]; + if p[3] != 0 { + cnt += 1; + for k in 0..3 { + sum[k] += u32::from(p[k]); + } + } + } + } + if cnt == 0 { + rgba.extend_from_slice(&[0, 0, 0, 0]); + } else { + let a = 255 * cnt / (ss * ss) as u32; + rgba.extend_from_slice(&[(sum[0] / cnt) as u8, (sum[1] / cnt) as u8, (sum[2] / cnt) as u8, a as u8]); } } } - let c = render_transparent(&mut scene, &cam, size, size); - png(c.w, c.h, c.rgba).ok_or_else(|| "icon: PNG encoding failed".to_string()) + png(size, size, rgba).ok_or_else(|| "icon: PNG encoding failed".to_string()) } /// A Windows icon file holding PNG images (Vista and later read these).