feat(view): add visual style previews

- preview render modes on hover without committing
- distinguish 2D and 3D wireframe fill and depth behavior

Refs #483
This commit is contained in:
Hakan Seven 2026-08-04 21:23:25 +03:00
commit b1d55c6a7e
49 changed files with 560 additions and 51 deletions

View file

@ -48,6 +48,9 @@ pub struct LocalWire {
pub tangent_geoms: Vec<TangentGeom>,
pub fill_tris: Vec<[f32; 3]>,
pub fill_tris_low: Vec<[f32; 3]>,
/// Preserves the planar SOLID classification through block expansion so
/// it never gets merged with unrelated annotation fills of the same style.
pub fill_is_2d_solid: bool,
/// Thickness-wall pick geometry, transformed by the insert like `points`
/// so a block child's extruded wall stays selectable at the instance's
/// place and scale. Never reaches the GPU.
@ -736,6 +739,7 @@ fn tessellate_sub_local(
tangent_geoms: wire.tangent_geoms,
fill_tris: wire.fill_tris,
fill_tris_low: wire.fill_tris_low,
fill_is_2d_solid: wire.fill_is_2d_solid,
pick_tris: wire.pick_tris,
pick_tris_low: wire.pick_tris_low,
color: wire.color,
@ -1191,6 +1195,9 @@ struct StyleKey {
/// discriminator, so greek fills must stay in their own batches even
/// when their color/style would otherwise collide with regular wires.
is_fill_only: bool,
/// Part of the batch key so planar SOLID fills remain independently
/// switchable after block geometry is merged by style.
fill_is_2d_solid: bool,
/// Bit-cast composed block-local depth for band wires (`0` = no override).
/// Keeps bands of different in-block draw ranks in separate batches so
/// each finalized WireModel carries one correct `depth_override`.
@ -1220,6 +1227,7 @@ struct BatchEntry {
/// reconstructs `high + low`). Without it absolute f32 fills quantize to
/// ~0.5 m and the greek-text rectangles shear.
fill_tris_low: Vec<[f32; 3]>,
fill_is_2d_solid: bool,
/// Accumulated thickness-wall pick geometry, paired high/low like
/// `fill_tris`. Pick-only — no GPU batch reads this.
pick_tris: Vec<[f32; 3]>,
@ -1261,6 +1269,7 @@ impl BatchEntry {
aci: u8,
plinegen: bool,
_is_fill_only: bool,
fill_is_2d_solid: bool,
) -> Self {
// `is_fill_only` is part of the StyleKey hash so greek fills never
// share a batch with regular wires (otherwise the finalized
@ -1275,6 +1284,7 @@ impl BatchEntry {
world_width,
aci,
plinegen,
fill_is_2d_solid,
min_x: f32::INFINITY,
min_y: f32::INFINITY,
max_x: f32::NEG_INFINITY,
@ -1308,6 +1318,7 @@ impl Batches {
world_width: b.world_width,
depth_override: b.local_depth,
fill_is_3d: false,
fill_is_2d_solid: b.fill_is_2d_solid,
pick_tris: b.pick_tris,
pick_tris_low: b.pick_tris_low,
dash_from_start: false,
@ -1352,6 +1363,7 @@ fn style_key(
aci: u8,
plinegen: bool,
is_fill_only: bool,
fill_is_2d_solid: bool,
local_depth: Option<f32>,
) -> StyleKey {
StyleKey {
@ -1377,6 +1389,7 @@ fn style_key(
aci,
plinegen,
is_fill_only,
fill_is_2d_solid,
depth_bits: local_depth.map_or(0, f32::to_bits),
}
}
@ -1708,6 +1721,7 @@ fn emit_wire(
lw.aci,
lw.plinegen,
lw.is_fill_only,
lw.fill_is_2d_solid,
local_depth,
);
@ -1730,6 +1744,7 @@ fn emit_wire(
lw.aci,
lw.plinegen,
lw.is_fill_only,
lw.fill_is_2d_solid,
)
});
entry.local_depth = local_depth;

View file

@ -712,6 +712,7 @@ pub(crate) fn tessellate_entity(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,
@ -961,6 +962,7 @@ pub(crate) fn tessellate_entity(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1402,6 +1404,7 @@ fn lod_stub_wire(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1491,6 +1494,7 @@ fn lod_stub_wire_3d(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,

View file

@ -376,6 +376,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
// MLINE dashes: A-type aligned, but the end-dash length is
@ -674,6 +675,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -714,6 +716,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -761,6 +764,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -789,6 +793,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -847,6 +852,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -887,6 +893,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -924,6 +931,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -954,6 +962,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1006,6 +1015,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1057,6 +1067,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1101,6 +1112,7 @@ pub fn tessellate(
world_width: polyline_band_width(entity),
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,
@ -1194,6 +1206,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,
@ -1253,6 +1266,7 @@ pub fn tessellate(
fill_tris,
fill_tris_low,
fill_is_3d,
fill_is_2d_solid: matches!(entity, EntityType::Solid(_)),
depth_override: None,
});
}
@ -1263,6 +1277,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -1306,6 +1321,7 @@ pub fn tessellate(
world_width: polyline_band_width(entity),
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,
@ -1350,6 +1366,7 @@ pub fn tessellate(
world_width,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,
@ -1482,6 +1499,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris,
pick_tris_low,
dash_from_start: false,

View file

@ -1649,6 +1649,9 @@ pub struct Scene {
/// constants. `[depth, half]` retains a fixed child sub-range for block
/// composition. Full sort/layout/block changes rebuild the labels.
draw_depth_cache: RefCell<Option<DrawDepthCache>>,
/// Shared empty map for 3-D wireframe, where true depth wins instead of
/// the entity submission order used by the optimized 2-D style.
no_draw_depths: Arc<HashMap<u64, [f32; 2]>>,
/// Cached hatch fill models, keyed by target block and geometry_epoch. View culling
/// is handled at draw time via `hatch_skip_flags` in the pipeline,
/// not at build time — that lets the GPU buffer stay stable across
@ -1960,6 +1963,7 @@ impl Scene {
interaction_handle_index_cache: RefCell::new(None),
sort_cache: RefCell::new(None),
draw_depth_cache: RefCell::new(None),
no_draw_depths: Arc::new(HashMap::default()),
hatch_cache: RefCell::new(HashMap::default()),
wipeout_cache: RefCell::new(HashMap::default()),
image_cache: RefCell::new(HashMap::default()),

View file

@ -145,6 +145,9 @@ pub struct WireModel {
/// every view mode. The render pass can't infer this from `fill_tris_low`
/// alone — a 2-D fill at UTM scale carries a low residual too.
pub fill_is_3d: bool,
/// `true` only for a planar SOLID entity's interior. Wireframe 3D omits
/// this fill while preserving its perimeter and every other 2-D overlay.
pub fill_is_2d_solid: bool,
}
impl WireModel {
@ -196,6 +199,7 @@ impl WireModel {
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
text_verts: Vec::new(),
@ -430,6 +434,7 @@ impl Default for WireModel {
fill_tris_low: Vec::new(),
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
}

View file

@ -82,8 +82,10 @@ impl Scene {
/// scene camera (tiled splits will append more later). `model_mode`
/// supplies its render mode (held on the tab, not the scene).
/// - **Paper layout**: one instance per content viewport entity
/// (`id > 1`, owned by the current layout block, switched on),
/// using each viewport's own camera and render mode.
/// (`id > 1`, owned by the current layout block, switched on), using each
/// viewport's own camera and render mode. `model_mode` temporarily
/// overrides the active viewport so the visual-style gallery can preview
/// on hover without modifying the document.
pub fn active_viewports(
&self,
canvas_w: f32,
@ -182,7 +184,11 @@ impl Scene {
tile_idx: None,
screen_rect,
camera,
render_mode: vp.render_mode,
render_mode: if self.active_viewport == Some(h) {
model_mode
} else {
vp.render_mode
},
active: self.active_viewport == Some(h),
grid_on: vp.status.grid_on,
paper_sheet: false,

View file

@ -263,6 +263,7 @@ pub fn frame_wire(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -783,4 +784,3 @@ mod tests {
assert!(out.iter().all(|p| p[0] <= 10.0 + 1e-3));
}
}

View file

@ -108,12 +108,15 @@ impl Face3DGpu {
/// true depth; fills with empty `fill_tris_low` (2D fills — text greek,
/// MultiLeader / dimension backgrounds) feed the 2D draw-order buffer.
/// - `keep_3d_mesh_fills`: when false (wireframe modes), the 3D side
/// is left empty; the 2D side is always populated.
/// is left empty.
/// - `show_2d_solid_fills`: false only for Wireframe 3D; removes the
/// legacy planar SOLID interior while keeping other 2-D overlays.
pub fn from_wires(
device: &wgpu::Device,
face3d_wires: &[WireModel],
all_wires: &[WireModel],
keep_3d_mesh_fills: bool,
show_2d_solid_fills: bool,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Self {
let depth_of =
@ -168,13 +171,17 @@ impl Face3DGpu {
// with both fill_tris and points (mesh edges + faces) keep the dim
// so PolyfaceMesh / PolygonMesh still look 3-D-shaded.
//
// 2-D fills always go to `verts_2d` (visible in every mode).
// 2-D fills go to `verts_2d`; only legacy planar SOLID interiors are
// omitted by Wireframe 3D.
// 3-D mesh face data goes to `verts_3d` only when
// `keep_3d_mesh_fills` is true.
for wire in all_wires {
if wire.fill_tris.is_empty() {
continue;
}
if !show_2d_solid_fills && wire.fill_is_2d_solid {
continue;
}
// A real 3-D surface fill (PolyfaceMesh / PolygonMesh) carries a
// double-single low residual paired with `fill_tris` — it lives at
// true world coordinates and must keep its real depth. 2-D fills

View file

@ -296,11 +296,12 @@ pub struct Pipeline {
/// a pick bumps only `selection_generation`, refreshing the overlay without
/// touching the main wire buffers.
pub cached_selection: (u64, u64),
/// `(wire_content_id, face3d_fill_active)` the Face3D edge/fill buffers were
/// uploaded for. A stable content id avoids retaining the resident wire Arc:
/// `(wire_content_id, face3d_fill_active, show_2d_solid_fills)` the Face3D
/// edge/fill buffers were uploaded for. A stable content id avoids retaining
/// the resident wire Arc:
/// that Arc must stay uniquely owned by Scene so a small edit can splice it
/// in place instead of rebuilding the whole drawing.
pub cached_face3d_key: (u64, bool),
pub cached_face3d_key: (u64, bool, bool),
/// Handle → indices into the resident wire set, built once per wire upload
/// (when `cached_wire_id` changes). Lets the selection/hover xray overlay
/// gather just the highlighted entity's wires (`O(highlighted)`) instead of
@ -1710,7 +1711,7 @@ impl Pipeline {
cached_epoch: (u64::MAX, u64::MAX, u64::MAX),
cached_wire_id: u64::MAX,
cached_selection: (u64::MAX, u64::MAX),
cached_face3d_key: (u64::MAX, false),
cached_face3d_key: (u64::MAX, false, false),
wire_handle_index: std::sync::Arc::new(rustc_hash::FxHashMap::default()),
render_sig: u64::MAX,
skip_geometry: false,
@ -2269,6 +2270,7 @@ impl Pipeline {
face3d_wires: &[WireModel],
all_wires: &[WireModel],
wireframe_only: bool,
show_2d_solid_fills: bool,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
@ -2279,11 +2281,15 @@ impl Pipeline {
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
// tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`);
// 2D fills (text-LOD greek, MultiLeader background) go to
// `chunks_2d` and are visible in every mode.
// `chunks_2d`. The 3-D wireframe additionally removes only legacy
// planar SOLID interiors; HATCH is handled by a separate pass.
let keep_3d_mesh_fills = !wireframe_only;
let solid_fill_hidden = |wire: &WireModel| {
!show_2d_solid_fills && wire.fill_is_2d_solid
};
let has_any_2d_fill = all_wires
.iter()
.any(|w| !w.fill_tris.is_empty() && w.points.is_empty());
.any(|w| !w.fill_tris.is_empty() && w.points.is_empty() && !solid_fill_hidden(w));
let has_any_3d_fill = !face3d_wires.is_empty()
|| all_wires
.iter()
@ -2297,6 +2303,7 @@ impl Pipeline {
face3d_wires,
all_wires,
keep_3d_mesh_fills,
show_2d_solid_fills,
depth_map,
));
}

View file

@ -313,6 +313,7 @@ pub fn apply_along(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@ -355,6 +356,7 @@ pub fn apply_along(
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,

View file

@ -100,6 +100,10 @@ pub struct ViewportData {
/// Hatch / wipeout uploads are deliberately *not* gated by this flag —
/// the user toggle should only affect 3D solids, not 2D fills.
pub(in crate::scene) view_wireframe: bool,
/// Legacy planar SOLID interiors remain filled in the optimized 2-D
/// wireframe, but become outlines in the 3-D wireframe. HATCH uses its
/// own pipeline and is intentionally unaffected.
pub(in crate::scene) show_2d_solid_fills: 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
@ -169,7 +173,9 @@ pub struct Primitive {
/// 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 uses a depth
/// edges. The optimized 2-D wireframe retains planar SOLID interiors and
/// entity draw order; the 3-D wireframe uses true depth and outlines them.
/// `*WithEdges` variants render both. HiddenLine uses a depth
/// prepass: face/mesh fills are uploaded but routed through depth-only
/// pipelines so hidden edges drop out. `FlatShaded` vs `GouraudShaded`
/// differ in shader uniform only and produce identical fill flags here.
@ -179,6 +185,7 @@ pub struct RenderModeFlags {
pub mesh_fill: bool,
pub show_3d_edges: bool,
pub hidden_line: bool,
pub show_2d_solid_fills: bool,
/// `true` for FlatShaded / FlatShadedWithEdges. The mesh shader
/// reads `Uniforms.flat_shade` and replaces the smooth per-vertex
/// normal with a per-triangle face normal so each triangle reads
@ -191,11 +198,20 @@ pub fn render_mode_flags(
) -> RenderModeFlags {
use acadrust::entities::ViewportRenderMode as M;
match mode {
M::Wireframe2D | M::Wireframe3D => RenderModeFlags {
M::Wireframe2D => RenderModeFlags {
face3d_fill: false,
mesh_fill: false,
show_3d_edges: true,
hidden_line: false,
show_2d_solid_fills: true,
flat_shade: false,
},
M::Wireframe3D => RenderModeFlags {
face3d_fill: false,
mesh_fill: false,
show_3d_edges: true,
hidden_line: false,
show_2d_solid_fills: false,
flat_shade: false,
},
M::HiddenLine => RenderModeFlags {
@ -203,6 +219,7 @@ pub fn render_mode_flags(
mesh_fill: true,
show_3d_edges: true,
hidden_line: true,
show_2d_solid_fills: true,
flat_shade: false,
},
M::FlatShaded => RenderModeFlags {
@ -210,6 +227,7 @@ pub fn render_mode_flags(
mesh_fill: true,
show_3d_edges: false,
hidden_line: false,
show_2d_solid_fills: true,
flat_shade: true,
},
M::GouraudShaded => RenderModeFlags {
@ -217,6 +235,7 @@ pub fn render_mode_flags(
mesh_fill: true,
show_3d_edges: false,
hidden_line: false,
show_2d_solid_fills: true,
flat_shade: false,
},
M::FlatShadedWithEdges => RenderModeFlags {
@ -224,6 +243,7 @@ pub fn render_mode_flags(
mesh_fill: true,
show_3d_edges: true,
hidden_line: false,
show_2d_solid_fills: true,
flat_shade: true,
},
M::GouraudShadedWithEdges => RenderModeFlags {
@ -231,6 +251,7 @@ pub fn render_mode_flags(
mesh_fill: true,
show_3d_edges: true,
hidden_line: false,
show_2d_solid_fills: true,
flat_shade: false,
},
}
@ -271,7 +292,7 @@ impl shader::Primitive for Primitive {
inner.cached_wire_id = u64::MAX;
inner.cached_selection = (u64::MAX, u64::MAX);
inner.cached_mesh_content_id = u64::MAX;
inner.cached_face3d_key = (u64::MAX, false);
inner.cached_face3d_key = (u64::MAX, false, false);
inner.cached_hatch_source = None;
inner.cached_preview_hatch_source = None;
inner.cached_wipeout_source = None;
@ -440,18 +461,26 @@ impl shader::Primitive for Primitive {
.map_or(true, |source| !Arc::ptr_eq(&source, &draw_depths))
|| (inner.cached_face3d_key.0 != vp.wire_content_id
&& !face_pass_unchanged);
if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 {
if face3d_changed
|| face3d_fill_active != inner.cached_face3d_key.1
|| vp.show_2d_solid_fills != inner.cached_face3d_key.2
{
inner.upload_face3d(
device,
&vp.face3d_wires[..],
&vp_wires[..],
!face3d_fill_active,
vp.show_2d_solid_fills,
&draw_depths,
);
inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires));
inner.cached_face3d_depth_source = Some(Arc::downgrade(&draw_depths));
}
inner.cached_face3d_key = (vp.wire_content_id, face3d_fill_active);
inner.cached_face3d_key = (
vp.wire_content_id,
face3d_fill_active,
vp.show_2d_solid_fills,
);
// Wire buffers are world-space, so a camera move alone doesn't
// change them — only the view_proj uniform (uploaded every frame).
// Gate the upload on the wire content id instead of the camera tick:
@ -1117,6 +1146,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
vp.wire_content_id.hash(&mut h);
vp.fill_mode.hash(&mut h);
vp.view_wireframe.hash(&mut h);
vp.show_2d_solid_fills.hash(&mut h);
vp.mesh_fill.hash(&mut h);
vp.show_3d_edges.hash(&mut h);
vp.hidden_line.hash(&mut h);
@ -2272,15 +2302,15 @@ impl Scene {
// are NOT part of this buffer anymore (they go in a separate per-frame
// overlay buffer below), so the base id is the source's stable content
// gen — a drag or camera move never re-uploads the base wire set.
let wire_content_id = self.last_model_wire_gen.get();
let wire_patch = self.model_wire_patch_for(wire_content_id);
let base_wire_content_id = self.last_model_wire_gen.get();
let base_wire_patch = self.model_wire_patch_for(base_wire_content_id);
// Split Face3D wires from the rest. The split is content-only (keyed
// by the wire-set content id), so while the geometry is unchanged it's
// memoized rather than re-walking every wire (handle lookup + clone)
// each frame — for every source, since all ids are stable now.
let (face3d_wires, other_arc) = {
let cached = { self.split_cache.borrow().get(&wire_content_id).cloned() };
let inherited_empty = if let Some((base, patch)) = wire_patch.as_ref() {
let cached = { self.split_cache.borrow().get(&base_wire_content_id).cloned() };
let inherited_empty = if let Some((base, patch)) = base_wire_patch.as_ref() {
if patch.face_pass_changed {
None
} else {
@ -2311,12 +2341,12 @@ impl Scene {
if c.len() > 8 {
c.clear();
}
c.insert(wire_content_id, (fa.clone(), oa.clone()));
c.insert(base_wire_content_id, (fa.clone(), oa.clone()));
(fa, oa)
});
self.split_cache
.borrow_mut()
.entry(wire_content_id)
.entry(base_wire_content_id)
.or_insert_with(|| (fa.clone(), oa.clone()));
(fa, oa.unwrap_or_else(|| Arc::clone(&base_arc)))
};
@ -2325,6 +2355,24 @@ impl Scene {
// per-frame buffer so the (potentially huge) base buffer stays resident
// and unchanged while a command preview or grip drag is live.
let all_wires = other_arc;
// The 3-D wireframe deliberately ignores entity draw order and lets
// true depth decide overlaps. Tag its resident wire id separately so
// switching between the 2-D and 3-D styles rebuilds the GPU constants
// even though the world-space geometry itself did not change. The
// incremental patch's base id receives the same tag, preserving the
// arena fast path after the first mode switch.
let wire_mode_tag = u64::from(
inst.render_mode == acadrust::entities::ViewportRenderMode::Wireframe3D,
);
let wire_content_id = base_wire_content_id
.wrapping_mul(2)
.wrapping_add(wire_mode_tag);
let wire_patch = base_wire_patch.map(|(base, patch)| {
(
base.wrapping_mul(2).wrapping_add(wire_mode_tag),
patch,
)
});
// A live overlay belongs to one drawing space, but every viewport that
// displays that space must project the same world-space preview. In a
// paper layout, model-space overlays go to all content viewports while
@ -2490,7 +2538,13 @@ impl Scene {
} else {
0x3000_0000_0000_0000 | inst.handle.value()
};
let draw_depths = self.draw_depth_map();
let draw_depths = if inst.render_mode
== acadrust::entities::ViewportRenderMode::Wireframe3D
{
Arc::clone(&self.no_draw_depths)
} else {
self.draw_depth_map()
};
let text_verts = self.gather_text_verts(
&all_wires,
wire_content_id,
@ -2567,6 +2621,7 @@ impl Scene {
show_viewcube: inst.active && show_viewcube,
fill_mode: self.document.header.fill_mode,
view_wireframe,
show_2d_solid_fills: flags.show_2d_solid_fills,
mesh_fill: flags.mesh_fill,
show_3d_edges: flags.show_3d_edges,
display_silhouette: flags.hidden_line || self.document.header.display_silhouette,

View file

@ -16,8 +16,9 @@ use iced::{mouse, Event, Rectangle};
pub struct ViewportPane<'a> {
pub scene: &'a Scene,
pub show_viewcube: bool,
/// Render mode applied to the Model layout's tiles. Paper-space content
/// viewports use the render mode stored on their own viewport entity.
/// Render mode applied to the active Model tile or active paper viewport.
/// The app may supply a temporary gallery-hover mode; inactive viewports
/// keep the values stored on their own tile/entity.
pub render_mode: acadrust::entities::ViewportRenderMode,
/// `Some(tile_idx)` → this widget renders a single Model pane (one shader
/// per `pane_grid` pane, filling its own bounds). `None` → the unified