Files
OneCAD/assets/icons/ui/source/icons.rs
T

1608 lines
79 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Toolbar and browser icons, drawn in code (no image assets). Each icon is designed on a
//! 24 × 24 grid and scaled to the target rectangle.
use egui::{Color32, Mesh, Painter, Pos2, Rect, Shape, Stroke, pos2, vec2};
/// `n + 1` points along the cubic Bézier curve with control points `c`.
fn bezier(c: [(f32, f32); 4], n: usize) -> Vec<(f32, f32)> {
(0..=n)
.map(|i| i as f32 / n as f32)
.map(|t| {
let u = 1.0 - t;
let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
(w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum())
})
.collect()
}
/// `n + 1` points along an ellipse from angle `a0` to `a1` (y down: angles below the centre
/// first).
fn ellipse(x: f32, y: f32, rx: f32, ry: f32, a0: f32, a1: f32, n: usize) -> Vec<(f32, f32)> {
(0..=n).map(|i| a0 + (a1 - a0) * i as f32 / n as f32).map(|a| (x + rx * a.cos(), y + ry * a.sin())).collect()
}
/// A gear's toothed outline about (x, y): `n` teeth from the root radius `root` out to `tip`,
/// turned by `phase` radians.
fn teeth(x: f32, y: f32, root: f32, tip: f32, n: usize, phase: f32) -> Vec<(f32, f32)> {
let step = std::f32::consts::TAU / n as f32;
let at = |a: f32, r: f32| (x + r * a.cos(), y - r * a.sin());
(0..n)
.flat_map(|k| {
let a = phase + step * k as f32;
[at(a - 0.3 * step, root), at(a - 0.15 * step, tip), at(a + 0.15 * step, tip), at(a + 0.3 * step, root)]
})
.collect()
}
struct Pen<'a> {
p: &'a Painter,
r: Rect,
ink: Color32,
fill: Color32,
accent: Color32,
w: f32,
}
impl Pen<'_> {
fn at(&self, x: f32, y: f32) -> Pos2 {
pos2(self.r.left() + x / 24.0 * self.r.width(), self.r.top() + y / 24.0 * self.r.height())
}
fn s(&self) -> f32 {
self.r.width() / 24.0
}
fn line(&self, pts: &[(f32, f32)], c: Color32) {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.at(*x, *y)).collect();
self.p.add(Shape::line(v, Stroke::new(self.w, c)));
}
fn poly(&self, pts: &[(f32, f32)], fill: Color32, c: Color32) {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.at(*x, *y)).collect();
self.p.add(Shape::convex_polygon(v, fill, Stroke::new(self.w, c)));
}
fn closed(&self, pts: &[(f32, f32)], c: Color32) {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.at(*x, *y)).collect();
self.p.add(Shape::closed_line(v, Stroke::new(self.w, c)));
}
fn circle(&self, x: f32, y: f32, r: f32, fill: Color32, c: Color32) {
self.p.circle(self.at(x, y), r * self.s(), fill, Stroke::new(self.w, c));
}
fn dot(&self, x: f32, y: f32, c: Color32) {
self.p.circle_filled(self.at(x, y), 1.6 * self.s(), c);
}
fn arc(&self, x: f32, y: f32, r: f32, a0: f32, a1: f32, c: Color32) {
let n = 24;
let pts: Vec<(f32, f32)> = (0..=n).map(|i| a0 + (a1 - a0) * i as f32 / n as f32).map(|a| (x + r * a.cos(), y - r * a.sin())).collect();
self.line(&pts, c);
}
fn arrow(&self, from: (f32, f32), to: (f32, f32), c: Color32) {
self.line(&[from, to], c);
let d = vec2(to.0 - from.0, to.1 - from.1).normalized();
let n = vec2(-d.y, d.x);
let tip = vec2(to.0, to.1);
let a = tip - d * 3.5 + n * 2.2;
let b = tip - d * 3.5 - n * 2.2;
self.poly(&[to, (a.x, a.y), (b.x, b.y)], c, c);
}
/// Isometric box from a 2D footprint height.
fn iso_box(&self, x0: f32, y0: f32, w: f32, d: f32, h: f32) {
let top = [(x0, y0), (x0 + w, y0 - d * 0.5), (x0 + w + d, y0), (x0 + d, y0 + d * 0.5)];
let front = [(x0, y0), (x0 + d, y0 + d * 0.5), (x0 + d, y0 + d * 0.5 + h), (x0, y0 + h)];
let side = [(x0 + d, y0 + d * 0.5), (x0 + w + d, y0), (x0 + w + d, y0 + h), (x0 + d, y0 + d * 0.5 + h)];
self.poly(&front, self.fill.gamma_multiply(0.85), self.ink);
self.poly(&side, self.fill.gamma_multiply(0.7), self.ink);
self.poly(&top, self.fill, self.ink);
}
/// A cubic Bézier curve through control points `c`.
fn curve(&self, c: [(f32, f32); 4], col: Color32) {
self.line(&bezier(c, 16), col);
}
fn dashed(&self, pts: &[(f32, f32)], c: Color32) {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.at(*x, *y)).collect();
self.p.add(Shape::dashed_line(&v, Stroke::new(self.w, c), 2.0 * self.s(), 1.6 * self.s()));
}
/// Fill the strip between two polylines that run the same way, such as the front and back
/// edges of a curved sheet. Unlike `poly`, the strip need not be convex.
fn band(&self, a: &[(f32, f32)], b: &[(f32, f32)], fill: Color32) {
let mut m = Mesh::default();
for (p, q) in a.iter().zip(b) {
m.colored_vertex(self.at(p.0, p.1), fill);
m.colored_vertex(self.at(q.0, q.1), fill);
}
let n = a.len().min(b.len()) as u32;
for i in 1..n {
let k = 2 * i;
m.add_triangle(k - 2, k - 1, k);
m.add_triangle(k - 1, k + 1, k);
}
self.p.add(Shape::mesh(m));
}
}
/// The stroke width for the Normal/Construction/Centerline line-type icons. It is heavier than
/// the generic icon weight (`Pen::w`). It is sized from whichever side of `r` is smaller. A
/// short, wide button and a square toolbar slot both get a stroke their gaps can survive next
/// to.
fn line_type_stroke(r: Rect) -> f32 {
(r.width().min(r.height()) / 24.0 * 2.0).max(1.6)
}
/// The Centerline icon's dot radius. It is sized from `r`. The stroke width plays no part in it.
/// It stays a round mark even next to a heavy stroke.
fn line_type_dot_radius(r: Rect) -> f32 {
(r.width().min(r.height()) * 0.16).max(2.4)
}
/// Paint the icon `name` into `r`. `accent` highlights the "action" part.
pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, accent: Color32) {
paint_with(p, r, name, ink, fill, accent, &crate::theme::Tokens::get().icons);
}
/// [`paint`] with the colours an icon draws of its own given (`theme::IconColors`).
pub(crate) fn paint_with(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, accent: Color32, ic: &crate::theme::IconColors) {
let ic = *ic;
let pen = Pen { p, r, ink, fill, accent, w: (r.width() / 24.0 * 1.4).max(1.0) };
let a = pen.accent;
match name {
"sketch" => {
pen.poly(&[(3.0, 14.0), (13.0, 9.0), (21.0, 13.0), (11.0, 18.0)], pen.fill, ink);
pen.line(&[(7.0, 13.0), (16.0, 4.0), (18.0, 6.0), (9.0, 15.0), (6.5, 15.5), (7.0, 13.0)], a);
}
"finish" => {
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, pen.accent);
pen.line(&[(7.0, 12.5), (10.5, 16.0), (17.0, 8.5)], a);
}
"extrude" => {
pen.iso_box(4.0, 11.0, 9.0, 7.0, 7.0);
pen.arrow((12.0, 9.0), (12.0, 2.5), a);
}
"revolve" => {
pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink);
pen.poly(&[(13.0, 7.0), (19.0, 7.0), (19.0, 17.0), (13.0, 17.0)], pen.fill, ink);
pen.arc(12.0, 12.0, 9.0, 3.6, 5.8, a);
pen.arrow((15.0, 20.4), (18.5, 19.0), a);
}
"box" => pen.iso_box(3.0, 9.0, 10.0, 8.0, 9.0),
"cylinder" => {
pen.poly(&[(5.0, 6.0), (19.0, 6.0), (19.0, 18.0), (5.0, 18.0)], pen.fill.gamma_multiply(0.8), Color32::TRANSPARENT);
pen.line(&[(5.0, 6.0), (5.0, 18.0)], ink);
pen.line(&[(19.0, 6.0), (19.0, 18.0)], ink);
let e = |y: f32, fillc: Color32| {
let pts: Vec<Pos2> =
(0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 7.0 * t.cos(), y + 2.5 * t.sin())).collect();
p.add(Shape::convex_polygon(pts, fillc, Stroke::new(pen.w, ink)));
};
pen.arc(12.0, 18.0, 7.0, std::f32::consts::PI, std::f32::consts::TAU, ink);
e(6.0, pen.fill);
}
"sphere" => {
pen.circle(12.0, 12.0, 8.5, pen.fill, ink);
pen.arc(12.0, 12.0, 8.5, 3.3, 6.1, ink.gamma_multiply(0.5));
p.circle_filled(pen.at(9.0, 8.5), 2.0 * pen.s(), ic.glint);
}
"torus" => {
let pts: Vec<Pos2> =
(0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 5.5 * t.sin())).collect();
p.add(Shape::convex_polygon(pts, pen.fill, Stroke::new(pen.w, ink)));
let pts: Vec<Pos2> =
(0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 3.5 * t.cos(), 11.0 + 1.8 * t.sin())).collect();
p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, ink)));
}
"fillet" => {
pen.poly(&[(4.0, 20.0), (4.0, 4.0), (10.0, 4.0), (10.0, 14.0), (20.0, 14.0), (20.0, 20.0)], pen.fill, Color32::TRANSPARENT);
pen.line(&[(4.0, 20.0), (4.0, 4.0)], ink);
pen.line(&[(4.0, 20.0), (20.0, 20.0)], ink);
pen.arc(4.0, 4.0, 16.0, -std::f32::consts::FRAC_PI_2, 0.0, ink);
pen.arc(20.0, 4.0, 10.0, std::f32::consts::PI, std::f32::consts::PI * 1.5, a);
}
"chamfer" => {
pen.poly(&[(4.0, 20.0), (4.0, 4.0), (12.0, 4.0), (20.0, 12.0), (20.0, 20.0)], pen.fill, ink);
pen.line(&[(12.0, 4.0), (20.0, 12.0)], a);
}
"combine" => {
pen.poly(&[(3.0, 8.0), (14.0, 8.0), (14.0, 19.0), (3.0, 19.0)], pen.fill, ink);
pen.poly(&[(9.0, 4.0), (21.0, 4.0), (21.0, 15.0), (9.0, 15.0)], pen.fill.gamma_multiply(0.75), a);
}
"pattern_rect" => {
for (x, y) in [(4.0, 4.0), (14.0, 4.0), (4.0, 14.0), (14.0, 14.0)] {
pen.poly(
&[(x, y), (x + 6.0, y), (x + 6.0, y + 6.0), (x, y + 6.0)],
if x == 4.0 && y == 4.0 { pen.fill } else { Color32::TRANSPARENT },
if x == 4.0 && y == 4.0 { ink } else { a },
);
}
}
"pattern_circ" => {
for i in 0..6 {
let t = i as f32 / 6.0 * std::f32::consts::TAU;
pen.circle(
12.0 + 7.5 * t.cos(),
12.0 + 7.5 * t.sin(),
2.4,
if i == 0 { pen.fill } else { Color32::TRANSPARENT },
if i == 0 { ink } else { a },
);
}
}
"hole" => {
pen.poly(&[(3.0, 8.0), (21.0, 8.0), (21.0, 20.0), (3.0, 20.0)], pen.fill, ink);
let pts: Vec<Pos2> =
(0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 5.0 * t.cos(), 8.0 + 2.0 * t.sin())).collect();
p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, a)));
pen.line(&[(7.0, 8.0), (7.0, 16.0)], a);
pen.line(&[(17.0, 8.0), (17.0, 16.0)], a);
}
"sweep" => {
pen.arc(20.0, 20.0, 14.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, ink);
pen.circle(6.0, 20.0, 3.0, pen.fill, a);
pen.circle(20.0, 6.0, 3.0, pen.fill, a);
}
"loft" => {
pen.poly(&[(3.0, 20.0), (15.0, 20.0), (21.0, 15.0), (9.0, 15.0)], pen.fill, ink);
pen.circle(12.0, 6.0, 3.5, Color32::TRANSPARENT, a);
pen.line(&[(3.0, 20.0), (8.5, 6.0)], a);
pen.line(&[(21.0, 15.0), (15.5, 6.0)], a);
}
"shell" => {
pen.iso_box(3.0, 9.0, 10.0, 8.0, 9.0);
pen.poly(&[(6.0, 9.0), (13.0, 5.5), (18.0, 8.0), (11.0, 11.5)], ic.highlight, a);
}
"draft" => {
pen.poly(&[(4.0, 20.0), (20.0, 20.0), (17.0, 5.0), (7.0, 5.0)], pen.fill, ink);
pen.line(&[(20.0, 20.0), (20.0, 5.0)], a);
}
"split" => {
pen.poly(&[(3.0, 6.0), (21.0, 6.0), (21.0, 18.0), (3.0, 18.0)], pen.fill, ink);
pen.line(&[(8.0, 2.0), (16.0, 22.0)], a);
}
"mirror" => {
pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink);
pen.poly(&[(3.0, 7.0), (9.0, 10.0), (9.0, 17.0), (3.0, 17.0)], pen.fill, ink);
pen.poly(&[(21.0, 7.0), (15.0, 10.0), (15.0, 17.0), (21.0, 17.0)], Color32::TRANSPARENT, a);
}
"lookat" => {
pen.poly(&[(4.0, 6.0), (16.0, 6.0), (16.0, 18.0), (4.0, 18.0)], pen.fill, ink);
pen.arrow((22.0, 12.0), (13.0, 12.0), a);
}
"style" => {
pen.poly(&[(12.0, 3.0), (20.0, 7.5), (12.0, 12.0), (4.0, 7.5)], pen.fill, ink);
pen.poly(&[(4.0, 7.5), (12.0, 12.0), (12.0, 21.0), (4.0, 16.5)], a, ink);
pen.poly(&[(12.0, 12.0), (20.0, 7.5), (20.0, 16.5), (12.0, 21.0)], Color32::TRANSPARENT, ink);
}
// Original bug glyph: rounded body, antennae and three pairs of legs.
"bug_report" => {
pen.circle(12.0, 13.5, 5.0, Color32::TRANSPARENT, ink);
pen.line(&[(12.0, 9.0), (12.0, 18.0)], ink);
pen.line(&[(9.5, 8.5), (8.0, 5.0)], ink);
pen.line(&[(14.5, 8.5), (16.0, 5.0)], ink);
for y in [10.0, 13.5, 17.0] {
pen.line(&[(3.0, y), (7.0, y)], ink);
pen.line(&[(17.0, y), (21.0, y)], ink);
}
}
// Update: an arrow down onto a tray (a newer release can be downloaded).
"update" => {
pen.line(&[(12.0, 3.5), (12.0, 14.5)], ink);
pen.line(&[(7.5, 10.0), (12.0, 14.5), (16.5, 10.0)], ink);
pen.line(&[(4.0, 15.0), (4.0, 20.0), (20.0, 20.0), (20.0, 15.0)], ink);
}
// Agent chat: a speech bubble with three dots.
"agent_chat" => {
pen.closed(&[(4.0, 5.0), (20.0, 5.0), (20.0, 16.0), (11.0, 16.0), (6.5, 20.0), (7.5, 16.0), (4.0, 16.0)], ink);
for x in [8.5, 12.0, 15.5] {
pen.dot(x, 10.5, ink);
}
}
// Help: a question mark in a ring.
"help" => {
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
pen.arc(12.0, 9.5, 3.2, -0.6, 3.4, ink);
pen.line(&[(13.9, 12.0), (12.0, 13.6), (12.0, 15.0)], ink);
pen.dot(12.0, 17.8, ink);
}
"sun" => {
pen.circle(12.0, 12.0, 4.5, a, ink);
for i in 0..8 {
let t = i as f32 / 8.0 * std::f32::consts::TAU;
pen.line(&[(12.0 + 7.0 * t.cos(), 12.0 + 7.0 * t.sin()), (12.0 + 9.5 * t.cos(), 12.0 + 9.5 * t.sin())], ink);
}
}
"moon" => {
let outer: Vec<Pos2> =
(0..=24).map(|i| -0.6 + i as f32 / 24.0 * 4.4).map(|t| pen.at(12.0 + 8.0 * t.cos(), 12.0 + 8.0 * t.sin())).collect();
let inner: Vec<Pos2> =
(0..=24).rev().map(|i| -0.9 + i as f32 / 24.0 * 3.9).map(|t| pen.at(15.5 + 6.5 * t.cos(), 9.5 + 6.5 * t.sin())).collect();
let mut pts = outer;
pts.extend(inner);
p.add(Shape::closed_line(pts, Stroke::new(pen.w, ink)));
}
"section" => {
pen.poly(&[(4.0, 8.0), (14.0, 4.0), (20.0, 8.0), (10.0, 12.0)], pen.fill, ink);
pen.poly(&[(4.0, 8.0), (10.0, 12.0), (10.0, 20.0), (4.0, 16.0)], pen.fill, ink);
pen.poly(&[(10.0, 12.0), (20.0, 8.0), (20.0, 16.0), (10.0, 20.0)], a, ink);
}
"presspull" => {
pen.poly(&[(3.0, 15.0), (12.0, 11.0), (21.0, 15.0), (12.0, 19.0)], pen.fill, ink);
pen.arrow((12.0, 15.0), (12.0, 3.0), a);
}
"move" => {
for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0)] {
pen.arrow((12.0, 12.0), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), a);
}
}
"configuration_table" => {
pen.poly(&[(3.0, 4.0), (21.0, 4.0), (21.0, 20.0), (3.0, 20.0)], pen.fill, ink);
pen.line(&[(3.0, 9.0), (21.0, 9.0)], a);
pen.line(&[(3.0, 14.0), (21.0, 14.0)], ink);
pen.line(&[(9.0, 4.0), (9.0, 20.0)], ink);
}
"params" => {
pen.line(&[(8.0, 20.0), (9.0, 8.0), (10.0, 5.0), (13.0, 4.0)], ink);
pen.line(&[(6.0, 11.0), (12.0, 11.0)], ink);
pen.line(&[(14.0, 13.0), (20.0, 20.0)], a);
pen.line(&[(20.0, 13.0), (14.0, 20.0)], a);
}
"compute" => {
pen.arc(12.0, 12.0, 7.5, 0.4, 3.0, ink);
pen.arc(12.0, 12.0, 7.5, 3.5, 6.0, a);
pen.arrow((5.0, 9.0), (4.6, 13.0), ink);
pen.arrow((19.0, 15.0), (19.4, 11.0), a);
}
"delete" => {
pen.line(&[(6.0, 6.0), (18.0, 18.0)], pen.accent);
pen.line(&[(18.0, 6.0), (6.0, 18.0)], pen.accent);
}
"measure" => {
pen.poly(&[(2.0, 15.0), (15.0, 2.0), (22.0, 9.0), (9.0, 22.0)], pen.fill, ink);
for i in 0..5 {
let t = 4.0 + i as f32 * 2.6;
pen.line(&[(t, 15.0 - (t - 2.0)), (t + 2.0, 15.0 - (t - 2.0) + 2.0)], ink);
}
}
"line" => {
pen.line(&[(4.0, 19.0), (20.0, 5.0)], a);
pen.dot(4.0, 19.0, ink);
pen.dot(20.0, 5.0, ink);
}
"rect" | "rect3" | "rect_center" => {
pen.closed(&[(4.0, 6.0), (20.0, 6.0), (20.0, 18.0), (4.0, 18.0)], a);
match name {
"rect" => {
pen.dot(4.0, 18.0, ink);
pen.dot(20.0, 6.0, ink);
}
"rect3" => {
pen.dot(4.0, 18.0, ink);
pen.dot(20.0, 18.0, ink);
pen.dot(20.0, 6.0, ink);
}
_ => {
pen.dot(12.0, 12.0, ink);
pen.dot(20.0, 6.0, ink);
}
}
}
"rect_rounded" => {
// Straight sides between quarter arcs of radius 3 at the corners.
pen.line(&[(7.0, 6.0), (17.0, 6.0)], a);
pen.line(&[(20.0, 9.0), (20.0, 15.0)], a);
pen.line(&[(17.0, 18.0), (7.0, 18.0)], a);
pen.line(&[(4.0, 15.0), (4.0, 9.0)], a);
let q = std::f32::consts::FRAC_PI_2;
pen.arc(17.0, 9.0, 3.0, 0.0, q, a);
pen.arc(7.0, 9.0, 3.0, q, 2.0 * q, a);
pen.arc(7.0, 15.0, 3.0, 2.0 * q, 3.0 * q, a);
pen.arc(17.0, 15.0, 3.0, 3.0 * q, 4.0 * q, a);
pen.dot(4.0, 18.0, ink);
pen.dot(20.0, 6.0, ink);
}
"circle" | "circle2" | "circle3" => {
pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, a);
match name {
"circle" => {
pen.dot(12.0, 12.0, ink);
pen.line(&[(12.0, 12.0), (17.6, 6.4)], ink);
}
"circle2" => {
pen.dot(4.0, 12.0, ink);
pen.dot(20.0, 12.0, ink);
}
_ => {
pen.dot(4.0, 12.0, ink);
pen.dot(12.0, 4.0, ink);
pen.dot(20.0, 12.0, ink);
}
}
}
"arc3" | "arc_center" => {
pen.arc(12.0, 16.0, 9.0, 0.0, std::f32::consts::PI, a);
pen.dot(3.0, 16.0, ink);
pen.dot(21.0, 16.0, ink);
if name == "arc3" {
pen.dot(12.0, 7.0, ink);
} else {
pen.dot(12.0, 16.0, ink);
}
}
"polygon" => {
let pts: Vec<(f32, f32)> =
(0..6).map(|i| i as f32 / 6.0 * std::f32::consts::TAU + 0.52).map(|t| (12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect();
pen.closed(&pts, a);
pen.dot(12.0, 12.0, ink);
}
"slot" => {
pen.arc(8.0, 12.0, 5.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI * 1.5, a);
pen.arc(16.0, 12.0, 5.0, -std::f32::consts::FRAC_PI_2, std::f32::consts::FRAC_PI_2, a);
pen.line(&[(8.0, 7.0), (16.0, 7.0)], a);
pen.line(&[(8.0, 17.0), (16.0, 17.0)], a);
pen.dot(8.0, 12.0, ink);
pen.dot(16.0, 12.0, ink);
}
"point" => {
pen.dot(12.0, 12.0, a);
pen.circle(12.0, 12.0, 4.0, Color32::TRANSPARENT, ink);
}
"dimension" => {
pen.line(&[(4.0, 18.0), (4.0, 8.0)], ink);
pen.line(&[(20.0, 18.0), (20.0, 8.0)], ink);
pen.arrow((12.0, 11.0), (4.5, 11.0), a);
pen.arrow((12.0, 11.0), (19.5, 11.0), a);
pen.line(&[(9.0, 15.0), (15.0, 15.0)], ink);
}
// The Sketch Palette's line type selector (normal, construction, centerline) paints all
// three on the same diagonal, from (3, 19) to (19, 3). Only the dash pattern changes
// between them. Normal draws in `ink` and ignores `accent`. It is a plain line. It is
// never construction geometry. Its colour must stay fixed even if a caller passes the
// construction orange for it. Construction and centerline draw only in `accent` and
// ignore `ink`. The caller decides the colour. The palette and the toolbar both pass
// the theme's construction orange.
//
// These three are drawn small. The Sketch Palette draws them down to about 18 px
// square. At that size a repeating dash pattern loses its gaps. It reads as a solid
// line. Two patterns that only differ in dash phase read as the same texture too.
// Normal, Construction and Centerline instead get three different silhouettes. One is
// an unbroken line. One is a line broken by a single gap. One is a line broken by a
// single gap with a bold dot sitting in it. A viewer can name the shape on sight. No
// rhythm needs measuring. The stroke width and the dot radius both come from the rect
// actually passed in. That keeps the shapes legible in the palette, the toolbar and the
// command palette, at every size any of them uses.
"normal" => {
let w = line_type_stroke(r);
p.add(Shape::line(vec![pen.at(3.0, 19.0), pen.at(19.0, 3.0)], Stroke::new(w, ink)));
}
"construction" => {
// One gap, no dot. It is an unmistakably broken line. Nothing sits in the break.
let w = line_type_stroke(r);
let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t);
for (t0, t1) in [(0.0, 0.35), (0.65, 1.0)] {
p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a)));
}
}
"centerline" => {
// The same gap as Construction, with a bold dot centred in it. The silhouette is
// line, dot, line. The dot's radius comes from the rect. It reads as a distinct
// round mark on its own, never as a thicker stretch of the stroke. The dashes stop
// well short of it on both sides. The dot never touches either stroke. It always
// reads as a separate shape.
let w = line_type_stroke(r);
let dot_r = line_type_dot_radius(r);
let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t);
for (t0, t1) in [(0.0, 0.26), (0.74, 1.0)] {
p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a)));
}
p.circle_filled(at(0.5), dot_r, a);
}
"infinite" => {
// An infinite line (#1359): Construction's broken line, with an open arrowhead at
// each end saying it runs on. Accent only, like Construction and Centerline.
let w = line_type_stroke(r);
let at = |t: f32| pen.at(3.0 + 16.0 * t, 19.0 - 16.0 * t);
for (t0, t1) in [(0.0, 0.38), (0.62, 1.0)] {
p.add(Shape::line(vec![at(t0), at(t1)], Stroke::new(w, a)));
}
// Each head: two short strokes back from the tip, either side of the line.
let tip_len = 0.3;
for (tip, back) in [(0.0f32, tip_len), (1.0, 1.0 - tip_len)] {
let (t, b) = (at(tip), at(back));
let along = b - t;
let side = egui::vec2(-along.y, along.x) * 0.55;
p.add(Shape::line(vec![b + side, t, b - side], Stroke::new(w, a)));
}
}
"c_hv" => {
pen.line(&[(4.0, 18.0), (20.0, 18.0)], a);
pen.line(&[(6.0, 4.0), (6.0, 14.0)], a);
}
"c_coincident" => {
pen.line(&[(3.0, 18.0), (12.0, 12.0)], ink);
pen.line(&[(12.0, 12.0), (21.0, 16.0)], ink);
pen.circle(12.0, 12.0, 2.6, ic.highlight, a);
}
"c_tangent" => {
pen.circle(12.0, 14.0, 6.0, Color32::TRANSPARENT, ink);
pen.line(&[(3.0, 8.0), (21.0, 8.0)], a);
}
"c_equal" => {
pen.line(&[(5.0, 9.0), (19.0, 9.0)], a);
pen.line(&[(5.0, 15.0), (19.0, 15.0)], a);
}
"c_parallel" => {
pen.line(&[(5.0, 19.0), (13.0, 5.0)], a);
pen.line(&[(11.0, 19.0), (19.0, 5.0)], a);
}
"c_perpendicular" => {
pen.line(&[(4.0, 19.0), (20.0, 19.0)], a);
pen.line(&[(12.0, 19.0), (12.0, 5.0)], a);
pen.line(&[(12.0, 15.0), (16.0, 15.0), (16.0, 19.0)], ink);
}
"c_fix" => {
pen.circle(12.0, 9.0, 4.0, Color32::TRANSPARENT, a);
pen.line(&[(12.0, 13.0), (12.0, 21.0)], a);
pen.line(&[(9.0, 21.0), (15.0, 21.0)], ink);
}
"c_midpoint" => {
pen.line(&[(3.0, 15.0), (21.0, 9.0)], ink);
pen.poly(&[(12.0, 9.0), (15.0, 12.0), (12.0, 15.0), (9.0, 12.0)], a, a);
}
"c_concentric" => {
pen.circle(12.0, 12.0, 8.5, Color32::TRANSPARENT, a);
pen.circle(12.0, 12.0, 4.0, Color32::TRANSPARENT, a);
}
"c_collinear" => {
pen.line(&[(3.0, 19.0), (10.0, 12.0)], a);
pen.line(&[(14.0, 8.0), (21.0, 1.0)], a);
}
"c_symmetry" => {
pen.line(&[(12.0, 3.0), (12.0, 21.0)], ink);
pen.poly(&[(4.0, 8.0), (9.0, 12.0), (4.0, 16.0)], a, a);
pen.poly(&[(20.0, 8.0), (15.0, 12.0), (20.0, 16.0)], a, a);
}
"undo" | "redo" => {
let (x0, x1) = if name == "undo" { (19.0, 5.0) } else { (5.0, 19.0) };
pen.line(&[(x0, 18.0), (x0, 12.0), (12.0, 9.0), (x1, 9.0)], ink);
pen.arrow((x1 + (x0 - x1) * 0.15, 9.0), (x1, 9.0), ink);
}
"new" => {
pen.poly(&[(6.0, 3.0), (15.0, 3.0), (19.0, 7.0), (19.0, 21.0), (6.0, 21.0)], ic.highlight, ink);
pen.line(&[(15.0, 3.0), (15.0, 7.0), (19.0, 7.0)], ink);
}
"open" => {
pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (20.0, 9.0), (20.0, 19.0), (3.0, 19.0)], pen.fill, ink);
}
"save" => {
pen.poly(&[(4.0, 4.0), (17.0, 4.0), (20.0, 7.0), (20.0, 20.0), (4.0, 20.0)], pen.fill, ink);
pen.poly(&[(8.0, 4.0), (16.0, 4.0), (16.0, 9.0), (8.0, 9.0)], ic.highlight, ink);
}
"export" => {
pen.poly(&[(4.0, 10.0), (4.0, 20.0), (16.0, 20.0), (16.0, 10.0)], pen.fill, ink);
pen.arrow((10.0, 14.0), (20.0, 4.0), a);
}
"import" => {
pen.iso_box(4.0, 12.0, 12.0, 7.0, 7.0);
pen.arrow((20.0, 3.0), (12.0, 11.0), a);
}
"home" => {
pen.poly(&[(4.0, 12.0), (12.0, 4.0), (20.0, 12.0)], pen.fill, ink);
pen.poly(&[(6.0, 12.0), (18.0, 12.0), (18.0, 20.0), (6.0, 20.0)], pen.fill, ink);
}
"fit" => {
for (cx, cy, dx, dy) in [(4.0, 4.0, 1.0, 1.0), (20.0, 4.0, -1.0, 1.0), (4.0, 20.0, 1.0, -1.0), (20.0, 20.0, -1.0, -1.0)] {
pen.line(&[(cx + dx * 5.0, cy), (cx, cy), (cx, cy + dy * 5.0)], ink);
}
pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 15.0), (9.0, 15.0)], pen.fill, ink);
}
"orbit" => {
pen.arc(12.0, 12.0, 8.0, 0.3, 5.5, ink);
pen.arrow((17.0, 17.5), (19.6, 14.5), a);
pen.dot(12.0, 12.0, ink);
}
"spin" => {
// A turntable: an ellipse with an arrow running round it, a post in the middle.
let pts: Vec<Pos2> = (0..=28).map(|i| 0.2 + i as f32 / 28.0 * 5.6).map(|t| pen.at(12.0 + 9.0 * t.cos(), 16.0 + 3.5 * t.sin())).collect();
p.add(Shape::line(pts, Stroke::new(pen.w, ink)));
pen.arrow((17.0, 19.0), (20.5, 17.0), a);
pen.line(&[(12.0, 15.5), (12.0, 4.0)], ink);
pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 4.0), (9.0, 4.0)], Color32::TRANSPARENT, ink);
}
"pan" => {
for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0)] {
pen.arrow((12.0 + 3.0 * dx, 12.0 + 3.0 * dy), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), ink);
}
}
"zoom" => {
pen.circle(10.0, 10.0, 6.0, ic.highlight, ink);
pen.line(&[(14.5, 14.5), (20.0, 20.0)], ink);
}
"perspective" => {
pen.closed(&[(4.0, 6.0), (20.0, 4.0), (20.0, 20.0), (4.0, 18.0)], ink);
pen.closed(&[(9.0, 9.0), (15.0, 8.5), (15.0, 15.5), (9.0, 15.0)], ink);
}
"eye" => {
pen.arc(12.0, 18.0, 10.0, 0.6, 2.54, ink);
pen.arc(12.0, 6.0, 10.0, 3.74, 5.68, ink);
pen.circle(12.0, 12.0, 2.6, ink, ink);
}
"body" => pen.iso_box(4.0, 10.0, 8.0, 6.0, 6.0),
// Component: a box with a smaller one stacked on it.
"component" => {
pen.iso_box(2.0, 13.0, 9.0, 6.0, 6.0);
pen.iso_box(9.0, 6.0, 5.0, 4.0, 4.0);
}
"folder" => pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (21.0, 9.0), (21.0, 19.0), (3.0, 19.0)], ic.folder, ink),
"origin" => {
pen.arrow((6.0, 18.0), (20.0, 18.0), ic.axes[0]);
pen.arrow((6.0, 18.0), (6.0, 4.0), ic.axes[2]);
pen.arrow((6.0, 18.0), (15.0, 9.0), ic.axes[1]);
}
"plane" => pen.poly(&[(3.0, 15.0), (12.0, 8.0), (21.0, 11.0), (12.0, 18.0)], ic.plane, ink),
"axis" => {
pen.line(&[(4.0, 20.0), (20.0, 4.0)], ink);
pen.circle(4.0, 20.0, 2.0, ink, ink);
}
"settings" => {
// A gear: a solid ring with six chunky, square-ish teeth and a see-through centre
// hole (#290; thin rays read as a sun).
let (teeth, root, tip, hole) = (6usize, 7.0f32, 10.5f32, 3.2f32);
let pitch = std::f32::consts::TAU / teeth as f32;
// Half-widths (radians) of a tooth at its root and at its tip: nearly parallel sides.
let (half_root, half_tip) = (pitch * 0.3, pitch * 0.22);
let polar = |a: f32, r: f32| (12.0 + r * a.cos(), 12.0 + r * a.sin());
let start = -std::f32::consts::FRAC_PI_2;
let mut outline = Vec::new();
for i in 0..teeth {
let c = start + i as f32 * pitch;
// The tooth, filled; it reaches into the ring so no seam shows.
let quad = [polar(c - half_root, root - 1.0), polar(c - half_tip, tip), polar(c + half_tip, tip), polar(c + half_root, root - 1.0)];
let v: Vec<Pos2> = quad.iter().map(|(x, y)| pen.at(*x, *y)).collect();
p.add(Shape::convex_polygon(v, pen.fill, Stroke::NONE));
outline.extend([polar(c - half_root, root), polar(c - half_tip, tip), polar(c + half_tip, tip), polar(c + half_root, root)]);
// The root circle up to the next tooth.
for k in 1..4 {
let a = c + half_root + (pitch - 2.0 * half_root) * k as f32 / 4.0;
outline.push(polar(a, root));
}
}
// The ring: a thick stroke, so the hole stays transparent on any background.
p.circle_stroke(pen.at(12.0, 12.0), (root + hole) * 0.5 * pen.s(), Stroke::new((root - hole) * pen.s(), pen.fill));
pen.closed(&outline, ink);
pen.circle(12.0, 12.0, hole, Color32::TRANSPARENT, ink);
}
// Sketch tools (sketch-style: thin outlines, accent for what the tool makes).
"canvas" | "decal" => {
if name == "canvas" {
pen.poly(&[(3.0, 5.0), (21.0, 5.0), (21.0, 19.0), (3.0, 19.0)], pen.fill, ink);
} else {
pen.poly(&[(3.0, 5.0), (21.0, 5.0), (21.0, 13.0), (15.0, 19.0), (3.0, 19.0)], pen.fill, ink);
pen.line(&[(21.0, 13.0), (15.0, 13.0), (15.0, 19.0)], ink);
}
pen.line(&[(5.0, 17.0), (9.5, 10.5), (12.5, 14.5), (14.5, 12.0), (18.0, 16.5)], a);
pen.circle(16.5, 8.5, 1.6, a, a);
}
"dxf" | "svg" => {
pen.poly(&[(5.0, 2.5), (15.0, 2.5), (19.5, 7.0), (19.5, 21.5), (5.0, 21.5)], pen.fill, ink);
pen.line(&[(15.0, 2.5), (15.0, 7.0), (19.5, 7.0)], ink);
let label = if name == "dxf" { "DXF" } else { "SVG" };
p.text(pen.at(12.2, 15.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(6.4 * pen.s()), a);
}
"c_smooth" => {
let pts: Vec<(f32, f32)> =
(0..=24).map(|i| i as f32 / 24.0).map(|t| (3.0 + 18.0 * t, 12.0 - 7.0 * (t * std::f32::consts::TAU).sin())).collect();
let (l, r) = pts.split_at(13);
pen.line(l, ink);
let mut r2 = vec![pts[12]];
r2.extend_from_slice(r);
pen.line(&r2, a);
pen.dot(12.0, 12.0, ink);
}
"c_polygon" => {
let pts: Vec<(f32, f32)> =
(0..6).map(|i| i as f32 / 6.0 * std::f32::consts::TAU).map(|t| (12.0 + 8.5 * t.cos(), 12.0 + 8.5 * t.sin())).collect();
pen.closed(&pts, a);
pen.line(&[(12.0, 12.0), (20.5, 12.0)], ink);
pen.dot(12.0, 12.0, ink);
}
"auto_constrain" | "constrainer" => {
pen.closed(&[(3.0, 8.0), (15.0, 8.0), (15.0, 20.0), (3.0, 20.0)], ink);
if name == "auto_constrain" {
for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0), (0.7, 0.7), (-0.7, -0.7), (0.7, -0.7), (-0.7, 0.7)] {
let k = if dx * dy == 0.0 { 4.0 } else { 2.5 };
pen.line(&[(18.0, 6.0), (18.0 + k * dx, 6.0 + k * dy)], a);
}
} else {
pen.line(&[(13.0, 6.0), (16.0, 9.0), (21.5, 2.5)], a);
}
pen.line(&[(6.0, 17.0), (12.0, 17.0)], a);
pen.line(&[(6.0, 11.0), (6.0, 14.0)], a);
}
"project" | "project_surface" => {
if name == "project" {
pen.poly(&[(2.0, 18.0), (12.0, 14.0), (22.0, 18.0), (12.0, 22.0)], pen.fill, ink);
pen.closed(&[(6.0, 18.0), (12.0, 15.6), (18.0, 18.0), (12.0, 20.4)], a);
} else {
pen.line(&[(2.0, 19.0), (7.0, 16.0), (12.0, 17.5), (17.0, 15.5), (22.0, 18.0)], ink);
pen.line(&[(6.0, 19.5), (12.0, 20.5), (18.0, 18.5)], a);
}
pen.iso_box(7.0, 5.0, 5.0, 5.0, 4.0);
pen.arrow((12.0, 10.5), (12.0, 15.0), a);
}
"intersect" | "intersection_curve" => {
pen.iso_box(6.0, 6.0, 7.0, 5.0, 11.0);
if name == "intersect" {
pen.line(&[(2.0, 13.0), (14.0, 8.0), (22.0, 12.0)], ink.gamma_multiply(0.6));
pen.line(&[(6.0, 13.0), (11.0, 15.5), (18.0, 12.0)], a);
} else {
pen.arc(12.0, 26.0, 13.0, 0.6, 2.55, ink.gamma_multiply(0.6));
pen.line(&[(6.0, 14.5), (11.0, 16.5), (18.0, 13.5)], a);
}
}
"include" => {
pen.iso_box(4.0, 9.0, 10.0, 7.0, 9.0);
pen.line(&[(4.0, 9.0), (11.0, 12.5), (21.0, 7.5)], a);
pen.dot(11.0, 12.5, a);
}
"spun" => {
pen.line(&[(12.0, 2.0), (12.0, 22.0)], ink);
pen.line(&[(14.0, 5.0), (18.0, 9.0), (18.0, 15.0), (14.0, 19.0)], ink);
pen.line(&[(10.0, 5.0), (6.0, 9.0), (6.0, 15.0), (10.0, 19.0)], a);
pen.arc(12.0, 12.0, 6.0, 0.3, 2.8, ink.gamma_multiply(0.6));
}
"iso_curve" => {
pen.poly(&[(3.0, 16.0), (10.0, 6.0), (21.0, 8.0), (14.0, 19.0)], pen.fill, ink);
pen.line(&[(6.5, 11.0), (17.5, 13.5)], ink.gamma_multiply(0.5));
pen.line(&[(7.7, 17.3), (14.8, 6.8)], a);
}
"fit_section" => {
pen.closed(&[(3.0, 18.0), (8.0, 6.0), (14.0, 15.0), (19.0, 5.0), (21.0, 18.0)], ink.gamma_multiply(0.6));
pen.line(&[(8.0, 6.0), (3.0, 18.0)], ink.gamma_multiply(0.6));
pen.line(&[(14.0, 15.0), (3.0, 18.0)], ink.gamma_multiply(0.6));
pen.line(&[(14.0, 15.0), (21.0, 18.0)], ink.gamma_multiply(0.6));
pen.line(&[(2.0, 12.0), (7.0, 10.5), (12.0, 12.5), (17.0, 10.0), (22.0, 11.5)], a);
}
"sketch_mirror" => {
p.add(Shape::dashed_line(&[pen.at(12.0, 2.0), pen.at(12.0, 22.0)], Stroke::new(pen.w, ink), 2.5 * pen.s(), 1.8 * pen.s()));
pen.closed(&[(3.0, 6.0), (9.0, 10.0), (9.0, 18.0), (3.0, 18.0)], ink);
pen.closed(&[(21.0, 6.0), (15.0, 10.0), (15.0, 18.0), (21.0, 18.0)], a);
}
"sketch_circ_pattern" => paint(p, r, "pattern_circ", ink, Color32::TRANSPARENT, a),
"pattern_flip" => {
// Opposing arrows show reversal, with a short reference tick between them.
pen.arrow((4.0, 7.0), (20.0, 7.0), a);
pen.arrow((20.0, 17.0), (4.0, 17.0), ink);
pen.line(&[(12.0, 10.0), (12.0, 14.0)], ink);
}
"pattern_center" => {
// An original on a reference axis, with one equally spaced copy on either side.
pen.line(&[(12.0, 3.0), (12.0, 21.0)], a);
pen.line(&[(4.0, 12.0), (20.0, 12.0)], ink);
for x in [4.0, 12.0, 20.0] {
pen.circle(x, 12.0, 2.4, fill, if x == 12.0 { a } else { ink });
}
}
"sketch_rect_pattern" => paint(p, r, "pattern_rect", ink, Color32::TRANSPARENT, a),
"line_mid" => {
pen.line(&[(3.0, 19.0), (21.0, 5.0)], a);
pen.dot(12.0, 12.0, ink);
pen.dot(21.0, 5.0, ink);
pen.circle(12.0, 12.0, 3.0, Color32::TRANSPARENT, ink);
}
"arc_tangent" => {
pen.line(&[(2.0, 18.0), (11.0, 18.0)], ink);
pen.arc(11.0, 10.0, 8.0, -std::f32::consts::FRAC_PI_2, 0.6, a);
pen.dot(11.0, 18.0, ink);
}
"circle_tt" | "circle_ttt" => {
if name == "circle_ttt" {
pen.closed(&[(3.0, 20.0), (21.0, 20.0), (12.0, 4.0)], ink);
} else {
pen.line(&[(21.0, 20.0), (3.0, 20.0), (12.0, 4.0)], ink);
}
pen.circle(12.0, 14.74, 5.26, Color32::TRANSPARENT, a);
}
"arc_slot" => {
let (t0, t1) = (0.75_f32, 2.4_f32);
pen.arc(12.0, 21.0, 13.0, t0, t1, a);
pen.arc(12.0, 21.0, 7.0, t0, t1, a);
pen.arc(12.0 + 10.0 * t0.cos(), 21.0 - 10.0 * t0.sin(), 3.0, t0, t0 - std::f32::consts::PI, a);
pen.arc(12.0 + 10.0 * t1.cos(), 21.0 - 10.0 * t1.sin(), 3.0, t1, t1 + std::f32::consts::PI, a);
pen.dot(12.0, 21.0, ink);
}
"ellipse" => {
let pts: Vec<(f32, f32)> =
(0..40).map(|i| i as f32 / 40.0 * std::f32::consts::TAU).map(|t| (12.0 + 9.5 * t.cos(), 12.0 + 5.5 * t.sin())).collect();
pen.closed(&pts, a);
pen.dot(12.0, 12.0, ink);
pen.line(&[(12.0, 12.0), (21.5, 12.0)], ink);
}
"spline" | "spline_cv" => {
let c = [(3.0_f32, 18.0_f32), (8.0, 3.0), (16.0, 21.0), (21.0, 6.0)];
let pts: Vec<(f32, f32)> = (0..=24)
.map(|i| i as f32 / 24.0)
.map(|t| {
let u = 1.0 - t;
let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
(w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum())
})
.collect();
if name == "spline_cv" {
pen.line(&c, ink.gamma_multiply(0.6));
for q in c {
pen.dot(q.0, q.1, ink);
}
} else {
for i in [0, 8, 16, 24] {
if let Some(q) = pts.get(i) {
pen.dot(q.0, q.1, ink);
}
}
}
pen.line(&pts, a);
}
"conic" => {
pen.line(&[(3.0, 20.0), (12.0, 3.0), (21.0, 20.0)], ink.gamma_multiply(0.6));
let pts: Vec<(f32, f32)> =
(0..=20).map(|i| i as f32 / 20.0).map(|t| (3.0 + 18.0 * t, 20.0 - 4.0 * 0.78 * 17.0 * t * (1.0 - t))).collect();
pen.line(&pts, a);
pen.dot(3.0, 20.0, ink);
pen.dot(21.0, 20.0, ink);
}
"text" => {
p.text(pen.at(12.0, 11.5), egui::Align2::CENTER_CENTER, "A", egui::FontId::proportional(17.0 * pen.s()), a);
pen.line(&[(4.0, 20.5), (20.0, 20.5)], ink);
}
"curvature_comb" => {
let f = |x: f32| 16.0 - 6.0 * ((x - 3.0) / 18.0 * std::f32::consts::PI).sin();
let mut env = Vec::new();
for i in 0..=8 {
let x = 3.0 + 18.0 * i as f32 / 8.0;
let k = 1.0 + 5.0 * ((x - 3.0) / 18.0 * std::f32::consts::PI).sin();
pen.line(&[(x, f(x)), (x, f(x) - k)], a.gamma_multiply(0.7));
env.push((x, f(x) - k));
}
pen.line(&env, a);
let pts: Vec<(f32, f32)> = (0..=24).map(|i| 3.0 + 18.0 * i as f32 / 24.0).map(|x| (x, f(x))).collect();
pen.line(&pts, ink);
}
"min_radius" => {
pen.line(&[(3.0, 20.0), (7.0, 9.0), (12.0, 5.5), (17.0, 9.0), (21.0, 20.0)], ink);
pen.circle(12.0, 10.5, 5.0, Color32::TRANSPARENT, a);
pen.arrow((12.0, 10.5), (15.5, 7.0), a);
pen.dot(12.0, 10.5, ink);
}
"iso_analysis" => {
pen.poly(&[(3.0, 17.0), (9.0, 5.0), (21.0, 7.0), (15.0, 19.0)], pen.fill, ink);
for k in 1..4 {
let t = k as f32 / 4.0;
pen.line(&[(3.0 + 6.0 * t, 17.0 - 12.0 * t), (15.0 + 6.0 * t, 19.0 - 12.0 * t)], a);
pen.line(&[(3.0 + 12.0 * t, 17.0 + 2.0 * t), (9.0 + 12.0 * t, 5.0 + 2.0 * t)], a);
}
}
"accessibility" => {
pen.poly(&[(2.0, 20.0), (22.0, 20.0), (22.0, 15.0), (2.0, 15.0)], ic.access[0], ink);
pen.poly(&[(8.0, 15.0), (16.0, 15.0), (16.0, 17.5), (8.0, 17.5)], ic.access[1], Color32::TRANSPARENT);
pen.poly(&[(6.0, 10.0), (18.0, 10.0), (18.0, 12.5), (6.0, 12.5)], pen.fill, ink);
pen.arrow((4.0, 2.0), (4.0, 13.0), a);
pen.arrow((20.0, 2.0), (20.0, 13.0), a);
}
"environment_map" => {
let pts: Vec<Pos2> =
(0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect();
p.add(Shape::convex_polygon(pts, ic.sky, Stroke::new(pen.w, ink)));
pen.poly(&[(3.6, 13.0), (20.4, 13.0), (18.5, 17.5), (5.5, 17.5)], ic.floor, Color32::TRANSPARENT);
pen.line(&[(3.2, 12.6), (20.8, 12.6)], ic.highlight);
p.circle_filled(pen.at(9.0, 8.0), 2.2 * pen.s(), ic.highlight);
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
}
"zebra" | "draft_analysis" | "curvature_map" => {
let pts: Vec<Pos2> =
(0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect();
match name {
"zebra" => {
p.add(Shape::convex_polygon(pts, ic.highlight, Stroke::new(pen.w, ink)));
for k in 0..3 {
let y = 6.5 + k as f32 * 4.5;
let h = (81.0 - (y + 1.0 - 12.0) * (y + 1.0 - 12.0)).max(0.0).sqrt();
pen.poly(&[(12.0 - h, y), (12.0 + h, y), (12.0 + h, y + 2.2), (12.0 - h, y + 2.2)], ic.stripe, Color32::TRANSPARENT);
}
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
}
"draft_analysis" => {
pen.poly(&[(4.0, 20.0), (20.0, 20.0), (17.0, 5.0), (7.0, 5.0)], ic.draft[0], ink);
pen.poly(&[(7.0, 5.0), (17.0, 5.0), (16.0, 9.0), (8.0, 9.0)], ic.draft[1], Color32::TRANSPARENT);
pen.arrow((12.0, 16.0), (12.0, 1.5), a);
}
_ => {
p.add(Shape::convex_polygon(pts, ic.curvature[0], Stroke::new(pen.w, ink)));
pen.circle(14.0, 10.0, 5.0, ic.curvature[1], Color32::TRANSPARENT);
pen.circle(15.5, 8.5, 2.2, ic.curvature[2], Color32::TRANSPARENT);
}
}
}
"center_of_mass" => {
pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, ink);
let q = |a0: f32| {
let pts: Vec<Pos2> = std::iter::once(pen.at(12.0, 12.0))
.chain(
(0..=8)
.map(|i| a0 + i as f32 / 8.0 * std::f32::consts::FRAC_PI_2)
.map(|t| pen.at(12.0 + 8.0 * t.cos(), 12.0 - 8.0 * t.sin())),
)
.collect();
p.add(Shape::convex_polygon(pts, a, Stroke::NONE));
};
q(0.0);
q(std::f32::consts::PI);
pen.circle(12.0, 12.0, 8.0, Color32::TRANSPARENT, ink);
}
"sketch_fillet" => {
pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink);
pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink);
pen.arc(12.0, 12.0, 8.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, a);
p.add(Shape::dashed_line(
&[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)],
Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)),
2.0 * pen.s(),
1.5 * pen.s(),
));
}
"sketch_chamfer" => {
pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink);
pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink);
pen.line(&[(4.0, 12.0), (12.0, 4.0)], a);
p.add(Shape::dashed_line(
&[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)],
Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)),
2.0 * pen.s(),
1.5 * pen.s(),
));
}
// The three chamfer types, for the Chamfer Type row of the corner tool window. Each
// draws the cut it makes: equal legs with a tick on each, two unequal legs, and one leg
// with the angle the cut leaves it at.
"sketch_chamfer_equal" => {
pen.line(&[(4.0, 21.0), (4.0, 12.0)], ink);
pen.line(&[(12.0, 4.0), (21.0, 4.0)], ink);
pen.line(&[(4.0, 12.0), (12.0, 4.0)], a);
pen.line(&[(2.5, 9.5), (5.5, 6.5)], a);
pen.line(&[(9.5, 2.5), (6.5, 5.5)], a);
p.add(Shape::dashed_line(
&[pen.at(4.0, 12.0), pen.at(4.0, 4.0), pen.at(12.0, 4.0)],
Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)),
2.0 * pen.s(),
1.5 * pen.s(),
));
}
"sketch_chamfer_two" => {
pen.line(&[(4.0, 21.0), (4.0, 10.0)], ink);
pen.line(&[(18.0, 4.0), (21.0, 4.0)], ink);
pen.line(&[(4.0, 10.0), (18.0, 4.0)], a);
p.add(Shape::dashed_line(
&[pen.at(4.0, 10.0), pen.at(4.0, 4.0), pen.at(18.0, 4.0)],
Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)),
2.0 * pen.s(),
1.5 * pen.s(),
));
}
"sketch_chamfer_angle" => {
pen.line(&[(4.0, 21.0), (4.0, 13.0)], ink);
pen.line(&[(14.0, 4.0), (21.0, 4.0)], ink);
pen.line(&[(4.0, 13.0), (14.0, 4.0)], a);
pen.arc(4.0, 13.0, 6.0, -std::f32::consts::FRAC_PI_2, 0.73, a);
p.add(Shape::dashed_line(
&[pen.at(4.0, 13.0), pen.at(4.0, 4.0), pen.at(14.0, 4.0)],
Stroke::new(pen.w * 0.7, ink.gamma_multiply(0.5)),
2.0 * pen.s(),
1.5 * pen.s(),
));
}
"offset" => {
pen.closed(&[(8.0, 9.0), (16.0, 9.0), (16.0, 15.0), (8.0, 15.0)], ink);
pen.line(&[(8.0, 4.0), (16.0, 4.0)], a);
pen.line(&[(21.0, 9.0), (21.0, 15.0)], a);
pen.line(&[(16.0, 20.0), (8.0, 20.0)], a);
pen.line(&[(3.0, 15.0), (3.0, 9.0)], a);
pen.arc(16.0, 9.0, 5.0, 0.0, std::f32::consts::FRAC_PI_2, a);
pen.arc(16.0, 15.0, 5.0, -std::f32::consts::FRAC_PI_2, 0.0, a);
pen.arc(8.0, 15.0, 5.0, std::f32::consts::PI, std::f32::consts::PI * 1.5, a);
pen.arc(8.0, 9.0, 5.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, a);
}
// Offset mode icons, drawn with the house `dashed` helper. The source is the solid
// line and each result a dashed one. One Side has a single result; Two Sides has one
// each way at two different gaps; Symmetric has one each way at the same gap.
"offset_one_side" => {
pen.line(&[(8.0, 4.0), (8.0, 20.0)], ink);
pen.dashed(&[(18.0, 4.0), (18.0, 20.0)], a);
}
"offset_two_sides" => {
pen.line(&[(13.0, 4.0), (13.0, 20.0)], ink);
pen.dashed(&[(3.0, 4.0), (3.0, 20.0)], a);
pen.dashed(&[(20.0, 4.0), (20.0, 20.0)], a);
}
"offset_symmetric" => {
pen.line(&[(12.0, 4.0), (12.0, 20.0)], ink);
pen.dashed(&[(4.0, 4.0), (4.0, 20.0)], a);
pen.dashed(&[(20.0, 4.0), (20.0, 20.0)], a);
}
"trim" => {
pen.line(&[(12.0, 3.0), (12.0, 21.0)], ink);
pen.line(&[(3.0, 12.0), (12.0, 12.0)], ink);
p.add(Shape::dashed_line(&[pen.at(12.0, 12.0), pen.at(21.0, 12.0)], Stroke::new(pen.w, a), 2.0 * pen.s(), 1.6 * pen.s()));
pen.line(&[(15.0, 15.0), (19.0, 19.0)], a);
pen.line(&[(19.0, 15.0), (15.0, 19.0)], a);
}
"patch" => {
// A wire loop with a surface spanned across it.
pen.poly(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0)], ic.sheet, ink);
pen.line(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0), (4.0, 16.0)], a);
}
"stitch" => {
// Two surface patches sewn along their shared edge.
pen.poly(&[(3.0, 18.0), (11.0, 14.0), (11.0, 4.0), (3.0, 8.0)], ic.sheet, ink);
pen.poly(&[(13.0, 14.0), (21.0, 18.0), (21.0, 8.0), (13.0, 4.0)], ic.sheet_2, ink);
for y in [6.0, 9.0, 12.0] {
pen.line(&[(10.0, y), (14.0, y + 1.5)], a);
}
}
"thicken" => {
// A wavy sheet (accent edges) grown downward into a slab, shaded like `iso_box`.
let f = [(2.0, 12.5), (7.0, 9.5), (12.0, 15.5), (17.0, 12.5)];
let b = [(6.4, 8.9), (11.4, 5.9), (16.4, 11.9), (21.4, 8.9)];
let d = f.map(|(x, y)| (x, y + 6.0));
pen.band(&bezier(b, 16), &bezier(f, 16), ic.sheet);
pen.band(&bezier(f, 16), &bezier(d, 16), pen.fill.gamma_multiply(0.85));
pen.poly(&[(17.0, 12.5), (21.4, 8.9), (21.4, 14.9), (17.0, 18.5)], pen.fill.gamma_multiply(0.7), ink);
pen.line(&[(2.0, 12.5), (2.0, 18.5)], ink);
pen.line(&[(17.0, 12.5), (17.0, 18.5)], ink);
pen.line(&[(21.4, 8.9), (21.4, 14.9)], ink);
pen.curve(d, ink);
pen.line(&[(6.4, 8.9), (2.0, 12.5)], ink);
pen.line(&[(21.4, 8.9), (17.0, 12.5)], a);
pen.curve(b, a);
pen.curve(f, a);
}
"extend" => {
pen.line(&[(20.0, 3.0), (20.0, 21.0)], ink);
pen.line(&[(3.0, 12.0), (10.0, 12.0)], ink);
pen.dot(10.0, 12.0, ink);
pen.arrow((10.0, 12.0), (19.0, 12.0), a);
}
"break" => {
pen.line(&[(3.0, 17.0), (10.5, 11.0)], ink);
pen.line(&[(13.5, 10.0), (21.0, 5.0)], a);
pen.dot(12.0, 10.5, a);
pen.line(&[(9.0, 5.0), (15.0, 16.0)], ink.gamma_multiply(0.5));
}
"scale" => {
pen.closed(&[(3.0, 13.0), (11.0, 13.0), (11.0, 21.0), (3.0, 21.0)], ink);
pen.closed(&[(3.0, 3.0), (21.0, 3.0), (21.0, 21.0), (3.0, 21.0)], a);
pen.dot(3.0, 21.0, ink);
pen.arrow((11.0, 13.0), (18.0, 6.0), a);
}
"blend" => {
pen.line(&[(2.0, 19.0), (8.0, 15.0)], ink);
pen.line(&[(16.0, 9.0), (22.0, 5.0)], ink);
pen.curve([(8.0, 15.0), (12.5, 12.0), (11.5, 12.0), (16.0, 9.0)], a);
pen.dot(8.0, 15.0, ink);
pen.dot(16.0, 9.0, ink);
}
// Surface modelling: open sheets (no caps) in the sheet colours, the input or the result
// in the accent.
"surface_extrude" => {
// An open profile (accent) pulled up into a sheet.
let top = [(4.0, 11.0), (8.0, 8.0), (13.0, 14.0), (20.0, 10.0)];
let bottom = [(4.0, 20.0), (8.0, 17.0), (13.0, 23.0), (20.0, 19.0)];
pen.band(&bezier(top, 16), &bezier(bottom, 16), ic.sheet);
pen.line(&[(4.0, 11.0), (4.0, 20.0)], ink);
pen.line(&[(20.0, 10.0), (20.0, 19.0)], ink);
pen.curve(top, ink);
pen.curve(bottom, a);
pen.arrow((12.0, 8.5), (12.0, 2.5), a);
}
"surface_revolve" => {
// An open profile (accent) turned about the axis into a hollow band.
let left = [(5.0, 6.0), (7.0, 10.0), (5.0, 14.0), (6.0, 18.0)];
let right = [(19.0, 6.0), (17.0, 10.0), (19.0, 14.0), (18.0, 18.0)];
pen.band(&bezier(left, 16), &bezier(right, 16), ic.sheet);
let front = ellipse(12.0, 18.0, 6.0, 2.2, 0.0, std::f32::consts::PI, 16);
pen.poly(&front, ic.sheet, Color32::TRANSPARENT);
// A closed fill takes each point once: a repeated end point spikes `poly`'s edge.
let mut opening = ellipse(12.0, 6.0, 7.0, 2.2, 0.0, std::f32::consts::TAU, 32);
opening.pop();
pen.poly(&opening, ic.sheet_light, ink);
pen.line(&front, ink);
pen.curve(left, ink);
pen.line(&[(12.0, 1.5), (12.0, 3.8)], ink);
pen.line(&[(12.0, 20.2), (12.0, 22.5)], ink);
pen.curve(right, a);
}
"surface_sweep" => {
// A straight profile (accent) carried along a curved path: a ribbon.
let near = [(4.0, 13.0), (8.0, 5.0), (14.0, 5.0), (20.0, 9.0)];
let far = [(4.0, 20.0), (8.0, 12.0), (14.0, 12.0), (20.0, 16.0)];
pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet);
pen.curve(near, ink);
pen.curve(far, ink);
pen.line(&[(4.0, 13.0), (4.0, 20.0)], a);
pen.line(&[(20.0, 9.0), (20.0, 16.0)], ink);
}
"surface_loft" => {
// Two open sections (accent) joined by a sheet.
let top = [(5.0, 6.0), (9.0, 3.0), (14.0, 8.0), (19.0, 5.0)];
let bottom = [(3.0, 19.0), (8.0, 15.0), (14.0, 22.0), (21.0, 18.0)];
pen.band(&bezier(top, 16), &bezier(bottom, 16), ic.sheet);
pen.line(&[(5.0, 6.0), (3.0, 19.0)], ink);
pen.line(&[(19.0, 5.0), (21.0, 18.0)], ink);
pen.curve(top, a);
pen.curve(bottom, a);
}
"surface_offset" => {
// A wavy sheet and its offset copy (dashed), the distance shown by the arrow.
let back = [(6.0, 14.0), (10.0, 11.0), (15.0, 17.0), (21.0, 14.0)];
let front = [(3.0, 20.0), (7.0, 17.0), (12.0, 23.0), (18.0, 20.0)];
pen.band(&bezier(back, 16), &bezier(front, 16), ic.sheet);
pen.curve(back, ink);
pen.curve(front, ink);
pen.line(&[(6.0, 14.0), (3.0, 20.0)], ink);
pen.line(&[(21.0, 14.0), (18.0, 20.0)], ink);
pen.dashed(&bezier(back.map(|(x, y)| (x, y - 9.0)), 16), a);
pen.dashed(&bezier(front.map(|(x, y)| (x, y - 9.0)), 16), a);
pen.dashed(&[(6.0, 5.0), (3.0, 11.0)], a);
pen.dashed(&[(21.0, 5.0), (18.0, 11.0)], a);
pen.arrow((12.0, 17.0), (12.0, 8.5), a);
}
"ruled" => {
// A strip hanging off a block's edge (accent), ruled by straight lines.
pen.poly(&[(2.0, 12.0), (12.0, 12.0), (12.0, 22.0), (2.0, 22.0)], pen.fill, ink);
pen.poly(&[(12.0, 12.0), (21.0, 4.0), (21.0, 14.0), (12.0, 22.0)], ic.sheet, ink);
pen.line(&[(15.0, 9.3), (15.0, 19.3)], a);
pen.line(&[(18.0, 6.7), (18.0, 16.7)], a);
pen.line(&[(12.0, 12.0), (12.0, 22.0)], a);
}
"boundary" => {
// Four boundary curves (accent) meeting at corners, the patch between them a Coons
// patch with its iso lines.
let (c00, c10, c11, c01) = ((4.0, 17.0), (10.0, 5.0), (20.0, 7.0), (17.0, 20.0));
let left = [c00, (5.0, 12.0), (8.0, 8.0), c10];
let top = [c10, (13.0, 4.0), (17.0, 4.0), c11];
let right = [c01, (19.0, 16.0), (20.0, 11.0), c11];
let bottom = [c00, (8.0, 20.0), (13.0, 21.0), c01];
let n = 12;
let lerp = |p: (f32, f32), q: (f32, f32), s: f32| (p.0 + (q.0 - p.0) * s, p.1 + (q.1 - p.1) * s);
let (b, t) = (bezier(bottom, n), bezier(top, n));
// Each row runs across the patch (u) at one height (v) up the left and right sides.
let rows: Vec<Vec<(f32, f32)>> = bezier(left, n)
.into_iter()
.zip(bezier(right, n))
.enumerate()
.map(|(j, (lv, rv))| {
let v = j as f32 / n as f32;
b.iter()
.zip(&t)
.enumerate()
.map(|(i, (bu, tu))| {
let u = i as f32 / n as f32;
let corners = lerp(lerp(c00, c01, u), lerp(c10, c11, u), v);
let (ruled_v, ruled_u) = (lerp(*bu, *tu, v), lerp(lv, rv, u));
(ruled_v.0 + ruled_u.0 - corners.0, ruled_v.1 + ruled_u.1 - corners.1)
})
.collect()
})
.collect();
for (lo, hi) in rows.iter().zip(rows.iter().skip(1)) {
pen.band(lo, hi, ic.sheet);
}
for j in [n / 3, 2 * n / 3] {
if let Some(r) = rows.get(j) {
pen.line(r, ic.sheet_line);
}
pen.line(&rows.iter().filter_map(|r| r.get(j).copied()).collect::<Vec<_>>(), ic.sheet_line);
}
for c in [left, top, right, bottom] {
pen.curve(c, a);
}
for (x, y) in [c00, c10, c11, c01] {
pen.dot(x, y, ink);
}
}
"surface_blend" => {
// Two separate sheets at different heights bridged by a smooth strip (accent) that
// leaves both edges tangent.
let near = [(13.0, 20.0), (8.5, 20.0), (10.5, 11.0), (6.0, 11.0)];
let far = near.map(|(x, y)| (x + 4.4, y - 3.6));
pen.poly(&[(2.0, 11.0), (6.0, 11.0), (10.4, 7.4), (6.4, 7.4)], ic.sheet, ink);
pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet_light);
pen.curve(far, a);
pen.poly(&[(13.0, 20.0), (17.0, 20.0), (21.4, 16.4), (17.4, 16.4)], ic.sheet, ink);
pen.curve(near, a);
}
"untrim" => {
// A trimmed sheet whose hole (dashed, accent) is filled back in.
pen.poly(&[(3.0, 17.0), (9.0, 5.0), (21.0, 7.0), (16.0, 20.0)], ic.sheet, ink);
let hole = ellipse(12.2, 12.3, 4.2, 3.2, 0.0, std::f32::consts::TAU, 32);
pen.poly(hole.get(1..).unwrap_or_default(), ic.sheet_light, Color32::TRANSPARENT);
pen.dashed(&hole, a);
}
"unstitch" => {
// Two sheets pulled apart from their shared edge.
pen.poly(&[(2.0, 18.0), (9.0, 14.0), (9.0, 4.0), (2.0, 8.0)], ic.sheet, ink);
pen.poly(&[(15.0, 14.0), (22.0, 18.0), (22.0, 8.0), (15.0, 4.0)], ic.sheet_2, ink);
pen.arrow((10.8, 11.0), (7.0, 11.0), a);
pen.arrow((13.2, 11.0), (17.0, 11.0), a);
}
"reverse_normal" => {
// A sheet with its old normal (faded) and the flipped one (accent).
pen.poly(&[(2.0, 14.0), (14.0, 10.0), (22.0, 13.0), (10.0, 18.0)], ic.sheet, ink);
pen.arrow((12.0, 14.0), (12.0, 3.5), ink.gamma_multiply(0.5));
pen.arrow((12.0, 14.0), (12.0, 22.5), a);
}
"surface_fillet" => {
// A wall sheet and a floor sheet joined by a round strip (accent edges).
let near = [(3.0, 16.0), (3.0, 18.8), (5.2, 21.0), (8.0, 21.0)];
let far = near.map(|(x, y)| (x + 4.4, y - 3.6));
pen.poly(&[(3.0, 16.0), (3.0, 8.0), (7.4, 4.4), (7.4, 12.4)], ic.sheet_2, ink);
pen.poly(&[(8.0, 21.0), (16.0, 21.0), (20.4, 17.4), (12.4, 17.4)], ic.sheet, ink);
pen.band(&bezier(near, 16), &bezier(far, 16), ic.sheet_light);
pen.line(&[(4.5, 19.5), (8.9, 15.9)], ic.sheet_line);
pen.curve(far, a);
pen.curve(near, a);
}
"continuity" => {
// A straight curve joining a curved one (accent) at a point, with the curved one's
// curvature comb: teeth that grow as the curve tightens.
pen.line(&[(2.0, 17.0), (12.0, 13.0)], ink);
let c = [(12.0, 13.0), (16.5, 11.2), (20.0, 8.0), (21.0, 3.0)];
let pts = bezier(c, 10);
let mut tips = Vec::new();
for (i, ((x, y), (x1, y1))) in pts.iter().zip(pts.iter().skip(1)).enumerate() {
let len = (x1 - x).hypot(y1 - y).max(1e-6);
let h = 0.8 + 4.2 * i as f32 / 10.0;
let tip = (x + (y1 - y) / len * h, y - (x1 - x) / len * h);
if i % 2 == 0 {
pen.line(&[(*x, *y), tip], ic.sheet_line);
}
tips.push(tip);
}
pen.line(&tips, ic.sheet_line);
pen.curve(c, a);
pen.dot(12.0, 13.0, a);
}
// Assembly: joints (accent: the joint itself).
"joint" => {
pen.iso_box(2.0, 13.0, 7.0, 5.0, 5.0);
pen.iso_box(12.0, 4.0, 7.0, 5.0, 5.0);
pen.line(&[(9.0, 15.0), (15.0, 9.0)], a);
pen.circle(12.0, 12.0, 2.6, pen.fill, a);
}
"as_built" => {
pen.iso_box(1.0, 10.0, 8.0, 5.0, 8.0);
pen.iso_box(10.0, 10.0, 8.0, 5.0, 8.0);
pen.circle(13.0, 15.0, 2.6, pen.fill, a);
}
"joint_origin" => {
pen.arrow((8.0, 17.0), (20.0, 17.0), ink);
pen.arrow((8.0, 17.0), (14.0, 11.0), ink);
pen.arrow((8.0, 17.0), (8.0, 3.0), a);
pen.circle(8.0, 17.0, 2.4, pen.fill, a);
}
"rigid_group" => {
pen.iso_box(3.0, 13.0, 5.0, 3.0, 4.0);
pen.iso_box(13.0, 13.0, 5.0, 3.0, 4.0);
pen.iso_box(8.0, 4.0, 5.0, 3.0, 4.0);
pen.closed(&[(2.0, 3.0), (22.0, 3.0), (22.0, 21.0), (2.0, 21.0)], a);
}
"drive" => {
pen.circle(12.0, 12.0, 4.0, pen.fill, ink);
pen.dot(12.0, 12.0, ink);
pen.arc(12.0, 12.0, 8.5, 0.3, 4.6, a);
pen.arrow((11.0, 20.4), (14.5, 20.3), a);
}
// A crank on its pivot and the closed path its far end draws (accent).
"trace" => {
pen.curve([(15.0, 13.0), (22.0, 13.0), (23.0, 3.0), (15.0, 3.0)], a);
pen.curve([(15.0, 3.0), (8.0, 3.0), (9.0, 13.0), (15.0, 13.0)], a);
pen.line(&[(5.0, 20.0), (15.0, 13.0)], ink);
pen.circle(5.0, 20.0, 2.4, pen.fill, ink);
pen.dot(15.0, 13.0, a);
}
"motion_link" => {
pen.circle(7.0, 14.0, 4.5, pen.fill, ink);
pen.circle(17.5, 9.0, 3.0, pen.fill, ink);
pen.line(&[(7.0, 14.0), (17.5, 9.0)], a);
pen.dot(7.0, 14.0, a);
pen.dot(17.5, 9.0, a);
}
// Couplings: the driven part in the accent.
"gear" => {
pen.circle(9.0, 14.0, 5.2, pen.fill, Color32::TRANSPARENT);
pen.closed(&teeth(9.0, 14.0, 5.2, 6.8, 9, 0.0), ink);
pen.dot(9.0, 14.0, ink);
pen.circle(17.6, 7.4, 3.2, pen.fill, Color32::TRANSPARENT);
pen.closed(&teeth(17.6, 7.4, 3.2, 4.6, 6, 0.3), a);
pen.dot(17.6, 7.4, a);
}
"rack_pinion" => {
pen.circle(12.0, 7.4, 4.2, pen.fill, Color32::TRANSPARENT);
pen.closed(&teeth(12.0, 7.4, 4.2, 5.8, 8, 0.2), ink);
pen.dot(12.0, 7.4, ink);
let mut rack = vec![(2.0, 18.0)];
for k in 0..5 {
let x = 2.5 + 4.0 * k as f32;
rack.extend([(x, 14.3), (x + 1.0, 12.8), (x + 2.2, 12.8), (x + 3.2, 14.3)]);
}
rack.extend([(22.0, 14.3), (22.0, 18.0)]);
pen.closed(&rack, a);
pen.arrow((7.0, 21.5), (17.5, 21.5), a);
}
"screw" => {
pen.poly(&[(9.5, 2.0), (14.5, 2.0), (14.5, 22.0), (9.5, 22.0)], pen.fill, ink);
for y in [5.0, 8.0, 17.0, 20.0] {
pen.line(&[(9.5, y), (14.5, y - 2.0)], ink);
}
pen.poly(&[(6.0, 10.0), (18.0, 10.0), (18.0, 15.0), (6.0, 15.0)], pen.fill, a);
pen.arrow((21.0, 17.0), (21.0, 6.0), a);
}
"interference" => {
pen.closed(&[(3.0, 5.0), (14.0, 5.0), (14.0, 16.0), (3.0, 16.0)], ink);
pen.poly(&[(9.0, 10.0), (14.0, 10.0), (14.0, 16.0), (9.0, 16.0)], ic.interference, ic.interference);
pen.closed(&[(9.0, 10.0), (21.0, 10.0), (21.0, 20.0), (9.0, 20.0)], ink);
}
// Sheet metal: thin plates (fill), the made or changed part in the accent.
"flange" => {
pen.poly(&[(2.0, 17.0), (12.0, 13.0), (18.0, 16.0), (8.0, 20.0)], pen.fill, ink);
pen.poly(&[(12.0, 13.0), (12.0, 5.0), (18.0, 8.0), (18.0, 16.0)], pen.fill.gamma_multiply(0.8), a);
pen.arrow((20.5, 15.0), (20.5, 4.0), a);
}
"hem" => {
pen.line(&[(2.0, 16.0), (16.0, 16.0)], ink);
pen.line(&[(2.0, 13.0), (16.0, 13.0)], ink);
pen.arc(16.0, 11.5, 4.5, -1.57, 1.57, a);
pen.line(&[(16.0, 7.0), (8.0, 7.0)], a);
}
// Fastener: a socket head screw seen from the side.
"fastener" => {
pen.poly(&[(6.0, 3.0), (18.0, 3.0), (18.0, 9.0), (6.0, 9.0)], pen.fill, ink);
pen.line(&[(10.0, 3.0), (10.0, 6.0), (14.0, 6.0), (14.0, 3.0)], a);
pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 21.0), (9.0, 21.0)], pen.fill.gamma_multiply(0.85), ink);
for y in [12.0, 15.0, 18.0] {
pen.line(&[(9.0, y), (15.0, y + 1.5)], ink);
}
}
"fold" => {
pen.line(&[(2.0, 18.0), (12.0, 18.0)], ink);
pen.line(&[(12.0, 18.0), (19.0, 6.0)], a);
pen.line(&[(9.0, 21.0), (15.0, 15.0)], ic.fold);
pen.arc(12.0, 18.0, 6.0, 0.2, 1.0, a);
pen.arrow((18.0, 16.0), (17.0, 12.5), a);
}
"unfold" => {
pen.line(&[(3.0, 18.0), (12.0, 18.0), (12.0, 7.0)], ink);
pen.line(&[(12.0, 18.0), (22.0, 18.0)], a);
pen.arc(12.0, 18.0, 7.0, 0.3, 1.4, a);
pen.arrow((17.5, 14.0), (19.0, 16.0), a);
}
"refold" => {
pen.line(&[(3.0, 18.0), (12.0, 18.0), (21.0, 18.0)], ink);
pen.line(&[(12.0, 18.0), (12.0, 6.0)], a);
pen.arc(12.0, 18.0, 7.0, 0.3, 1.4, a);
pen.arrow((15.0, 11.5), (13.5, 10.5), a);
}
"flat" => {
pen.poly(&[(3.0, 6.0), (21.0, 6.0), (21.0, 18.0), (3.0, 18.0)], pen.fill, ink);
for x in [9.0, 15.0] {
for k in 0..3 {
let y = 7.5 + k as f32 * 4.0;
pen.line(&[(x, y), (x, y + 2.2)], a);
}
}
}
"convert_sheet" => {
pen.iso_box(2.0, 9.0, 6.0, 4.0, 6.0);
pen.arrow((11.0, 12.0), (15.0, 12.0), a);
pen.poly(&[(15.0, 15.0), (21.0, 12.0), (23.0, 13.0), (17.0, 16.0)], pen.fill, a);
}
"sheet_rules" => {
pen.poly(&[(2.0, 15.0), (11.0, 11.0), (17.0, 14.0), (8.0, 18.0)], pen.fill, ink);
for y in [5.0, 9.0, 13.0] {
pen.line(&[(14.0, y), (22.0, y)], a);
}
}
// Plastic: a wall or plate (fill) with the feature in the accent.
"boss" => {
pen.poly(&[(2.0, 16.0), (13.0, 12.0), (22.0, 16.0), (11.0, 20.0)], pen.fill, ink);
pen.poly(&[(9.0, 7.0), (15.0, 7.0), (15.0, 16.0), (9.0, 16.0)], pen.fill.gamma_multiply(0.85), a);
pen.circle(12.0, 7.0, 3.0, pen.fill, a);
pen.circle(12.0, 7.0, 1.2, ink, ink);
}
"lip" => {
pen.poly(&[(4.0, 21.0), (4.0, 9.0), (10.0, 9.0), (10.0, 21.0)], pen.fill, ink);
pen.poly(&[(4.0, 9.0), (4.0, 4.0), (7.0, 4.0), (7.0, 9.0)], pen.fill, a);
pen.poly(&[(14.0, 21.0), (14.0, 6.0), (20.0, 6.0), (20.0, 21.0)], pen.fill, ink);
pen.closed(&[(17.0, 6.0), (17.0, 11.0), (20.0, 11.0), (20.0, 6.0)], a);
}
"snap" => {
pen.poly(&[(2.0, 20.0), (2.0, 16.0), (22.0, 16.0), (22.0, 20.0)], pen.fill, ink);
pen.poly(&[(9.0, 16.0), (9.0, 4.0), (12.0, 4.0), (12.0, 16.0)], pen.fill, a);
pen.poly(&[(12.0, 4.0), (16.0, 8.0), (12.0, 8.0)], a, a);
}
"rest" => {
pen.poly(&[(2.0, 16.0), (13.0, 12.0), (22.0, 16.0), (11.0, 20.0)], pen.fill, ink);
pen.poly(&[(7.0, 14.0), (13.0, 11.5), (17.0, 13.5), (11.0, 16.0)], a, a);
pen.line(&[(7.0, 14.0), (7.0, 11.5), (13.0, 9.0), (17.0, 11.0), (17.0, 13.5)], a);
}
"plastic_rule" => {
pen.circle(7.0, 8.0, 3.2, pen.fill, a);
pen.circle(11.0, 13.0, 3.2, pen.fill, a);
pen.circle(5.5, 16.0, 3.2, pen.fill, a);
for y in [6.0, 11.0, 16.0] {
pen.line(&[(15.0, y), (22.0, y)], ink);
}
}
"plastic_assign" => {
pen.iso_box(2.0, 12.0, 7.0, 5.0, 6.0);
pen.arrow((19.0, 6.0), (13.0, 11.0), a);
pen.circle(19.0, 6.0, 3.0, pen.fill, a);
}
// Appearance: a shaded ball with three paint dabs.
"appearance" => {
pen.circle(10.0, 13.0, 7.5, pen.fill, ink);
pen.arc(10.0, 13.0, 4.5, 1.8, 3.2, ic.highlight);
pen.circle(18.0, 5.0, 2.6, ic.paint[0], ink);
pen.circle(20.5, 11.0, 2.6, ic.paint[1], ink);
pen.circle(19.0, 17.5, 2.6, ic.paint[2], ink);
}
"warning" => {
pen.poly(&[(12.0, 3.0), (21.0, 20.0), (3.0, 20.0)], ic.warning, ink);
pen.line(&[(12.0, 9.0), (12.0, 14.0)], ink);
pen.dot(12.0, 17.0, ink);
}
// A failed feature: a disc with a cross, a different shape from the warning's triangle, so
// it reads without colour (#972). Drawn in the caller's colours only.
"error" => {
pen.circle(12.0, 12.0, 10.0, pen.fill, ink);
pen.line(&[(8.0, 8.0), (16.0, 16.0)], ink);
pen.line(&[(16.0, 8.0), (8.0, 16.0)], ink);
}
_ => {
pen.poly(&[(5.0, 5.0), (19.0, 5.0), (19.0, 19.0), (5.0, 19.0)], pen.fill, ink);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A debug dump of everything `paint` draws for `name`, for comparing two colour choices
/// without depending on epaint's internal shape fields.
fn drawn(name: &str, ink: Color32, fill: Color32, accent: Color32) -> String {
let ctx = egui::Context::default();
let screen = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0));
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
let out = ctx.run_ui(input, |ui| {
let r = Rect::from_min_size(Pos2::ZERO, vec2(24.0, 24.0));
paint(ui.painter(), r, name, ink, fill, accent);
});
format!("{:?}", out.shapes)
}
/// Command icons not drawn yet (the fallback square) or drawn past their square, from before
/// this check. Remove a name when its icon is fixed; never add one.
const KNOWN_GAPS: &[&str] =
&["cylinder", "pipe", "offset_face", "thread", "boundary_fill", "coil", "rib", "emboss", "capture", "revert", "camera", "material"];
/// What is wrong with the icon `name`: not drawn (the fallback square), or drawn past its
/// square by more than a stroke width.
fn icon_problem(name: &str) -> Option<String> {
if drawn(name, Color32::BLACK, Color32::WHITE, Color32::BLUE) == drawn("", Color32::BLACK, Color32::WHITE, Color32::BLUE) {
return Some("no icon drawn".into());
}
let side = 48.0;
let r = Rect::from_min_size(Pos2::ZERO, vec2(side, side));
let ctx = egui::Context::default();
let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0))), ..Default::default() };
let out = ctx.run_ui(input, |ui| paint(ui.painter(), r, name, Color32::BLACK, Color32::WHITE, Color32::BLUE));
let used = out.shapes.iter().fold(Rect::NOTHING, |b, s| b.union(s.shape.visual_bounding_rect()));
(!r.expand(side / 24.0 * 1.4).contains_rect(used)).then(|| format!("drawn over {used:?}"))
}
/// Every command's icon is drawn and stays inside its square.
#[test]
fn every_command_icon_is_drawn_inside_its_square() {
let bad: Vec<String> = solidcraft_engine::command_specs()
.into_iter()
.filter(|c| !c.icon.is_empty() && !KNOWN_GAPS.contains(&c.icon))
.filter_map(|c| icon_problem(c.icon).map(|why| format!("{} ({}): {why}", c.id, c.icon)))
.collect();
assert!(bad.is_empty(), "{}", bad.join("\n"));
for gap in KNOWN_GAPS {
assert!(icon_problem(gap).is_some(), "`{gap}` is fixed now: take it off KNOWN_GAPS");
}
}
/// The Sketch Palette and the toolbar both rely on this contract. Construction and
/// centerline must track whatever accent colour the caller passes (both callers pass the
/// theme's construction orange) and must not change with `ink`.
#[test]
fn construction_and_centerline_icons_follow_the_callers_accent_only() {
for name in ["construction", "centerline", "infinite"] {
let base = drawn(name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(1, 2, 3));
let other_accent = drawn(name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(4, 5, 6));
assert_ne!(base, other_accent, "{name} icon must change with the caller's accent colour");
let other_ink = drawn(name, Color32::from_rgb(9, 9, 9), Color32::WHITE, Color32::from_rgb(1, 2, 3));
assert_eq!(base, other_ink, "{name} icon has no ink-coloured parts: ink alone must not change it");
}
}
/// Normal is a plain line type. It is never construction geometry. Its icon must never pick
/// up the accent colour. It can never render construction orange even if a caller passes it
/// by mistake.
#[test]
fn normal_icon_never_uses_the_accent_colour() {
let one_accent = drawn("normal", Color32::BLACK, Color32::WHITE, Color32::from_rgb(1, 2, 3));
let other_accent = drawn("normal", Color32::BLACK, Color32::WHITE, Color32::from_rgb(4, 5, 6));
assert_eq!(one_accent, other_accent, "the Normal icon must not change with the accent colour");
let other_ink = drawn("normal", Color32::from_rgb(9, 9, 9), Color32::WHITE, Color32::from_rgb(1, 2, 3));
assert_ne!(one_accent, other_ink, "the Normal icon must change with ink");
}
/// The dashes (as their first and last point) and the dots (as centre and radius) `paint`
/// actually drew for `name`, at `side` pixels square: the Sketch Palette draws these three
/// icons down to about 18 px.
fn dashes_and_dots(name: &str, side: f32) -> (Vec<(Pos2, Pos2)>, Vec<(Pos2, f32)>) {
let ctx = egui::Context::default();
let screen = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0));
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
let r = Rect::from_min_size(Pos2::ZERO, vec2(side, side));
let out = ctx.run_ui(input, |ui| {
paint(ui.painter(), r, name, Color32::BLACK, Color32::WHITE, Color32::from_rgb(232, 140, 48));
});
let dashes = out
.shapes
.iter()
.filter_map(|s| match &s.shape {
Shape::Path(p) => Some((*p.points.first()?, *p.points.last()?)),
_ => None,
})
.collect();
let dots = out.shapes.iter().filter_map(|s| if let Shape::Circle(c) = &s.shape { Some((c.center, c.radius)) } else { None }).collect();
(dashes, dots)
}
/// Pins the dash count and the gap. A future tidy-up might add dashes back in. It might
/// narrow the gap to look more faithful to the ISO convention. Either change would flatten
/// the icon back into a near-solid line at the size the Sketch Palette actually draws it,
/// about 18 px square. That is the exact defect this fix addressed.
#[test]
fn construction_and_centerline_read_as_two_dashes_with_a_gap_wider_than_the_stroke() {
for (name, want_dots) in [("construction", 0), ("centerline", 1)] {
for side in [18.0, 26.0] {
let (dashes, dots) = dashes_and_dots(name, side);
assert_eq!(dashes.len(), 2, "{name} at {side}px must be exactly two dashes");
assert_eq!(dots.len(), want_dots, "{name} at {side}px has the wrong number of dots");
let (_, gap_start) = dashes[0];
let (gap_end, _) = dashes[1];
let gap = gap_start.distance(gap_end);
let stroke = line_type_stroke(Rect::from_min_size(Pos2::ZERO, vec2(side, side)));
assert!(
gap > stroke * 2.0,
"{name} at {side}px: the gap of {gap:.1}px must exceed the {stroke:.1}px stroke by a wide margin, or the dashes read as one solid line"
);
}
}
}
/// Pins the shape of the Centerline dot. It must be large enough to read as a round mark
/// beside the stroke. It must sit clear of both dashes. The silhouette must stay line, dot,
/// line.
#[test]
fn centerline_dot_is_a_bold_mark_clear_of_both_dashes() {
for side in [18.0, 26.0] {
let (dashes, dots) = dashes_and_dots("centerline", side);
let (center, radius) = dots[0];
let stroke = line_type_stroke(Rect::from_min_size(Pos2::ZERO, vec2(side, side)));
assert!(radius > stroke * 1.2, "centerline dot at {side}px: radius {radius:.1}px must exceed the {stroke:.1}px stroke by a wide margin");
for (a, b) in &dashes {
assert!(center.distance(*a) > radius, "centerline dot at {side}px must not touch a dash end");
assert!(center.distance(*b) > radius, "centerline dot at {side}px must not touch a dash end");
}
}
}
}