Rename displayed product to SolidCraft and add header bug reporting

This commit is contained in:
bherbruck committed 2026-10-11 16:16:48 -04:00
1 parent 1d3048062d
commit af536a7907
85 files changed
+442 -242

No files matched your search

+6 -6
View File
@@ -11,14 +11,14 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT");
pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE");
/// The user and developer guide (docs/book, published by .github/workflows/pages.yml).
pub const DOCS_URL: &str = "https://bherbruck.github.io/solvecraft/";
pub const DOCS_URL: &str = "https://bherbruck.github.io/solidcraft/";
const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md");
use solvecraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
/// Fonts: what this build carries.
pub const FONTS: &str = "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and \
the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans \
(SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans \
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolveCraft builds.";
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.";
#[derive(Default)]
pub struct HelpState {
@@ -32,7 +32,7 @@ pub struct HelpState {
pub fn diagnostics(app: &SolveApp) -> String {
let renderer = if app.viewport.gpu.is_some() { "GPU" } else { "CPU" };
format!(
"SolveCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
"SolidCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
std::env::consts::OS,
std::env::consts::ARCH,
app.help.gpu.as_ref().map(|g| format!(", {g}")).unwrap_or_default(),
@@ -71,7 +71,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
crate::context_menu::menu_frame(ui, |ui| {
for (id, label) in [
("docs", "Documentation"),
("about", "About SolveCraft"),
("about", "About SolidCraft"),
("shortcuts", "Keyboard Shortcuts"),
("report", "Report an Issue…"),
] {
@@ -100,12 +100,12 @@ fn about(app: &mut SolveApp, ctx: &egui::Context) {
}
let t = Tokens::get();
let mut open = true;
crate::frame::window(ctx, "About SolveCraft", crate::frame::Width::Normal)
crate::frame::window(ctx, "About SolidCraft", crate::frame::Width::Normal)
.id(egui::Id::new("sc_about"))
.open(&mut open)
.default_size(vec2(440.0, 560.0))
.show(ctx, |ui| {
ui.label(RichText::new("SolveCraft").size(22.0).strong());
ui.label(RichText::new("SolidCraft").size(22.0).strong());
ui.label(format!("Version {VERSION} · commit {COMMIT} · built {BUILD_DATE}"));
ui.label(RichText::new("Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.").color(t.text_dim));
ui.add_space(6.0);
+1 -1
View File
@@ -343,7 +343,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(36, 24))).show(ui, |ui| {
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(RichText::new("SolveCraft").size(26.0).strong().color(t.text));
ui.label(RichText::new("SolidCraft").size(26.0).strong().color(t.text));
ui.add_space(12.0);
ui.label(RichText::new("Start").size(16.0).color(t.text_dim));
});
+11
View File
@@ -192,6 +192,17 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
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);
}
}
// Help: a question mark in a ring.
"help" => {
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
+2 -2
View File
@@ -10,7 +10,7 @@ use crate::{SolveApp, dialogs, help};
const MAX_BODY: usize = 24_000;
const MAX_IMAGE_PIXELS: usize = 16_000_000;
const ISSUE_URL: &str = "https://github.com/bherbruck/solvecraft/issues/new";
const ISSUE_URL: &str = "https://github.com/bherbruck/solidcraft/issues/new";
thread_local! {
static CAD_INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
@@ -480,7 +480,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if ui.add_enabled(draft.image.is_some() && app.services.pick_save.is_some(), egui::Button::new("Save screenshot…")).clicked()
&& let Some(image) = draft.image.as_ref()
&& let Some(path) =
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solvecraft-issue-{}.png", app.shot_token), &["png"]))
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solidcraft-issue-{}.png", app.shot_token), &["png"]))
{
let result = crate::control::save_screenshot(image, Some(&path));
draft.note = if result.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
+2 -2
View File
@@ -87,7 +87,7 @@ pub fn command_for(app: &SolveApp, key: &str) -> Option<&'static str> {
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
let spec = solvecraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
if RESERVED.contains(&key) {
return Err(format!("{key} is used by SolveCraft itself"));
return Err(format!("{key} is used by SolidCraft itself"));
}
if !key.is_empty() {
let others: Vec<&'static str> = commands_for(app, key).into_iter().filter(|o| *o != id).collect();
@@ -224,7 +224,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
Some((_, "reset_all")) => reset(app, None),
Some((_, "export")) => {
let text = crate::toolbar_custom::export(app).to_string();
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolveCraft shortcuts.json", &["json"])) {
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolidCraft shortcuts.json", &["json"])) {
app.keymap.message = std::fs::write(&path, text).err().map(|e| format!("export: {e}"));
}
}
+1 -1
View File
@@ -16,7 +16,7 @@
"sketch.make_selection": "選択を{linetype}にする",
"ui.1_degree_of_freedom": "自由度 1",
"ui.3d_sketch": "3D スケッチ",
"ui.about": "SolveCraft について",
"ui.about": "SolidCraft について",
"ui.activate": "アクティブ化",
"ui.active_component": "アクティブなコンポーネント",
"ui.agent_cursor": "AIエージェントカーソル",
+1 -1
View File
@@ -616,7 +616,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
}
});
ui.end_row();
note(ui, "Anti-aliasing applies the next time SolveCraft starts");
note(ui, "Anti-aliasing applies the next time SolidCraft starts");
ui.label("Renderer");
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
(Some(_), Some(g)) => format!("GPU: {g}"),
+1 -1
View File
@@ -63,7 +63,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
.min_width(420.0)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.show(ctx, |ui| {
ui.label("SolveCraft closed before these designs were saved. Their last autosave can be opened.");
ui.label("SolidCraft closed before these designs were saved. Their last autosave can be opened.");
ui.add_space(6.0);
for e in app.recoverable.iter().take(20) {
ui.label(RichText::new(&e.name).strong());
+142 -6
View File
@@ -36,6 +36,13 @@ pub fn promoted(tab: &str, panel: &str) -> usize {
}
}
fn report_rect(bar: Rect, captions: f32, tabs_start: f32) -> (Rect, bool) {
let right = bar.right() - captions - 90.0;
let labeled = right - tabs_start >= 340.0;
let width = if labeled { 124.0 } else { 28.0 };
(Rect::from_min_size(pos2(right - width, bar.top() + 4.0), vec2(width, 26.0)), labeled)
}
pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
egui::Panel::top("sc_appbar").exact_size(34.0).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
@@ -45,14 +52,20 @@ 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 };
// At high UI scale, keep the report, Home, Help and window controls reachable.
// The theme shortcut remains available in Preferences.
let compact_actions = r.right() - left < 640.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);
let mut x = left + 128.0;
if !compact_actions {
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolidCraft", FontId::proportional(14.0), t.app_bar_text);
}
let mut x = left + if compact_actions { 60.0 } else { 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));
x += 30.0;
let width = if compact_actions { 22.0 } else { 26.0 };
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(width, 26.0));
x += if compact_actions { 22.0 } else { 30.0 };
let resp = ui.interact(br, ui.id().with(("ab", icon)), Sense::click());
crate::scenario::publish_handle(&format!("appbar:{icon}"), br.center());
if resp.hovered() {
@@ -83,7 +96,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
app.home.show(rev);
}
let theme_tip = if app.ui.dark { app.tr("ui.light") } else { app.tr("ui.dark") };
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
if !compact_actions && click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
app.ui.dark = !app.ui.dark;
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
}
@@ -109,7 +122,34 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
}
// Document tabs.
let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 };
crate::documents::tabs(app, ui, r, x + 16.0, r.right() - captions - 90.0);
let tabs_start = x + 16.0;
let (report, report_label) = report_rect(r, captions, tabs_start);
let tabs_end = report.left() - 8.0;
// Narrow title bars can fit only part of the tab strip. Its hit regions must not
// extend into the persistent report and window controls.
let tabs_rect = Rect::from_min_max(pos2(tabs_start.min(tabs_end), r.top()), pos2(tabs_end, r.bottom()));
let mut tabs_ui = ui.new_child(egui::UiBuilder::new().max_rect(tabs_rect));
tabs_ui.set_clip_rect(tabs_rect.intersect(ui.clip_rect()));
crate::documents::tabs(app, &mut tabs_ui, r, tabs_start, tabs_end);
let response = ui.interact(report, ui.id().with("sc_report_button"), Sense::click());
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Report a bug"));
crate::scenario::publish_handle("titlebar:report", report.center());
ui.painter().rect_filled(report, 4.0, if response.hovered() || response.has_focus() { t.hover } else { t.app_bar });
ui.painter().rect_stroke(report, 4.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
let icon = Rect::from_center_size(pos2(report.left() + 14.0, report.center().y), vec2(18.0, 18.0));
icons::paint(ui.painter(), icon, "bug_report", t.app_bar_text, t.app_bar_text, t.app_bar);
if report_label {
ui.painter().text(
pos2(report.left() + 29.0, report.center().y),
Align2::LEFT_CENTER,
"Report a bug",
FontId::proportional(13.0),
t.app_bar_text,
);
}
if response.on_hover_text("Report a bug — review a screenshot and open a GitHub draft").clicked() {
crate::help::run(app, &ui.ctx().clone(), "report");
}
// Preferences, at the top right like Fusion's profile menu (also Ctrl+,).
let gear = Rect::from_center_size(pos2(r.right() - captions - 70.0, r.center().y), vec2(24.0, 24.0));
let gresp = ui.interact(gear, ui.id().with("sc_prefs_button"), Sense::click());
@@ -612,3 +652,99 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
}
}
}
#[cfg(test)]
mod header_report_tests {
use super::*;
fn captured_frame(h: &mut crate::scenario::Harness) {
let token = h.app.shot_token;
crate::issue_report::receive(&mut h.app, token, std::sync::Arc::new(egui::ColorImage::new([2, 2], vec![Color32::GREEN; 4])));
h.frames(3);
}
#[test]
fn header_report_reserves_controls_at_normal_and_double_scale_widths() {
for physical_width in [900.0, 1400.0] {
for scale in [1.0, 2.0] {
let width = physical_width / scale;
let bar = Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 34.0));
for captions in [0.0, crate::titlebar::WIDTH] {
let (report, labeled) = report_rect(bar, captions, 354.0);
assert!(bar.contains_rect(report));
let prefs = Rect::from_center_size(pos2(width - captions - 70.0, 17.0), vec2(24.0, 24.0));
assert!(!report.intersects(prefs));
assert!(report.right() < width - captions - 82.0);
assert_eq!(labeled, width - captions - 90.0 - 354.0 >= 340.0);
}
}
}
}
#[test]
fn header_report_actual_click_opens_capture_without_changing_healthy_design() {
for width in [450.0, 700.0, 900.0, 1400.0] {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.app.custom_titlebar = true;
h.app.run("solid.box", json!({"length":35,"width":12,"height":4})).unwrap();
h.call("ui.resize", json!({"width":width,"height":720}));
h.frames(3);
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
let point = h.widget("Report a bug").expect("compact and full buttons retain an accessible name");
h.step(&json!({"click":point})).unwrap();
assert!(h.app.issue_report.draft.is_some(), "width {width}: header click must reach the report");
assert!(crate::issue_report::pending(&h.app));
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
captured_frame(&mut h);
// Existing review content scrolls vertically when the window is narrow.
if width < 700.0 {
let [x, y] = h.widget("Close draft").unwrap();
h.call("ui.scroll", json!({"x":x,"y":y - 60.0,"delta":-1200}));
// Let the scroll animation finish before locating the click target.
h.frames(60);
}
h.step(&json!({"widget":"Close draft"})).unwrap();
assert!(h.app.issue_report.draft.is_none(), "width {width}: review closes through its actual button");
h.step(&json!({"click":{"handle":"appbar:home"}})).unwrap();
assert!(h.app.home.open, "width {width}: Home remains reachable beside Report");
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
}
}
#[test]
fn header_report_preserves_new_dimension_draft_and_single_commit() {
crate::dim_view::cancel();
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
h.app.run("sketch.line", json!({"points":[[0,0],[20,0]]})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 80.0;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
h.settle();
h.step(&json!({"start":"sketch.dimension"})).unwrap();
for point in [[0, 0, 0], [20, 0, 0], [10, 5, 0]] {
h.step(&json!({"click":{"world":point}})).unwrap();
}
h.step(&json!({"text":"30 mm"})).unwrap();
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
h.step(&json!({"widget":"Report a bug"})).unwrap();
h.frames(3);
assert!(h.app.issue_report.draft.is_some() && crate::dim_view::editing());
captured_frame(&mut h);
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
h.step(&json!({"text":"Describe this dimension problem."})).unwrap();
h.step(&json!({"key":"Enter"})).unwrap();
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
h.step(&json!({"widget":"Close draft"})).unwrap();
h.frames(2);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
h.step(&json!({"key":"Enter"})).unwrap();
assert!(!crate::dim_view::editing());
assert_eq!(h.app.session.undo.len(), before.1 + 1);
assert_eq!(h.app.session.doc.param("d1").unwrap().expr, "30 mm");
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), before.0);
}
}
+1 -1
View File
@@ -177,7 +177,7 @@ pub fn export(app: &SolveApp) -> Value {
pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
if v.get("format").and_then(Value::as_str) != Some("solvecraft-shortcuts/1") {
return Err("not a SolveCraft shortcuts file".into());
return Err("not a SolidCraft shortcuts file".into());
}
if let Some(s) = v.get("shortcuts") {
app.keymap.load(s);