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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-27 00:08:37 +03:00
commit 2145ab8a66
8 changed files with 273 additions and 90 deletions

View file

@ -784,16 +784,25 @@ impl Pipeline {
device: &wgpu::Device,
face3d_wires: &[WireModel],
all_wires: &[WireModel],
wireframe_only: bool,
) {
let has_fills =
!face3d_wires.is_empty() || all_wires.iter().any(|w| !w.fill_tris.is_empty());
// Edge buffer is always built from `face3d_wires`, so 3DFACE
// outlines stay on the screen regardless of mode.
self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires);
// Fill buffer: in wireframe mode we drop the 3DFACE quads
// (those come from `face3d_wires.key_vertices`) but keep
// `all_wires.fill_tris` — that channel carries 2D content like
// text-LOD greek rectangles and PolyfaceMesh face triangles
// which shouldn't disappear with the toggle.
let face3d_for_fill: &[WireModel] = if wireframe_only { &[] } else { face3d_wires };
let has_fills = !face3d_for_fill.is_empty()
|| all_wires.iter().any(|w| !w.fill_tris.is_empty());
if !has_fills {
self.gpu_face3d_fill = None;
self.gpu_face3d_edges = vec![];
return;
} else {
self.gpu_face3d_fill =
Some(Face3DGpu::from_wires(device, face3d_for_fill, all_wires));
}
self.gpu_face3d_fill = Some(Face3DGpu::from_wires(device, face3d_wires, all_wires));
self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires);
}
pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) {

View file

@ -98,6 +98,22 @@ pub struct Primitive {
/// Header.fill_mode (FILLMODE): when false, hatch / wipeout / face3d-fill
/// uploads short-circuit so the renderer draws only wireframe.
pub(super) fill_mode: bool,
/// Per-view "Wireframe vs Solid" toggle. When `true`, 3D face fills
/// are dropped on the upload path so 3D faces draw as edges only.
/// Hatch / wipeout uploads are deliberately *not* gated by this flag —
/// the user toggle should only affect 3D solids, not 2D fills.
pub(super) view_wireframe: bool,
/// Whether the active render mode wants 3D mesh fills uploaded. Off
/// in `Wireframe2D` / `Wireframe3D`; on for every shaded variant. Set
/// at the same point `view_wireframe` is computed so the two stay in
/// lock-step for the gating logic in `prepare()`.
#[allow(dead_code)]
pub(super) mesh_fill: bool,
/// Whether the active render mode wants 3D mesh / face edges
/// rendered on top of fills. Most shaded modes turn this off; the
/// `*WithEdges` variants and the pure wireframes leave it on.
#[allow(dead_code)]
pub(super) show_3d_edges: bool,
pub(super) geometry_epoch: u64,
/// Camera generation captured when this Primitive was assembled. Paired
/// with `geometry_epoch` so the wire buffers re-upload when the view
@ -105,6 +121,48 @@ pub struct Primitive {
pub(super) camera_generation: u64,
}
/// Flags the render pipeline consumes, derived from
/// [`acadrust::entities::ViewportRenderMode`]. Each shaded variant fills
/// 3D faces and meshes; the pure wireframes drop the fill and keep only
/// edges. `*WithEdges` variants render both. `HiddenLine` is currently
/// rendered as plain solid (until Phase 2 wires up the depth-only
/// prepass); `FlatShaded` vs `GouraudShaded` differ in shader uniform
/// only and produce identical fill flags here.
#[derive(Clone, Copy, Debug)]
pub struct RenderModeFlags {
pub face3d_fill: bool,
pub mesh_fill: bool,
pub show_3d_edges: bool,
}
pub fn render_mode_flags(
mode: acadrust::entities::ViewportRenderMode,
) -> RenderModeFlags {
use acadrust::entities::ViewportRenderMode as M;
match mode {
M::Wireframe2D | M::Wireframe3D => RenderModeFlags {
face3d_fill: false,
mesh_fill: false,
show_3d_edges: true,
},
M::HiddenLine => RenderModeFlags {
face3d_fill: true,
mesh_fill: true,
show_3d_edges: true,
},
M::FlatShaded | M::GouraudShaded => RenderModeFlags {
face3d_fill: true,
mesh_fill: true,
show_3d_edges: false,
},
M::FlatShadedWithEdges | M::GouraudShadedWithEdges => RenderModeFlags {
face3d_fill: true,
mesh_fill: true,
show_3d_edges: true,
},
}
}
// ── shader::Primitive impl ────────────────────────────────────────────────
impl shader::Primitive for Primitive {
@ -132,6 +190,11 @@ impl shader::Primitive for Primitive {
pipeline.upload_uniforms(queue, &self.uniforms);
let cur_key = (self.geometry_epoch, self.camera_generation);
let fill_mode = self.fill_mode;
// 3D face fill requires *both* the doc-level FILLMODE *and* the
// per-view Solid toggle. Hatches / wipeouts deliberately ignore
// the view toggle so 2D fills stay on even when the user picks
// the Wireframe overlay style.
let face3d_fill_active = fill_mode && !self.view_wireframe;
if cur_key != pipeline.cached_epoch {
// Static buffers (hatches/images/meshes) only need refresh on a
// real geometry change, not on every camera tick.
@ -149,12 +212,15 @@ impl shader::Primitive for Primitive {
// Wires re-upload on every camera change because the visible
// subset shifts under frustum culling.
pipeline.upload_wires(device, &self.wires[..]);
if fill_mode {
pipeline.upload_face3d(device, &self.face3d_wires[..], &self.wires[..]);
} else {
// Edges still need to draw, but no fill_tris are forwarded.
pipeline.upload_face3d(device, &self.face3d_wires[..], &[]);
}
// `wireframe_only=true` keeps the face3d edge buffer but
// drops the fill — that's the on-screen result of toggling
// the render mode to Wireframe2D / Wireframe3D.
pipeline.upload_face3d(
device,
&self.face3d_wires[..],
&self.wires[..],
!face3d_fill_active,
);
pipeline.cached_epoch = cur_key;
}
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
@ -337,7 +403,10 @@ impl Scene {
hover_region: Option<usize>,
bounds: Rectangle,
show_viewcube: bool,
render_mode: acadrust::entities::ViewportRenderMode,
) -> Primitive {
let flags = render_mode_flags(render_mode);
let view_wireframe = !flags.face3d_fill;
let cam = self.camera.borrow();
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
// Record the active widget's aspect so view_world_aabb() can compute
@ -379,6 +448,9 @@ impl Scene {
bg_color,
show_viewcube,
fill_mode: self.document.header.fill_mode,
view_wireframe,
mesh_fill: flags.mesh_fill,
show_3d_edges: flags.show_3d_edges,
geometry_epoch: self.geometry_epoch,
camera_generation: self.camera_generation,
}
@ -391,10 +463,13 @@ impl Scene {
vp_handle: Handle,
hover_region: Option<usize>,
bounds: Rectangle,
render_mode: acadrust::entities::ViewportRenderMode,
) -> Primitive {
let flags = render_mode_flags(render_mode);
let view_wireframe = !flags.face3d_fill;
let cam = match self.camera_for_viewport(vp_handle) {
Some(c) => c,
None => return self.build_primitive(hover_region, bounds, false),
None => return self.build_primitive(hover_region, bounds, false, render_mode),
};
let base_arc = self.model_wires_for_viewport_arc(vp_handle);
@ -423,6 +498,9 @@ impl Scene {
bg_color: self.bg_color,
show_viewcube: false,
fill_mode: self.document.header.fill_mode,
view_wireframe,
mesh_fill: flags.mesh_fill,
show_3d_edges: flags.show_3d_edges,
geometry_epoch: self.geometry_epoch,
camera_generation: self.camera_generation,
}
@ -435,8 +513,11 @@ impl Scene {
vp_handle: Handle,
hover_region: Option<usize>,
bounds: Rectangle,
render_mode: acadrust::entities::ViewportRenderMode,
) -> PaperViewportPrimitive {
PaperViewportPrimitive(self.build_viewport_primitive(vp_handle, hover_region, bounds))
PaperViewportPrimitive(
self.build_viewport_primitive(vp_handle, hover_region, bounds, render_mode),
)
}
/// Update viewcube hover state from cursor position within `bounds`.

View file

@ -20,14 +20,20 @@ pub struct ViewportPane<'a> {
pub scene: &'a Scene,
pub mode: ViewportPaneMode,
pub show_viewcube: bool,
pub render_mode: acadrust::entities::ViewportRenderMode,
}
impl<'a> ViewportPane<'a> {
pub fn model(scene: &'a Scene, show_viewcube: bool) -> Self {
pub fn model(
scene: &'a Scene,
show_viewcube: bool,
render_mode: acadrust::entities::ViewportRenderMode,
) -> Self {
Self {
scene,
mode: ViewportPaneMode::Model,
show_viewcube,
render_mode,
}
}
@ -39,6 +45,7 @@ impl<'a> ViewportPane<'a> {
scene,
mode: ViewportPaneMode::Paper { handle },
show_viewcube: false,
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
}
}
}
@ -52,11 +59,20 @@ impl<'a> ViewportPane<'a> {
pub struct PaperViewportPane<'a> {
pub scene: &'a Scene,
pub handle: Handle,
pub render_mode: acadrust::entities::ViewportRenderMode,
}
impl<'a> PaperViewportPane<'a> {
pub fn new(scene: &'a Scene, handle: Handle) -> Self {
Self { scene, handle }
pub fn new(
scene: &'a Scene,
handle: Handle,
render_mode: acadrust::entities::ViewportRenderMode,
) -> Self {
Self {
scene,
handle,
render_mode,
}
}
}
@ -71,7 +87,12 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for PaperViewportPan
bounds: Rectangle,
) -> Self::Primitive {
self.scene
.build_active_viewport_primitive(self.handle, state.hover_region, bounds)
.build_active_viewport_primitive(
self.handle,
state.hover_region,
bounds,
self.render_mode,
)
}
fn update(
@ -109,14 +130,18 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
bounds: Rectangle,
) -> Self::Primitive {
match &self.mode {
ViewportPaneMode::Model => {
self.scene
.build_primitive(state.hover_region, bounds, self.show_viewcube)
}
ViewportPaneMode::Paper { handle } => {
self.scene
.build_viewport_primitive(*handle, state.hover_region, bounds)
}
ViewportPaneMode::Model => self.scene.build_primitive(
state.hover_region,
bounds,
self.show_viewcube,
self.render_mode,
),
ViewportPaneMode::Paper { handle } => self.scene.build_viewport_primitive(
*handle,
state.hover_region,
bounds,
self.render_mode,
),
}
}