fix(web): render OSNAP menu symbols as SVG instead of font glyphs

The object-snap menu drew each mode's marker as a Unicode geometric glyph
(◻ △ ◯ …). The web build bundles only Fira Sans, which lacks them, so they
rendered as tofu boxes (#138). Add an SVG per snap mode under
assets/icons/osnap, map them via icons::osnap, and draw them with the same
tinted-SVG path the rest of the chrome uses — font-independent on every
platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-20 14:27:59 +03:00
commit d36cc9786f
16 changed files with 68 additions and 12 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7 L17 17 M17 7 L7 17"/><rect x="4" y="4" width="16" height="16" stroke-dasharray="2.5 2.5"/></svg>

After

Width:  |  Height:  |  Size: 266 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="7"/></svg>

After

Width:  |  Height:  |  Size: 193 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="14" height="14"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12 H7 M11 12 H13 M17 12 H20"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5 V19 M15 5 V19 M5 9 H19 M5 15 H19"/></svg>

After

Width:  |  Height:  |  Size: 211 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="11" height="11"/><rect x="9" y="9" width="10" height="10"/></svg>

After

Width:  |  Height:  |  Size: 246 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6 L18 18 M18 6 L6 18"/></svg>

After

Width:  |  Height:  |  Size: 197 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5 L19 18 H5 Z"/></svg>

After

Width:  |  Height:  |  Size: 191 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6 H18 L6 18 H18 Z"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="7.5"/><path d="M7 7 L17 17 M17 7 L7 17"/></svg>

After

Width:  |  Height:  |  Size: 230 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 16 L13 6 M11 18 L18 8"/></svg>

After

Width:  |  Height:  |  Size: 198 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4 V19 H20"/><path d="M6 13 H12 V19"/></svg>

After

Width:  |  Height:  |  Size: 211 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 L20 12 L12 20 L4 12 Z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="15" r="5.5"/><path d="M4 8 H20"/></svg>

After

Width:  |  Height:  |  Size: 215 B

View file

@ -15,6 +15,25 @@ const TRI_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/tri_right.svg");
const UNDO: &[u8] = include_bytes!("../../assets/icons/ui/undo.svg");
const REDO: &[u8] = include_bytes!("../../assets/icons/ui/redo.svg");
// OSNAP marker symbols. Rendered as SVG (not Unicode glyphs) so the snap menu
// shows the right shapes on the web build, whose bundled Fira Sans lacks the
// geometric glyphs and rendered them as tofu boxes. (#138)
const OSNAP_ENDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/endpoint.svg");
const OSNAP_MIDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/midpoint.svg");
const OSNAP_CENTER: &[u8] = include_bytes!("../../assets/icons/osnap/center.svg");
const OSNAP_NODE: &[u8] = include_bytes!("../../assets/icons/osnap/node.svg");
const OSNAP_QUADRANT: &[u8] = include_bytes!("../../assets/icons/osnap/quadrant.svg");
const OSNAP_INTERSECTION: &[u8] = include_bytes!("../../assets/icons/osnap/intersection.svg");
const OSNAP_EXTENSION: &[u8] = include_bytes!("../../assets/icons/osnap/extension.svg");
const OSNAP_INSERTION: &[u8] = include_bytes!("../../assets/icons/osnap/insertion.svg");
const OSNAP_PERPENDICULAR: &[u8] =
include_bytes!("../../assets/icons/osnap/perpendicular.svg");
const OSNAP_TANGENT: &[u8] = include_bytes!("../../assets/icons/osnap/tangent.svg");
const OSNAP_NEAREST: &[u8] = include_bytes!("../../assets/icons/osnap/nearest.svg");
const OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent.svg");
const OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg");
const OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg");
const LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg");
const LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg");
const LAY_FRZ: &[u8] = include_bytes!("../../assets/icons/layers/layfrz.svg");
@ -82,6 +101,29 @@ pub fn raw<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
.into()
}
/// SVG bytes for an OSNAP mode's marker symbol, for the snap menu. (#138)
pub fn osnap(snap: crate::snap::SnapType) -> &'static [u8] {
use crate::snap::SnapType as S;
match snap {
S::Endpoint => OSNAP_ENDPOINT,
S::Midpoint => OSNAP_MIDPOINT,
S::Center => OSNAP_CENTER,
S::Node => OSNAP_NODE,
S::Quadrant => OSNAP_QUADRANT,
S::Intersection => OSNAP_INTERSECTION,
S::Extension => OSNAP_EXTENSION,
S::Insertion => OSNAP_INSERTION,
S::Perpendicular => OSNAP_PERPENDICULAR,
S::Tangent => OSNAP_TANGENT,
S::Nearest => OSNAP_NEAREST,
S::ApparentIntersection => OSNAP_APPARENT,
S::Parallel => OSNAP_PARALLEL,
S::Grid => OSNAP_GRID,
// Not shown in the snap menu; fall back to a neutral marker.
S::ObjectPick => OSNAP_NEAREST,
}
}
/// Layer visibility icon bytes (on / off).
pub fn layer_visible(visible: bool) -> &'static [u8] {
if visible {

View file

@ -36,8 +36,8 @@ pub fn snap_popup_overlay<'a>(snapper: &'a Snapper, right_offset: f32) -> Elemen
// Snap mode rows
let mut rows: Vec<Element<'_, Message>> = Vec::new();
for &(snap_type, icon, label) in ALL_SNAP_MODES {
rows.push(snap_row(snap_type, icon, label, snapper.is_on(snap_type)));
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
rows.push(snap_row(snap_type, label, snapper.is_on(snap_type)));
}
// "Object Snap Settings…" footer
@ -101,18 +101,18 @@ pub fn snap_popup_overlay<'a>(snapper: &'a Snapper, right_offset: f32) -> Elemen
// ── Individual snap row ───────────────────────────────────────────────────
fn snap_row<'a>(
snap_type: SnapType,
icon: &'a str,
label: &'a str,
active: bool,
) -> Element<'a, Message> {
fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a, Message> {
let checkmark = crate::ui::icons::check_cell(active, CHECK_COLOR);
let icon_el = text(icon)
.size(11)
.color(ICON_COLOR)
.width(Length::Fixed(16.0));
// SVG marker (not a Unicode glyph) so the symbols render on the web build,
// whose bundled font lacks them and showed tofu boxes. (#138)
let icon_el = container(crate::ui::icons::tinted::<Message>(
crate::ui::icons::osnap(snap_type),
13.0,
ICON_COLOR,
))
.width(Length::Fixed(16.0))
.align_x(iced::Center);
let label_el = text(label)
.size(11)