Collect application resources for #433 (#833)

This commit is contained in:
Nathan Dane authored and GitHub committed 2026-10-14 00:49:32 -04:00
1 parent d5ff5ea1a5
commit 9f76cca8b4
107 files changed
+2940 -2660

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
//! Collected glyph artwork. Caption geometry adapted from VectorCraft (MIT OR Apache-2.0).
use crate::titlebar::Caption;
use egui::{Color32, Pos2, Rect, Stroke, Ui, pos2, vec2};
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
let tri = |dir: f32, dx: f32| {
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
};
let bar = |dx: f32| p.line_segment([pos2(c.x + dx, c.y - 5.0), pos2(c.x + dx, c.y + 5.0)], Stroke::new(1.6, col));
match what {
"first" => {
bar(-5.0);
tri(-1.0, 1.0);
}
"back" => tri(-1.0, 0.0),
"forward" => tri(1.0, 0.0),
_ => {
tri(1.0, -1.0);
bar(5.0);
}
}
}
pub(crate) fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
let p = ui.painter();
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
match c {
Caption::Minimize => {
p.line_segment([g.left_center(), g.right_center()], s);
}
// Restore: a front square with the corner of the one behind it.
Caption::Maximize if maximized => {
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
}
Caption::Maximize => {
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
}
Caption::Close => {
p.line_segment([g.left_top(), g.right_bottom()], s);
p.line_segment([g.right_top(), g.left_bottom()], s);
}
}
}
pub(crate) fn badge(p: &egui::Painter, r: Rect, name: &str, c: Color32) {
let s = Stroke::new(1.3, c);
match name {
"lock" => {
let body = Rect::from_min_max(pos2(r.left() + 1.5, r.center().y - 0.5), pos2(r.right() - 1.5, r.bottom() - 0.5));
p.rect_filled(body, 1.5, c);
let top = pos2(r.center().x, r.top() + 4.0);
p.add(egui::Shape::line(
(0..=8)
.map(|k| {
let a = std::f32::consts::PI * (k as f32 / 8.0);
pos2(top.x - 3.2 * a.cos(), top.y - 3.2 * a.sin() + 1.0)
})
.chain([pos2(top.x + 3.2, body.top())])
.collect(),
s,
));
p.line_segment([pos2(top.x - 3.2, top.y + 1.0), pos2(top.x - 3.2, body.top())], s);
}
"pin" => {
let head = pos2(r.center().x + 2.0, r.top() + 4.0);
p.circle_filled(head, 3.2, c);
p.line_segment([head, pos2(r.left() + 1.5, r.bottom() - 1.0)], Stroke::new(1.6, c));
}
_ => {}
}
}
pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
let c = r.center();
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
// Local coordinates: the pin points down, its head on top.
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
let stroke = Stroke::new(1.4, col);
if on {
p.add(egui::Shape::convex_polygon(head, col, stroke));
} else {
p.add(egui::Shape::closed_line(head, stroke));
}
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
}
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
let s = 20.0 / 412.217;
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
// 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,
));
}
pub(crate) fn cross(p: &egui::Painter, r: Rect, s: Stroke) {
p.line_segment([r.left_top(), r.right_bottom()], s);
p.line_segment([r.right_top(), r.left_bottom()], s);
}
pub(crate) fn radial_centre(p: &egui::Painter, c: Pos2, direction: Option<(egui::Vec2, bool)>, t: crate::theme::Tokens) {
p.circle(c, 15.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, t.border));
if let Some((d, enabled)) = direction {
p.line_segment([c + d * 6.0, c + d * 15.0], Stroke::new(3.0, if enabled { t.accent } else { t.border }));
} else {
p.circle_filled(c, 3.0, t.text_dim);
}
}
pub(crate) fn disclosure(p: &egui::Painter, ar: Rect, open: bool, ink: Color32) {
let pts = if open {
vec![pos2(ar.left() + 2.0, ar.top() + 4.0), pos2(ar.right() - 2.0, ar.top() + 4.0), pos2(ar.center().x, ar.bottom() - 3.0)]
} else {
vec![pos2(ar.left() + 4.0, ar.top() + 2.0), pos2(ar.right() - 3.0, ar.center().y), pos2(ar.left() + 4.0, ar.bottom() - 2.0)]
};
p.add(egui::Shape::convex_polygon(pts, ink, Stroke::NONE));
}
File diff suppressed because it is too large. Load diff
+86
View File
@@ -0,0 +1,86 @@
//! Constraint, assembly snap and dimension marker artwork.
use crate::dialogs_assembly::SnapKind;
use crate::theme::Tokens;
use egui::{Color32, Pos2, Stroke, vec2};
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
let halo = Stroke::new(4.5, Color32::from_black_alpha(150));
let white = Stroke::new(1.5, Color32::WHITE);
if let Some(tip) = tip {
let d = (tip - c).normalized();
let n = vec2(-d.y, d.x);
let head = vec![tip + d * 2.0, tip - d * 9.0 + n * 5.0, tip - d * 9.0 - n * 5.0];
painter.line_segment([c, tip], halo);
painter.add(egui::Shape::convex_polygon(head.clone(), Color32::TRANSPARENT, halo));
painter.line_segment([c, tip], Stroke::new(2.0, col));
painter.add(egui::Shape::convex_polygon(head, col, Stroke::NONE));
}
let shape = |fill: Color32, stroke: Stroke, grow: f32| -> egui::Shape {
match kind {
SnapKind::FaceCenter => egui::Shape::rect_filled(egui::Rect::from_center_size(c, vec2(12.0 + grow, 12.0 + grow)), 1.5, fill),
SnapKind::CircleCenter => egui::Shape::circle_filled(c, 7.0 + grow / 2.0, fill),
SnapKind::EdgeMid => {
let r = 8.0 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r * 0.93, r * 0.7), c + vec2(-r * 0.93, r * 0.7)], fill, stroke)
}
SnapKind::Vertex => {
let r = 7.5 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)], fill, stroke)
}
}
};
painter.add(shape(Color32::from_black_alpha(150), Stroke::NONE, 4.0));
painter.add(shape(col, Stroke::NONE, 0.0));
match kind {
SnapKind::FaceCenter => {
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(12.0, 12.0)), 1.5, white, egui::StrokeKind::Middle);
}
SnapKind::CircleCenter => {
painter.circle_stroke(c, 7.0, white);
painter.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], Stroke::new(1.2, Color32::WHITE));
painter.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], Stroke::new(1.2, Color32::WHITE));
}
SnapKind::EdgeMid | SnapKind::Vertex => {
painter.add(shape(Color32::TRANSPARENT, white, 0.0));
}
}
// What it snaps to (hovered), or which component it is (picked): a small label.
if let Some(tag) = tag {
let t = Tokens::get();
let font = egui::FontId::proportional(11.5);
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
let at = c + vec2(12.0, 10.0);
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
painter.rect(r, 3.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, col), egui::StrokeKind::Inside);
painter.galley(at, g, t.text);
}
}
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
Some(match name {
"Coincident" => "c_coincident",
"Parallel" => "c_parallel",
"Perpendicular" => "c_perpendicular",
"Collinear" => "c_collinear",
"Tangent" => "c_tangent",
"Smooth" => "c_smooth",
"Equal" => "c_equal",
"Concentric" => "c_concentric",
"MidPoint" => "c_midpoint",
"Symmetry" => "c_symmetry",
"Fix" => "c_fix",
_ => return None,
})
}
pub(crate) fn glyph_letter(name: &str) -> &'static str {
match name {
"Horizontal" => "H",
"Vertical" => "V",
_ => "?",
}
}
pub(crate) fn dimension_arrow(painter: &egui::Painter, t: Pos2, back: Pos2, col: Color32) {
let d = (t - back).normalized();
let n = vec2(-d.y, d.x);
painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE));
}
+167
View File
@@ -0,0 +1,167 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Align2, Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcraft_engine::geom::Vec3;
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Vec<Pos2>,
pub(crate) outer_edges: Vec<bool>,
pub(crate) centre: Pos2,
pub(crate) label: String,
pub(crate) text_axes: Option<(egui::Vec2, egui::Vec2)>,
pub(crate) depth: f64,
}
// Private marker for label meshes with atlas-pixel UVs. The output hook replaces
// it with the font texture before any renderer sees the frame.
pub(crate) const LABEL_PIXELS: egui::TextureId = egui::TextureId::User(u64::from_le_bytes(*b"cubetext"));
struct LabelAtlas;
impl egui::plugin::Plugin for LabelAtlas {
fn debug_name(&self) -> &'static str {
"navigation cube label atlas"
}
fn output_hook(&mut self, ctx: &egui::Context, output: &mut egui::FullOutput) {
let size = ctx.fonts(|f| f.font_image_size());
let normalize = vec2(1.0 / size[0].max(1) as f32, 1.0 / size[1].max(1) as f32);
for shape in &mut output.shapes {
if let Shape::Mesh(mesh) = &mut shape.shape
&& mesh.texture_id == LABEL_PIXELS
{
let mesh = std::sync::Arc::make_mut(mesh);
mesh.texture_id = egui::TextureId::default();
for v in &mut mesh.vertices {
v.uv = (v.uv.to_vec2() * normalize).to_pos2();
}
}
}
}
}
/// Warp the font mesh into the projected face, so labels turn with the model.
pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, font_size: f32) {
let Some((r, down)) = face.text_axes else { return };
let opacity = ((face.depth - 0.18) / 0.17).clamp(0.0, 1.0) as f32;
if opacity <= 0.0 {
return;
}
let ink = ink.gamma_multiply(opacity);
let galley = painter.layout_no_wrap(face.label.clone(), FontId::proportional(font_size), ink);
let centre = galley.rect.center();
// Keep atlas-pixel UVs until the frame is complete: another label or widget
// can grow the atlas after this mesh is created, invalidating early UVs.
painter.ctx().add_plugin(LabelAtlas);
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
let atlas = painter.ctx().fonts(|f| f.font_image_size());
let primitives =
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
for primitive in primitives {
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
mesh.texture_id = LABEL_PIXELS;
for v in &mut mesh.vertices {
v.uv = pos2(v.uv.x * atlas[0] as f32, v.uv.y * atlas[1] as f32);
let local = v.pos - centre;
v.pos = face.centre + r * local.x + down * (local.y * 1.35);
}
painter.add(Shape::mesh(mesh));
}
}
}
pub(crate) fn paint_cube(painter: &egui::Painter, faces: &[Facet], hovered: Option<usize>, t: Tokens, labels: bool, scale: f32) {
// A shared, opaque triangle mesh has no antialiasing fringes between facets.
// Per-polygon fringes and miter joins overlap as a face becomes edge-on.
let mut fill_mesh = egui::epaint::Mesh::default();
let mut edges: Vec<(Pos2, Pos2, usize, bool)> = Vec::new();
let hovered_direction = hovered.and_then(|i| faces.get(i)).map(|f| f.direction);
for face in faces {
let base = t.cube_face;
let shade = (0.72 + 0.28 * face.depth.min(1.0)) as f32;
let highlighted = hovered_direction == Some(face.direction);
let fill = if highlighted {
t.cube_hover
} else {
Color32::from_rgb((f32::from(base.r()) * shade) as u8, (f32::from(base.g()) * shade) as u8, (f32::from(base.b()) * shade) as u8)
};
let centre = fill_mesh.vertices.len() as u32;
fill_mesh.colored_vertex(face.centre, fill);
for &p in &face.points {
fill_mesh.colored_vertex(p, fill);
}
for (j, (&a, &b)) in face.points.iter().zip(face.points.iter().cycle().skip(1)).take(face.points.len()).enumerate() {
fill_mesh.add_triangle(centre, centre + 1 + j as u32, centre + 1 + ((j + 1) % face.points.len()) as u32);
// Coplanar picking regions have no visible seams. Only actual cube
// edges and the hovered target's boundary receive an outline.
let outer = face.outer_edges.get(j).copied().unwrap_or(false);
if !highlighted && !outer {
continue;
}
if let Some(edge) = edges
.iter_mut()
.find(|(p, q, _, _)| (p.distance_sq(a) < 1e-6 && q.distance_sq(b) < 1e-6) || (p.distance_sq(b) < 1e-6 && q.distance_sq(a) < 1e-6))
{
edge.2 += usize::from(highlighted);
edge.3 |= outer;
} else {
edges.push((a, b, usize::from(highlighted), outer));
}
}
}
painter.add(Shape::mesh(fill_mesh));
for (a, b, highlights, outer) in edges {
// Two highlighted coplanar patches share an internal subdivision, not
// a target boundary. Actual cube edges remain visible across faces.
if !outer && highlights != 1 {
continue;
}
let highlighted = highlights > 0;
painter.line_segment([a, b], Stroke::new(1.0, if highlighted { t.accent } else { t.cube_border }));
}
if labels {
for face in faces {
face_text(painter, face, t.cube_text, 14.0 * scale);
}
}
}
pub(crate) fn arrow(painter: &egui::Painter, at: Pos2, direction: egui::Vec2, hover: bool, t: Tokens) {
let side = vec2(-direction.y, direction.x);
painter.add(Shape::convex_polygon(
vec![at + direction * 7.0, at - direction * 5.0 + side * 7.0, at - direction * 5.0 - side * 7.0],
if hover { t.accent_soft } else { t.cube_face },
Stroke::new(1.0, if hover { t.accent } else { t.cube_border }),
));
}
pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, hover: bool, t: Tokens, scale: f32) {
let (start, end) =
if clockwise { (-std::f32::consts::FRAC_PI_4 + 0.12, 0.0) } else { (-std::f32::consts::FRAC_PI_4 - 0.12, -std::f32::consts::FRAC_PI_2) };
let points: Vec<_> = (0..=16)
.map(|i| {
let angle = egui::lerp(start..=end, f32::from(i as u8) / 16.0);
at + vec2(angle.cos(), angle.sin()) * (32.0 * scale)
})
.collect();
painter.add(Shape::line(points.clone(), Stroke::new(5.0 * scale, if hover { t.accent } else { t.cube_border })));
painter.add(Shape::line(points.clone(), Stroke::new(3.0 * scale, if hover { t.accent_soft } else { t.cube_face })));
if let Some(tip) = points.last() {
let sign = if clockwise { 1.0 } else { -1.0 };
arrow(painter, *tip, vec2(-end.sin(), end.cos()) * (sign * scale), hover, t);
}
}
pub(crate) const AXIS_RADIUS: f32 = 11.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
let col = if d.x != 0.0 {
solidcraft_engine::view::colors::AXIS_X
} else if d.y != 0.0 {
solidcraft_engine::view::colors::AXIS_Y
} else {
solidcraft_engine::view::colors::AXIS_Z
};
let col = Color32::from_rgb(col.0, col.1, col.2);
let positive = !name.starts_with('-');
let ink = if depth < -0.01 { col.gamma_multiply(0.55) } else { col };
painter.line_segment([c, at], Stroke::new(if positive { 2.0 } else { 1.0 }, ink));
painter.circle_filled(at, AXIS_RADIUS, if hovered { t.accent_soft } else { t.overlay });
painter.circle_stroke(at, AXIS_RADIUS, Stroke::new(if hovered { 2.0 } else { 1.0 }, if hovered { t.accent } else { ink }));
painter.text(at, Align2::CENTER_CENTER, name.to_uppercase(), FontId::proportional(11.0), ink);
}
+14
View File
@@ -0,0 +1,14 @@
//! Original agent pointer and label. Animation stays with the UI controller.
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
pub(crate) const AGENT: Color32 = Color32::from_rgb(168, 85, 247);
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
// An arrow pointer, tip at `p`.
let pts: Vec<Pos2> =
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
painter.add(Shape::convex_polygon(pts.clone(), AGENT, Stroke::NONE));
painter.add(Shape::closed_line(pts, Stroke::new(1.5, Color32::WHITE)));
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(11.0), Color32::WHITE);
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
painter.rect_filled(r, 4.0, AGENT);
painter.galley(r.min + vec2(5.0, 2.0), label, Color32::WHITE);
}