feat(view): one list of visual styles behind every way of choosing one

The ribbon offered four styles and dispatched each one's id as a command.
Two of those ids were not visual-style commands: "Shaded" carried SOLID,
which draws a 2D filled polygon, so choosing it started a draw command;
"Hidden" carried HIDDEN, which matched nothing at all, so it did nothing.
The intent had been for the button to fire its tool's event, but the
dropdown never reads one -- it dispatches the id.

Behind that sat two generations of the same feature. The render-mode
picker offered the seven styles a viewport can actually be drawn in;
everything else -- the ribbon, VSCURRENT, SHADEMODE, VISUALSTYLES and a
handful of bare verbs -- went through a binary wireframe-or-shaded flag
that could only approximate them, reporting "Hidden (shown shaded)" and
"X-Ray (shown as wireframe)" when asked for something it had no way to
draw. Four descriptions of one choice, each drifting on its own.

There is one list now. Each style names itself once -- mode, label, icon,
and the command that applies it -- and the ribbon, the picker, the
VISUALSTYLES verb and the interactive prompt all read from it, down to
the line that lists the choices, so what is offered cannot disagree with
what works. Keywords are the render modes' own names, since there is one
set of styles left to name.

The binary path is gone rather than kept alongside: its message, its
module event, the tool definitions that produced it, the ribbon's
special-cased highlight arms, the bare style verbs and the older keyword
spellings. X-Ray goes with it -- no render mode draws one, and it was
already coming out as a plain wireframe. Three files that had been left
holding a single icon constant each fold into the list.

Note: ModuleEvent::SetWireframe leaves the plugin API with it.

Closes #621

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-08-07 14:15:59 +03:00
commit b5a1146966
18 changed files with 170 additions and 187 deletions

View file

@ -127,7 +127,6 @@ MO, *PROPERTIES
UN, *UNITS
TP, *TOOLPALETTES
SSM, *SHEETSET
VW, *WIREFRAME
HI, *HIDE
3O, *3DORBIT
ORBIT, *3DORBIT

View file

@ -17,8 +17,6 @@ pub enum ModuleEvent {
/// Remove all loaded models from the scene.
#[allow(dead_code)]
ClearModels,
/// Toggle wireframe rendering.
SetWireframe(bool),
/// Toggle the layer manager panel.
ToggleLayers,
/// Ask the host to open a native file picker. On selection the host

View file

@ -103,66 +103,40 @@ impl OpenCADStudio {
}
}
"CLEAR" | "CLR" => return Some(Task::done(Message::ClearScene)),
"WIREFRAME" => return Some(Task::done(Message::SetWireframe(true))),
// Visual-style commands. OCS renders either a wireframe or a shaded
// view; the named styles map onto the closest of the two and the
// chosen style is reported so the mapping is explicit. (`SOLID` is
// intentionally NOT a visual-style verb — it is the 2D filled-polygon
// draw command; the shaded ribbon button drives `SetWireframe`.)
"VS" | "VSCURRENT" | "SHADEMODE" => {
// One interactive picker behind every verb that asks for a style,
// offering exactly what the render-mode widget offers. (#621)
"VS" | "VSCURRENT" | "SHADEMODE" | "VISUALSTYLES" => {
use crate::command::KeywordCommand;
use crate::modules::view::visual_style;
let c = KeywordCommand::new(
"VSCURRENT",
"VSCURRENT visual style [Shaded / Wireframe / Hidden / Realistic / Conceptual / X-ray]:",
vec![
("Shaded", "SHADED", None),
("Wireframe", "WIREFRAME", None),
("Hidden", "HIDDEN", None),
("Realistic", "REALISTIC", None),
("Conceptual", "CONCEPTUAL", None),
("X-Ray", "XRAY", None),
],
visual_style::keyword_prompt(),
visual_style::keyword_choices(),
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
// The named visual-style shortcuts still switch directly, and the
// `<name> <style>` argument form (also what the picker dispatches).
cmd if cmd == "HIDDENLINE"
|| cmd == "XRAY"
|| cmd == "REALISTIC"
|| cmd == "CONCEPTUAL"
|| cmd == "2DWIREFRAME"
|| cmd == "3DWIREFRAME"
|| cmd.starts_with("VSCURRENT ")
// A style by name: `VSCURRENT FLAT`, and what the picker dispatches.
cmd if cmd.starts_with("VSCURRENT ")
|| cmd.starts_with("SHADEMODE ")
|| cmd.starts_with("VS ") =>
{
let style = match cmd {
"VS" | "VSCURRENT" | "SHADEMODE" => String::new(),
s if s.starts_with("VS ")
|| s.starts_with("VSCURRENT ")
|| s.starts_with("SHADEMODE ") =>
{
cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase()
}
other => other.to_string(),
};
let (wireframe, label) = match style.as_str() {
"" | "SHADED" | "S" | "REALISTIC" | "CONCEPTUAL" => (false, "Shaded"),
"2DWIREFRAME" | "3DWIREFRAME" | "WIREFRAME" | "W" => (true, "Wireframe"),
"HIDDENLINE" | "HIDDEN" | "H" => (false, "Hidden (shown shaded)"),
"XRAY" | "X" => (true, "X-Ray (shown as wireframe)"),
_ => {
self.command_line.push_error(
"Usage: VSCURRENT <2dwireframe|wireframe|hidden|realistic|conceptual|shaded|xray>",
);
use crate::modules::view::visual_style;
let style = cmd.split_whitespace().nth(1).unwrap_or("");
let Some(mode) = visual_style::mode_for_keyword(style) else {
self.command_line
.push_error(visual_style::keyword_prompt());
return Some(Task::none());
}
};
let label = visual_style::label_for(mode);
self.command_line
.push_output(crate::tf!("Visual style: {label}.").as_ref());
return Some(Task::done(Message::SetWireframe(wireframe)));
return Some(Task::done(Message::SetRenderMode(mode)));
}
// CLOSE — close the active drawing tab (with the unsaved-changes
// prompt the tab-close handler already runs).

View file

@ -327,12 +327,6 @@ inventory::submit!(crate::command::CommandRegistration {
// Visual styles (mapped to the wireframe / shaded view).
"VSCURRENT",
"SHADEMODE",
"HIDDENLINE",
"XRAY",
"REALISTIC",
"CONCEPTUAL",
"2DWIREFRAME",
"3DWIREFRAME",
// Raster image brightness / contrast / fade.
"ADJUST",
// Block list + block-attribute list (command-line forms).
@ -607,7 +601,6 @@ inventory::submit!(crate::command::CommandRegistration {
"WB",
"WBLOCK",
"WEBVERSION",
"WIREFRAME",
"XA",
"XATTACH",
"XDATA",

View file

@ -1050,26 +1050,18 @@ impl OpenCADStudio {
)));
}
// VISUALSTYLES <name> — apply a built-in visual style to the active
// viewport via its render mode (the style-definition manager dialog
// is not modelled; this applies the standard styles).
cmd if cmd == "VISUALSTYLES" || cmd.starts_with("VISUALSTYLES ") => {
use acadrust::entities::ViewportRenderMode as VRM;
let name = cmd.strip_prefix("VISUALSTYLES").unwrap_or("").trim().to_uppercase();
let mode = match name.as_str() {
"2DWIREFRAME" | "2D" => Some(VRM::Wireframe2D),
"3DWIREFRAME" | "WIREFRAME" | "3D" => Some(VRM::Wireframe3D),
"HIDDEN" | "HIDDENLINE" => Some(VRM::HiddenLine),
"FLAT" | "FLATSHADED" => Some(VRM::FlatShaded),
"REALISTIC" | "SHADED" | "GOURAUD" => Some(VRM::GouraudShaded),
"CONCEPTUAL" | "SHADEDWITHEDGES" => Some(VRM::GouraudShadedWithEdges),
_ => None,
};
match mode {
Some(m) => return Some(Task::done(Message::SetRenderMode(m))),
None => self.command_line.push_info(
"VISUALSTYLES <2DWIREFRAME|3DWIREFRAME|HIDDEN|REALISTIC|CONCEPTUAL|SHADED>",
),
// VISUALSTYLES <name> — put the active viewport in one of the seven
// render modes. The style-definition manager is not modelled.
cmd if cmd.starts_with("VISUALSTYLES ") => {
use crate::modules::view::visual_style;
let name = cmd.strip_prefix("VISUALSTYLES").unwrap_or("").trim();
match visual_style::mode_for_keyword(name) {
Some(mode) => return Some(Task::done(Message::SetRenderMode(mode))),
// Listed from the table, so the names offered are the names
// that work.
None => self
.command_line
.push_info(visual_style::keyword_prompt()),
}
}

View file

@ -1763,11 +1763,10 @@ pub enum Message {
OptionsThemeColorChanged(usize, String),
/// Switch the interface language and redraw localized views.
LanguageChanged(crate::i18n::Language),
/// Drop every entity from the active drawing.
ClearScene,
SetWireframe(bool),
/// Set the active tab's render mode (one of acadrust's seven visual
/// styles). Replaces the binary `SetWireframe` over time; the older
/// message stays for ribbon/CLI back-compat and forwards.
/// Set the active tab's render mode — one of the seven visual styles, and
/// the only way a style is ever set.
SetRenderMode(acadrust::entities::ViewportRenderMode),
/// Open or close the active viewport's visual-style flyout.
ToggleRenderModeMenu(acadrust::entities::ViewportRenderMode),

View file

@ -158,21 +158,6 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.tabs[i].properties = PropertiesPanel::empty();
self.command_line.push_output(crate::t!("Scene cleared.").as_ref());
}
ModuleEvent::SetWireframe(w) => {
let i = self.active_tab;
self.tabs[i].wireframe = w;
self.ribbon.set_wireframe(w);
self.tabs[i].visual_style = if w {
"Wireframe".into()
} else {
"Shaded".into()
};
self.command_line.push_output(if w {
"Visual style: Wireframe"
} else {
"Visual style: Shaded"
});
}
ModuleEvent::ToggleLayers => {
return Task::done(Message::ToggleLayers);
}

View file

@ -1188,18 +1188,6 @@ impl OpenCADStudio {
Task::none()
}
Message::SetWireframe(w) => {
// Back-compat shim: forward to the new render-mode path so
// the ribbon button + WIREFRAME / SOLID command line still
// work without duplicating the rendering plumbing.
let mode = if w {
acadrust::entities::ViewportRenderMode::Wireframe2D
} else {
acadrust::entities::ViewportRenderMode::FlatShaded
};
Task::done(Message::SetRenderMode(mode))
}
Message::SetRenderMode(mode) => {
self.render_mode_menu_open = false;
self.render_mode_preview = None;

View file

@ -38,8 +38,8 @@ impl canvas::Program<Message> for RenderModePreview {
bounds: Rectangle,
_cursor: iced::mouse::Cursor,
) -> Vec<canvas::Geometry> {
use acadrust::entities::ViewportRenderMode as M;
use acadrust::entities::ViewportRenderMode as M;
let mut frame = canvas::Frame::new(renderer, bounds.size());
let palette = theme.palette();
let ink = palette.background.base.text.scale_alpha(0.86);
@ -214,16 +214,10 @@ pub(super) fn viewport_controls<'a>(
render_mode_menu_open: bool,
render_mode_preview: Option<acadrust::entities::ViewportRenderMode>,
) -> Element<'a, Message> {
use acadrust::entities::ViewportRenderMode as M;
let render_modes = [
RenderModeChoice(M::Wireframe2D),
RenderModeChoice(M::Wireframe3D),
RenderModeChoice(M::HiddenLine),
RenderModeChoice(M::FlatShaded),
RenderModeChoice(M::GouraudShaded),
RenderModeChoice(M::FlatShadedWithEdges),
RenderModeChoice(M::GouraudShadedWithEdges),
];
let render_modes: Vec<RenderModeChoice> = crate::modules::view::visual_style::VISUAL_STYLES
.iter()
.map(|style| RenderModeChoice(style.mode))
.collect();
let danger_btn = move |bytes: &'static [u8],
msg: Message,
title: String,

View file

@ -163,16 +163,7 @@ pub(super) struct RenderModeChoice(pub acadrust::entities::ViewportRenderMode);
impl std::fmt::Display for RenderModeChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use acadrust::entities::ViewportRenderMode as M;
f.write_str(match self.0 {
M::Wireframe2D => "Wireframe 2D",
M::Wireframe3D => "Wireframe 3D",
M::HiddenLine => "Hidden Line",
M::FlatShaded => "Flat Shaded",
M::GouraudShaded => "Gouraud Shaded",
M::FlatShadedWithEdges => "Flat Shaded + Edges",
M::GouraudShadedWithEdges => "Gouraud Shaded + Edges",
})
f.write_str(crate::modules::view::visual_style::label_for(self.0))
}
}

View file

@ -1,10 +0,0 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/hidden.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "HIDDENLINE",
label: "Hidden",
icon: ICON,
event: ModuleEvent::Command("HIDDENLINE".to_string()),
}
}

View file

@ -2,7 +2,6 @@
mod cascade;
mod file_tabs;
mod hidden;
mod layout_tabs;
pub mod limits;
mod orbit;
@ -13,9 +12,9 @@ pub mod plot_window;
pub mod quick_print;
mod properties_palette;
mod sheetset;
mod solid;
mod tile_horiz;
mod tile_vert;
pub mod visual_style;
mod tool_palettes;
pub mod ucs_cmd;
mod ucs_icon;
@ -28,8 +27,6 @@ mod vports_config;
mod vports_join;
mod vports_named;
mod vports_restore;
mod wireframe;
mod xray;
mod zoom_ext;
mod zoom_in;
mod zoom_out;
@ -89,14 +86,12 @@ impl CadModule for ViewModule {
tools: vec![RibbonItem::LargeDropdown {
id: "VISUAL_STYLE",
label: "Visual\nStyle",
icon: wireframe::tool().icon,
items: vec![
("WIREFRAME", "Wireframe", wireframe::tool().icon),
("SOLID", "Shaded", solid::tool().icon),
("HIDDEN", "Hidden", hidden::tool().icon),
("XRAY", "X-Ray", xray::tool().icon),
],
default: "WIREFRAME",
icon: visual_style::VISUAL_STYLES[0].icon,
items: visual_style::VISUAL_STYLES
.iter()
.map(|style| (style.command, style.label, style.icon))
.collect(),
default: visual_style::VISUAL_STYLES[0].command,
}],
},
// ── Projection ────────────────────────────────────────────────────

View file

@ -1,13 +0,0 @@
// Solid (Shaded) visual style toggle.
// id "SOLID" is special-cased in ribbon.rs for active-state highlighting.
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/solid.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "SOLID",
label: "Solid",
icon: ICON,
event: ModuleEvent::SetWireframe(false),
}
}

View file

@ -0,0 +1,126 @@
// The visual styles a viewport can be drawn in, in one list.
//
// Three places offer the same choice and used to describe it separately: the
// Visual Style dropdown on the ribbon, the render-mode picker with its preview
// cube, and the VISUALSTYLES command. The ribbon's copy had drifted furthest —
// it named four styles, two of which reached nothing (its "Shaded" carried the
// id `SOLID`, which is the 2D solid *drawing* command, and `HIDDEN` matched no
// command at all). Naming each style once, next to the command that applies it,
// is what keeps the three in step. (#621)
use std::sync::OnceLock;
use acadrust::entities::ViewportRenderMode as Mode;
use crate::modules::IconKind;
const WIREFRAME_ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/wireframe.svg"));
const HIDDEN_ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/hidden.svg"));
const SHADED_ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/solid.svg"));
pub struct VisualStyle {
pub mode: Mode,
/// What the user sees, wherever the style is offered.
pub label: &'static str,
/// The command line that applies it. Ribbon items dispatch this verbatim,
/// so it doubles as the item's identity for the checkmark and the
/// last-used-tool memory. The keyword is the render mode's own name — there
/// is one set of styles now, so there is one set of names for them.
pub command: &'static str,
pub icon: IconKind,
}
/// Every style, in the order they are offered — wireframes, then hidden line,
/// then the shaded ones, each pair plain before with-edges.
pub const VISUAL_STYLES: &[VisualStyle] = &[
VisualStyle {
mode: Mode::Wireframe2D,
label: "Wireframe 2D",
command: "VISUALSTYLES WIREFRAME2D",
icon: WIREFRAME_ICON,
},
VisualStyle {
mode: Mode::Wireframe3D,
label: "Wireframe 3D",
command: "VISUALSTYLES WIREFRAME3D",
icon: WIREFRAME_ICON,
},
VisualStyle {
mode: Mode::HiddenLine,
label: "Hidden Line",
command: "VISUALSTYLES HIDDENLINE",
icon: HIDDEN_ICON,
},
VisualStyle {
mode: Mode::FlatShaded,
label: "Flat Shaded",
command: "VISUALSTYLES FLATSHADED",
icon: SHADED_ICON,
},
VisualStyle {
mode: Mode::GouraudShaded,
label: "Gouraud Shaded",
command: "VISUALSTYLES GOURAUDSHADED",
icon: SHADED_ICON,
},
VisualStyle {
mode: Mode::FlatShadedWithEdges,
label: "Flat Shaded + Edges",
command: "VISUALSTYLES FLATSHADEDWITHEDGES",
icon: SHADED_ICON,
},
VisualStyle {
mode: Mode::GouraudShadedWithEdges,
label: "Gouraud Shaded + Edges",
command: "VISUALSTYLES GOURAUDSHADEDWITHEDGES",
icon: SHADED_ICON,
},
];
impl VisualStyle {
/// The bare keyword, without the verb its `command` spells out.
pub fn keyword(&self) -> &'static str {
self.command
.strip_prefix("VISUALSTYLES ")
.unwrap_or(self.command)
}
}
/// The choices an interactive style prompt offers, in table order.
pub fn keyword_choices() -> Vec<(&'static str, &'static str, Option<&'static str>)> {
VISUAL_STYLES
.iter()
.map(|style| (style.label, style.keyword(), None))
.collect()
}
/// The prompt those choices are announced with. Built once from the table so
/// the line the user reads cannot list something the picker does not offer.
pub fn keyword_prompt() -> &'static str {
static PROMPT: OnceLock<String> = OnceLock::new();
PROMPT
.get_or_init(|| {
let listed: Vec<&str> = VISUAL_STYLES.iter().map(|style| style.label).collect();
format!("Visual style [{}]:", listed.join(" / "))
})
.as_str()
}
pub fn label_for(mode: Mode) -> &'static str {
VISUAL_STYLES
.iter()
.find(|style| style.mode == mode)
.map(|style| style.label)
.unwrap_or("Wireframe 2D")
}
/// The style a style keyword names. Only the seven exist; nothing maps onto a
/// nearest neighbour, because there is no longer anything else to map from.
pub fn mode_for_keyword(keyword: &str) -> Option<Mode> {
let keyword = keyword.trim().to_uppercase();
VISUAL_STYLES
.iter()
.find(|style| style.keyword() == keyword)
.map(|style| style.mode)
}

View file

@ -1,13 +0,0 @@
// Wireframe visual style toggle.
// id "WIREFRAME" is special-cased in ribbon.rs for active-state highlighting.
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/wireframe.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "WIREFRAME",
label: "Wireframe",
icon: ICON,
event: ModuleEvent::SetWireframe(true),
}
}

View file

@ -1,10 +0,0 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xray.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "XRAY",
label: "X-Ray",
icon: ICON,
event: ModuleEvent::Command("XRAY".to_string()),
}
}

View file

@ -321,7 +321,6 @@ impl Ribbon {
fn toggle_state(&self) -> widgets::ToggleState {
use widgets::ToggleState;
ToggleState {
wireframe: self.wireframe,
ortho_mode: self.ortho_mode,
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,

View file

@ -33,7 +33,6 @@ use super::LayerInfo;
/// and call site.
#[derive(Clone, Copy)]
pub(super) struct ToggleState {
pub wireframe: bool,
pub ortho_mode: bool,
pub show_viewcube: bool,
pub show_ucs_icon: bool,
@ -355,8 +354,6 @@ pub(super) fn is_active_tool(
state: &ToggleState,
) -> bool {
match id {
"WIREFRAME" => state.wireframe,
"SOLID" => !state.wireframe,
"ORTHO" => state.ortho_mode,
"PERSP" => !state.ortho_mode,
"NAVVCUBE" => state.show_viewcube,
@ -1125,7 +1122,6 @@ pub fn module_event_to_message(event: ModuleEvent) -> Message {
ModuleEvent::Command(cmd) => Message::Command(cmd),
ModuleEvent::OpenFileDialog => Message::OpenFile,
ModuleEvent::ClearModels => Message::ClearScene,
ModuleEvent::SetWireframe(w) => Message::SetWireframe(w),
ModuleEvent::ToggleLayers => Message::ToggleLayers,
// Needs the tool context + async picker — route through the normal
// ribbon-click handler rather than a direct 1:1 message.