feat(ui): improve text readability and add VernierCAD identity
ci / check (push) Failing after 4s

Agent: codex
This commit is contained in:
2026-09-15 21:39:11 +02:00
parent 100c112c7a
commit ec0b2f2891
21 changed files with 144 additions and 49 deletions
+4
View File
@@ -22,6 +22,10 @@ bits and missing shared libraries with `ldd`. Existing user data and releases
owned by another commit are preserved. Use `--dry-run` to inspect destinations;
use `--prefix DIR` for a self-contained staging prefix.
Include `vernier.svg` from `packaging/appimage/` beside the release binaries to
install the app's desktop icon. It is checksummed with the release and follows
the same `current` pointer as the executable.
Before activation, install under a staging prefix and run the installed-worker
`file-commands-actual` and `local-linux-release` driven workflows, including
save/reopen and STEP/STL export. Then run the staged native launcher with
Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 292 KiB

+2 -2
View File
@@ -11,7 +11,7 @@
--ink: #16181A; /* text · tiles · outlines oklch(0.21 0.006 250) */
--ink-warm: #1B1C19; /* warm ink — headings on paper */
--slate: #56564E; /* secondary text oklch(0.45 0.008 95) */
--mute: #9A958A; /* tertiary / metadata on paper */
--mute: #6F6B62; /* readable tertiary / metadata on paper */
--paper: #F4F1EA; /* primary background oklch(0.96 0.006 90) */
--paper-raised: #FBF9F3; /* raised card surface on paper */
@@ -23,7 +23,7 @@
--dark-raised: #1C1F22; /* raised row on dark (hover) */
--dark-line: #26292C; /* borders on dark */
--dark-text: #C9CCC8; /* primary text on dark */
--dark-mute: #7C8085; /* secondary text on dark */
--dark-mute: #A0A5AC; /* secondary text on dark */
--dark-dot: #3A3E43; /* inert terminal dot */
/* ---- Alternate accents (same role as amber; one at a time) ---- */
@@ -4,7 +4,7 @@
:root {
/* ---- Families ---- */
--font-display: 'Silkscreen', 'Courier New', monospace; /* display · logo · labels (bitmap) */
--font-display: 'Silkscreen', 'Courier New', monospace; /* brand · headings at 14px and above */
--font-mono: 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace; /* code · values · metadata */
--font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif; /* body · long-form */
@@ -19,8 +19,8 @@
--t-display-l: 34px; /* page title */
--t-display-m: 22px; /* section / icon tile */
--t-display-s: 14px; /* label / principle name */
--t-label: 11px; /* eyebrow / kicker (Silkscreen) */
--t-label-xs: 8px; /* micro caption */
--t-label: 12px; /* eyebrow / kicker (Plex Mono) */
--t-label-xs: 12px; /* small caption (Plex Mono) */
/* ---- Mono scale (IBM Plex Mono) ---- */
--t-mono-l: 15px; /* command line */
+43 -4
View File
@@ -21,6 +21,43 @@
use crate::theme::{self, ShellTheme};
use egui::{Color32, Rect, Response, Sense, Ui, Vec2, pos2, vec2};
/// VernierCAD's two-tone V. Coordinates match `packaging/appimage/vernier.svg`.
/// The shell draws native geometry so the accent follows Settings and the
/// mark stays sharp at every display scale. Tiny tiles omit the scale ticks.
pub fn app_logo(ui: &mut Ui, theme: ShellTheme, size: f32) -> Response {
let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
if ui.is_rect_visible(rect) {
let painter = ui.painter();
painter.rect_filled(rect, size * 40.0 / 256.0, theme::INK);
let at = |x: f32, y: f32| rect.min + vec2(x, y) * (size / 256.0);
for (points, color) in [
(
[(28.0, 56.0), (80.0, 56.0), (128.0, 164.0), (128.0, 232.0)],
theme::PAPER,
),
(
[(128.0, 164.0), (176.0, 56.0), (228.0, 56.0), (128.0, 232.0)],
theme.accent,
),
] {
painter.add(egui::Shape::convex_polygon(
points.into_iter().map(|(x, y)| at(x, y)).collect(),
color,
egui::Stroke::NONE,
));
}
if size >= 48.0 {
for y in [32.0, 43.0] {
painter.line_segment(
[at(212.0, y), at(230.0, y)],
egui::Stroke::new(size * 5.0 / 256.0, theme.accent),
);
}
}
}
response
}
/// The grid Nib is drawn on. Sixteen rows of sixteen.
pub const GRID: usize = 16;
@@ -657,8 +694,8 @@ fn tile_em(painter: &egui::Painter, size: f32) -> f32 {
/// the brackets are wider than they look, and two extra pixels each side is
/// what pushed the tile lockup past the 26px square it has to sit inside.
fn measure_lockup(painter: &egui::Painter, em: f32, gap: f32, word: &str) -> f32 {
let bracket = theme::caption(em * 1.5);
let body = theme::caption(em);
let bracket = egui::FontId::new(em * 1.5, theme::display());
let body = egui::FontId::new(em, theme::display());
let w = |font: egui::FontId, text: &str| {
painter
.layout_no_wrap(text.to_owned(), font, Color32::PLACEHOLDER)
@@ -681,8 +718,8 @@ fn paint_lockup(
letters: Color32,
word: &str,
) {
let bracket_font = theme::caption(em * 1.5);
let body_font = theme::caption(em);
let bracket_font = egui::FontId::new(em * 1.5, theme::display());
let body_font = egui::FontId::new(em, theme::display());
let width = measure_lockup(painter, em, gap, word);
let mut x = at.x - width / 2.0;
@@ -910,6 +947,8 @@ mod tests {
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
for surface in [Surface::Paper, Surface::Dark] {
logo_tile(ui, theme, 26.0, surface);
app_logo(ui, theme, 26.0);
app_logo(ui, theme, 64.0);
logo_wordmark(ui, theme, 26.0, surface);
nib(ui, 96.0, surface, theme.accent, true);
}
+1 -4
View File
@@ -344,10 +344,7 @@ pub fn constraint_tile(
response.on_hover_text(label)
}
/// A Silkscreen group caption — 8px, mute, wide-tracked.
///
/// Glance-only by construction: it takes a `&str` and paints one line, so
/// nobody can put a paragraph in a face whose `b` and `n` are the same height.
/// A small group caption, using the theme's readable caption face and size.
pub fn caption(ui: &mut Ui, text: &str) {
let galley =
ui.painter()
+4 -9
View File
@@ -4141,7 +4141,8 @@ fn title_bar(
brand_tile(ui, theme);
ui.label(
egui::RichText::new("VernierCAD")
.font(theme::caption(13.0))
.font(theme::sans(17.0))
.strong()
.color(theme::INK),
);
// THE VERSION IS THE FIRST THING TO GO. Everything on this
@@ -4464,15 +4465,9 @@ fn command_bar_field(ui: &mut egui::Ui, state: &mut ShellState, theme: ShellThem
);
}
/// The `[nb]` tile, from the brand module.
///
/// **NO LONGER A PLACEHOLDER.** The first pass painted an ink square with "nb"
/// on it, on the reading that the handoff carried the mark only as a
/// design-tool import. The `_ds_bundle.js` beside those screens carries
/// `Logo.jsx` in full — and the mark is live Silkscreen type rather than a
/// vector, which is exactly why no SVG of it exists to vendor.
/// The VernierCAD app mark; the maker's mark remains in the brand module.
fn brand_tile(ui: &mut egui::Ui, theme: ShellTheme) {
brand::logo_tile(ui, theme, 26.0, brand::Surface::Paper);
brand::app_logo(ui, theme, 26.0);
}
/// A 1px vertical divider in a horizontal row.
+4
View File
@@ -2264,6 +2264,10 @@ pub(crate) fn status_line(
.exact_size(theme::h_status(theme.compact))
.frame(frame)
.show(ui, |ui| {
// This read-only band has no controls. A normal interaction row
// would be taller than the band and clip the status text below
// the window edge, especially after increasing the font size.
ui.spacing_mut().interact_size.y = ui.available_height();
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 12.0;
let width = ui.available_width();
+19 -21
View File
@@ -33,7 +33,7 @@ pub const INK_WARM: Color32 = Color32::from_rgb(0x1B, 0x1C, 0x19);
/// `--slate` · secondary text.
pub const SLATE: Color32 = Color32::from_rgb(0x56, 0x56, 0x4E);
/// `--mute` · tertiary text and metadata.
pub const MUTE: Color32 = Color32::from_rgb(0x9A, 0x95, 0x8A);
pub const MUTE: Color32 = Color32::from_rgb(0x6F, 0x6B, 0x62);
/// `--dark-raised` · raised row on the dark surface (in-viewport chips).
pub const DARK_RAISED: Color32 = Color32::from_rgb(0x1C, 0x1F, 0x22);
/// `--dark-line` · borders on the dark surface.
@@ -41,7 +41,7 @@ pub const DARK_LINE: Color32 = Color32::from_rgb(0x26, 0x29, 0x2C);
/// `--dark-text` · primary text on the dark surface.
pub const DARK_TEXT: Color32 = Color32::from_rgb(0xC9, 0xCC, 0xC8);
/// `--dark-mute` · secondary text on the dark surface.
pub const DARK_MUTE: Color32 = Color32::from_rgb(0x7C, 0x80, 0x85);
pub const DARK_MUTE: Color32 = Color32::from_rgb(0xA0, 0xA5, 0xAC);
/// `--amber` · the default accent. The user may set any hue in Settings, so
/// read [`ShellTheme::accent`] rather than this constant at call sites.
@@ -102,10 +102,8 @@ pub const ROW_ROOMY: f32 = 36.0;
/// the only surface that prints a refusal — was cut through the middle and
/// the lower half painted below the window's edge (audit R2A-04).
///
/// The remaining two pixels of descender clip at BOTH densities are the
/// known, separately-tracked issue and are deliberately untouched here: the
/// band is four pixels short of its row at compact and this keeps it
/// exactly four pixels short at roomy.
/// The status painter limits its read-only row to the available inner height,
/// so neither density clips the text below the window edge.
#[must_use]
pub const fn h_status(compact: bool) -> f32 {
H_STATUS + row_height(compact) - ROW_COMPACT
@@ -244,11 +242,9 @@ impl ShellTheme {
style.spacing.window_margin = egui::Margin::same(16);
style.text_styles = [
// Silkscreen is GLANCE-ONLY: tabs, group captions, section
// headings. Never a paragraph — its `b` and `d` are the same
// height as its `n`, which is fine for four letters and hostile
// for forty words.
(TextStyle::Heading, FontId::new(11.0, display())),
// Small headings need distinct letterforms; reserve Silkscreen
// for the brand and larger display headings.
(TextStyle::Heading, caption(12.0)),
// Plex Mono is the value voice: labels on commands, dimensions,
// paths, ids.
(TextStyle::Button, FontId::new(13.0, FontFamily::Monospace)),
@@ -259,7 +255,7 @@ impl ShellTheme {
// Plex Sans carries the sentences — the explanatory lines in the
// timeline footer and the refusal dialog.
(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional)),
(TextStyle::Small, FontId::new(11.0, FontFamily::Monospace)),
(TextStyle::Small, mono(12.0)),
]
.into();
@@ -301,25 +297,27 @@ pub fn display() -> FontFamily {
FontFamily::Name("display".into())
}
/// A Silkscreen caption `FontId` at `size`.
///
/// The design uses exactly three: 8px group captions, 10–11px tabs and pills,
/// 22px section titles.
/// Small captions use Plex Mono at a readable minimum size. Larger headings
/// retain Silkscreen; brand lettering can request [`display`] explicitly.
#[must_use]
pub fn caption(size: f32) -> FontId {
FontId::new(size, display())
if size < 14.0 {
mono(size)
} else {
FontId::new(size, display())
}
}
/// A Plex Mono `FontId` at `size` — the value voice.
#[must_use]
pub fn mono(size: f32) -> FontId {
FontId::new(size, FontFamily::Monospace)
FontId::new(size.max(12.0), FontFamily::Monospace)
}
/// A Plex Sans `FontId` at `size` — sentences only.
/// A Plex Sans `FontId` with a 12px floor for readable helper text.
#[must_use]
pub fn sans(size: f32) -> FontId {
FontId::new(size, FontFamily::Proportional)
FontId::new(size.max(12.0), FontFamily::Proportional)
}
/// Whether [`install_fonts`] has run on this context.
@@ -468,7 +466,7 @@ mod tests {
// `·` is in the chrome copy everywhere and is NOT in
// Silkscreen. If the display family had no fallback this
// would be the notdef glyph.
let id = caption(8.0);
let id = FontId::new(14.0, display());
assert!(
f.glyph_width(&id, '·') > 0.0,
"the display family has no fallback behind Silkscreen — \
+34
View File
@@ -0,0 +1,34 @@
# VernierCAD: readable type and a clear V
The app keeps its paper, charcoal, copper, and technical typography. Small
captions use IBM Plex Mono with a 12px floor; larger display headings keep
Silkscreen. Secondary text has stronger contrast on both light and dark
surfaces. The status row fits its available height to prevent clipped text.
The app mark is a continuous two-tone V: ivory on the left, copper on the
right, a single joined point, and two separate measurement ticks. At small
sizes the shell omits the ticks. The native painter follows the user's accent.
The packaged SVG uses the default copper. The existing maker's mark and Nib
remain available for attribution.
Assets:
- `packaging/appimage/vernier.svg`: production vector app icon.
- `crates/vernier-ui/src/brand.rs`: native, resolution-independent app mark.
- `docs/design/verniercad-logo-concept.png`: revised concept board generated
with the built-in image-generation tool. Its bold wordmark is the user's
preferred design direction; generated lettering is not an identified or
bundled font. The shell uses the already bundled Plex Sans for its title.
Concept prompt: preserve the clean bold technical VernierCAD wordmark;
replace the notched caliper symbol with an unmistakable continuous V, ivory
left arm and copper right arm, deep triangular opening and one bottom point;
move the measurement ticks outside the letter; charcoal tile and cream
background; show a small-size proof. No internal slit, notch, or floating bar.
Validation: 445 active vernier-ui tests passed (three pre-existing ignored
tests). The `m1-smoke`, `m4-cutaway`, `m4-edge-pick`, and `ux-sketch-feedback`
workflows passed once on RADV with the installed worker. All five affected
screenshots were reviewed before updating their reference images; geometry,
selection and document assertions stayed enabled. This is a focused visual
change check, not a new full-release certification or determinism run.
Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

+5 -5
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title description">
<title id="title">VernierCAD</title>
<desc id="description">A dark drafting tile with an amber vernier scale.</desc>
<rect width="256" height="256" rx="42" fill="#17191c"/>
<path d="M54 64h-18v128h18M202 64h18v128h-18" fill="none" stroke="#f2b84b" stroke-width="14" stroke-linecap="square"/>
<path d="M78 92h100M78 128h100M78 164h100" fill="none" stroke="#f4f0e8" stroke-width="12" stroke-linecap="square"/>
<path d="M96 78v28M128 78v28M160 78v28M112 150v28M144 150v28" fill="none" stroke="#f2b84b" stroke-width="8"/>
<desc id="description">An ivory and copper V on an ink tile, with two precision scale ticks.</desc>
<rect width="256" height="256" rx="40" fill="#16181A"/>
<path d="M28 56H80L128 164V232Z" fill="#F4F1EA"/>
<path d="M128 164L176 56H228L128 232Z" fill="#C77A33"/>
<path d="M212 32H230M212 43H230" fill="none" stroke="#C77A33" stroke-width="5"/>
</svg>

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 548 B

+1
View File
@@ -1,6 +1,7 @@
{
"name": "m1-smoke-push-pull",
"notes": [
"2026-09-15: PNG updated for readable Plex captions, stronger secondary text, unclipped status text, and the VernierCAD V mark; geometry assertions unchanged.",
"2026-09-09: PNG rebaselined for gamma-space UI coverage blending. The",
"GPU regression measures half-covered black on white as 127 instead of 187.",
"The earlier GL drift measurements below are historical; this golden is RADV-only.",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "m4-cutaway",
"notes": [
"2026-09-15: PNGs updated for readable Plex captions, stronger secondary text, unclipped status text, and the VernierCAD V mark; section and picking assertions unchanged.",
"M4-10 THROUGH THE REAL SHELL: the section command aimed from the ribbon,",
"an offset typed into its card, the cutaway chip turned on, and the",
"resulting picture asserted by asking the ID buffer what is at the pixel",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "m4-edge-pick",
"notes": [
"2026-09-15: PNG updated for readable Plex captions, stronger secondary text, unclipped status text, and the VernierCAD V mark; picking assertions unchanged.",
"M4-1 THROUGH THE REAL SHELL: an edge and a vertex of the starter block are",
"picked in the viewport, and the hover says what is under the pointer before",
"each click. Nothing here asserts geometry — no feature is created — because",
+1
View File
@@ -14,6 +14,7 @@
"distance": 120
},
"notes": [
"2026-09-15: PNG updated for readable Plex captions, stronger secondary text, unclipped status text, and the VernierCAD V mark; sketch assertions unchanged.",
"The pending line snaps to horizontal and the 1mm grid before committing. The screenshot pins its rubber-band outline and snap label.",
"Four line clicks make a 40x30mm rectangle. Closing it floats the extrude card (bottom right, off the sketch); the height is typed into it and Enter commits. The 10mm extrusion must export 12000mm^3.",
"The RADV golden is exact on its measured adapter: mean 0, hot pixels 0, maximum channel delta 0. Other adapters require their own named budget."
+21 -1
View File
@@ -27,6 +27,9 @@ for binary in vernier-app vernier-worker; do
[[ "$ldd_output" != *"not found"* ]] || { echo "missing runtime library for $binary" >&2; exit 1; }
hashes[$binary]=$(sha256sum "$source_bin" | awk '{print $1}')
done
if [[ -f "$release_dir/vernier.svg" ]]; then
hashes[vernier.svg]=$(sha256sum "$release_dir/vernier.svg" | awk '{print $1}')
fi
if [[ -n "$prefix" ]]; then
[[ "$prefix" = /* ]] || { echo "--prefix must be absolute" >&2; exit 1; }
data_root=$prefix/share; state_root=$prefix/state; bin_root=$prefix/bin; app_root=$prefix/share/applications
@@ -60,6 +63,12 @@ if [[ -e "$target" || -L "$target" ]]; then
actual=$(sha256sum "$target/$binary" | awk '{print $1}')
[[ "$actual" == "$saved" && "$saved" == "${hashes[$binary]}" ]] || { echo "existing release hash mismatch: $binary" >&2; exit 1; }
done
if [[ -n "${hashes[vernier.svg]:-}" ]]; then
[[ -f "$target/vernier.svg" && ! -L "$target/vernier.svg" ]] || { echo "existing release has no owned icon" >&2; exit 1; }
saved=$(awk -F= '$1=="sha256_vernier.svg" {print $2}' "$target/.vernier-release")
actual=$(sha256sum "$target/vernier.svg" | awk '{print $1}')
[[ "$actual" == "$saved" && "$saved" == "${hashes[vernier.svg]}" ]] || { echo "existing release hash mismatch: vernier.svg" >&2; exit 1; }
fi
fi
if $dry_run; then echo "would install $release_dir as $target"; echo "would activate $current and preserve $previous"; exit 0; fi
mkdir -p "$releases" "$bin_root" "$app_root" "$state_root/vernier/logs"
@@ -72,6 +81,12 @@ if [[ ! -e "$target" ]]; then
[[ "$staged_hash" == "${hashes[$binary]}" ]] || { echo "source changed while staging $binary" >&2; exit 1; }
done
printf 'version=%s\ncommit=%s\nsha256_vernier-app=%s\nsha256_vernier-worker=%s\n' "$version" "$commit" "${hashes[vernier-app]}" "${hashes[vernier-worker]}" >"$stage/release/.vernier-release"
if [[ -n "${hashes[vernier.svg]:-}" ]]; then
cp "$release_dir/vernier.svg" "$stage/release/vernier.svg"
staged_hash=$(sha256sum "$stage/release/vernier.svg" | awk '{print $1}')
[[ "$staged_hash" == "${hashes[vernier.svg]}" ]] || { echo "source changed while staging icon" >&2; exit 1; }
printf 'sha256_vernier.svg=%s\n' "$staged_hash" >>"$stage/release/.vernier-release"
fi
fi
mkdir -p "$stage/bin" "$stage/applications"
cat >"$stage/bin/vernier" <<EOF
@@ -94,13 +109,18 @@ desktop_exec=${desktop_exec//\`/\\\`}
desktop_exec=${desktop_exec//%/%%}
# Escape the Exec value again for the desktop-file string layer.
desktop_exec=${desktop_exec//\\/\\\\}
desktop_icon=applications-graphics
if [[ -f "$stage/release/vernier.svg" || -f "$target/vernier.svg" ]]; then
# Follow the same atomic release pointer as the executable, including rollback.
desktop_icon=${current//\\/\\\\}/vernier.svg
fi
cat >"$stage/applications/vernier.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=VernierCAD
Comment=Parametric solid modeller
Exec="$desktop_exec"
Icon=applications-graphics
Icon=$desktop_icon
StartupWMClass=vernier
Terminal=false
Categories=Graphics;3DGraphics;