feat(render): phase 0 — render-mode pick_list + 3-flag pipeline plumbing

Replaces the binary Wireframe/Solid toggle with a 7-mode dropdown
that maps onto acadrust's `ViewportRenderMode` enum: Wireframe 2D /
Wireframe 3D / Hidden Line / Flat Shaded / Gouraud Shaded / Flat
Shaded + Edges / Gouraud Shaded + Edges. Per-tab state lives on
`DocumentTab::render_mode`; the old `SetWireframe` message forwards
to `SetRenderMode` so the ribbon + WIREFRAME / SOLID command line
keep working.

`scene::render::render_mode_flags()` translates the mode into three
pipeline-facing booleans (`face3d_fill`, `mesh_fill`,
`show_3d_edges`); `mesh_fill` / `show_3d_edges` are stubs that the
upcoming phases will consume. `Primitive` carries the derived
`view_wireframe` plus the two new fields through to `prepare()`.

`Pipeline::upload_face3d` takes a `wireframe_only` flag that drops
3DFACE quad fills (built from `key_vertices`) while keeping the
`all_wires.fill_tris` channel — that channel carries text-LOD greek
rectangles and PolyfaceMesh face triangles which must stay visible
in wireframe mode. 3DFACE edges stay on regardless of mode.

Solid3D mesh wireframe rendering, Hidden Line z-prepass, and Flat /
Gouraud shader uniforms come in phases 1–4.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-27 00:08:37 +03:00
1 parent 5ea996d5fe
commit 2145ab8a66
8 files changed
+273 -90

No files matched your search

+3 -1
View File
@@ -27,6 +27,7 @@ pub(super) struct DocumentTab {
pub(super) selected_grips: Vec<GripDef>,
pub(super) selected_handle: Option<Handle>,
pub(super) wireframe: bool,
pub(super) render_mode: acadrust::entities::ViewportRenderMode,
pub(super) visual_style: String,
pub(super) last_cursor_world: glam::Vec3,
pub(super) last_cursor_screen: iced::Point,
@@ -80,7 +81,8 @@ impl DocumentTab {
selected_grips: vec![],
selected_handle: None,
wireframe: false,
visual_style: "Shaded".into(),
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
visual_style: "Wireframe 2D".into(),
last_cursor_world: glam::Vec3::ZERO,
last_cursor_screen: iced::Point::ORIGIN,
history: HistoryState::default(),
+4
View File
@@ -308,6 +308,10 @@ pub enum Message {
SaveDialogCancel,
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.
SetRenderMode(acadrust::entities::ViewportRenderMode),
/// Switch camera projection: true = Orthographic, false = Perspective.
SetProjection(bool),
/// Select a ribbon module tab by index.
+33 -11
View File
@@ -654,19 +654,41 @@ impl OpenCADStudio {
}
Message::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()
// 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 {
"Shaded".into()
acadrust::entities::ViewportRenderMode::FlatShaded
};
self.command_line.push_output(if w {
"Visual style: Wireframe"
} else {
"Visual style: Shaded"
});
Task::done(Message::SetRenderMode(mode))
}
Message::SetRenderMode(mode) => {
use acadrust::entities::ViewportRenderMode as M;
let i = self.active_tab;
self.tabs[i].render_mode = mode;
// Keep the legacy `wireframe` bool synced — both wireframe
// modes set it, everything else clears it.
let wf = matches!(mode, M::Wireframe2D | M::Wireframe3D);
self.tabs[i].wireframe = wf;
self.ribbon.set_wireframe(wf);
let label = match mode {
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",
};
self.tabs[i].visual_style = label.into();
// Re-upload face3d fills on the next frame — the render
// pipeline keys its upload cache off `geometry_epoch`.
self.tabs[i].scene.bump_geometry();
self.command_line
.push_output(&format!("Visual style: {label}"));
Task::none()
}
+91 -13
View File
@@ -16,6 +16,27 @@ use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Tas
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
/// `pick_list` requires its items to implement `Display`; acadrust's
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
/// so wrap it locally with a friendly name renderer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
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",
})
}
}
impl OpenCADStudio {
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
// ── Floating panel windows ─────────────────────────────────────────
@@ -236,7 +257,11 @@ impl OpenCADStudio {
} else if is_paper {
paper_canvas_view(tab)
} else {
shader(ViewportPane::model(&tab.scene, self.show_viewcube))
shader(ViewportPane::model(
&tab.scene,
self.show_viewcube,
tab.render_mode,
))
.width(Fill)
.height(Fill)
.into()
@@ -357,15 +382,53 @@ impl OpenCADStudio {
)
};
let info = container(overlay::info_bar(
if is_paper {
&tab.scene.current_layout
} else {
"Custom View"
// Render-mode picker, top-left of the viewport. Drives whether
// 3D face / mesh fills and edges are rendered for the active
// tab. Hatch fills are deliberately *not* gated by this — the
// document's FILLMODE still owns 2D fill state.
let render_modes: Vec<RenderModeChoice> = vec![
RenderModeChoice(acadrust::entities::ViewportRenderMode::Wireframe2D),
RenderModeChoice(acadrust::entities::ViewportRenderMode::Wireframe3D),
RenderModeChoice(acadrust::entities::ViewportRenderMode::HiddenLine),
RenderModeChoice(acadrust::entities::ViewportRenderMode::FlatShaded),
RenderModeChoice(acadrust::entities::ViewportRenderMode::GouraudShaded),
RenderModeChoice(acadrust::entities::ViewportRenderMode::FlatShadedWithEdges),
RenderModeChoice(acadrust::entities::ViewportRenderMode::GouraudShadedWithEdges),
];
let info = container(
iced::widget::pick_list(
render_modes,
Some(RenderModeChoice(tab.render_mode)),
|c| Message::SetRenderMode(c.0),
)
.text_size(11),
)
.style(|_: &Theme| iced::widget::container::Style {
background: Some(iced::Background::Color(Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 0.75,
})),
border: iced::Border {
color: Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
},
width: 1.0,
radius: 4.0.into(),
},
&tab.visual_style,
))
.padding([4, 6]);
text_color: Some(Color {
r: 0.85,
g: 0.85,
b: 0.85,
a: 1.0,
}),
..Default::default()
})
.padding([4, 8]);
let viewport_mouse = mouse_area(container(
iced::widget::Space::new().width(Fill).height(Fill),
@@ -443,20 +506,35 @@ impl OpenCADStudio {
})
.width(Fill)
.height(Fill),
container(info).width(Fill).height(Fill),
selection_overlay,
viewport_mouse,
// Toggle sits ABOVE the viewport mouse_area so clicks
// inside the toggle's bounds reach it instead of the
// shader behind it; `opaque` blocks the same clicks from
// bubbling further down. Outside the toggle's footprint
// the outer Fill container is transparent — viewport
// drawing / selection stays unaffected.
container(iced::widget::opaque(info))
.width(Fill)
.height(Fill)
.align_x(iced::alignment::Horizontal::Left)
.align_y(iced::alignment::Vertical::Top),
]
.width(Fill)
.height(Fill)
};
if self.show_navbar && !tab.is_start {
// Anchor the nav toolbar to the vertical centre of the
// viewport area, flush against the right edge with a small
// gutter, so it stays aligned no matter how tall the
// window is.
let nav = container(overlay::nav_toolbar())
.align_right(Fill)
.align_top(Fill)
.align_y(iced::alignment::Vertical::Center)
.height(Fill)
.padding(iced::Padding {
top: 148.0,
top: 0.0,
right: 8.0,
bottom: 0.0,
left: 0.0,
@@ -805,7 +883,7 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M
let w = rect.width.clamp(1.0, canvas_w - x);
let h = rect.height.clamp(1.0, canvas_h - y);
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle))
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle, tab.render_mode))
.width(iced::Length::Fixed(w))
.height(iced::Length::Fixed(h));