From 77e1f1b876f62e7ec5621f7c129baf7e29f1d29d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 21 Aug 2026 18:57:23 +0300 Subject: [PATCH] fix(plot): respect non-printing layers Filter nested block, attribute, table, hatch, and wipeout geometry during export while preserving canvas visibility. --- src/app/update/file.rs | 2 +- src/app/update/mod.rs | 2 ++ src/entities/insert.rs | 19 ++++++++--- src/scene/cache/block_cache.rs | 58 +++++++++++++++++++++++++++++++--- src/scene/convert/tess.rs | 7 ++++ src/scene/entity.rs | 52 ++++++++++++++++++++---------- src/scene/mod.rs | 1 + src/scene/paper.rs | 21 +++++++++++- src/scene/project.rs | 1 + src/ui/window/layers.rs | 4 +++ 10 files changed, 140 insertions(+), 27 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 4a87a47db..720f86cc2 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -172,7 +172,7 @@ fn plot_scene_content( paper_wires.retain(|wire| wire.plot_visible); model_wires.retain(|wire| wire.plot_visible); let paper_hatches = scene.paper_plot_hatches().as_ref().clone(); - let paper_wipeouts = scene.paper_canvas_wipeouts().as_ref().clone(); + let paper_wipeouts = scene.paper_plot_wipeouts().as_ref().clone(); if scene.current_layout == "Model" { let splits = crate::io::pdf_export::PlotGroupSplits { wires: paper_wires.len(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 9e1cce871..e5fb0be1c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2457,6 +2457,8 @@ impl OpenCADStudio { } } + self.tabs[i].layers.refresh_sort(); + self.tabs[i] .scene .invalidate_layer_dependencies(&targets); diff --git a/src/entities/insert.rs b/src/entities/insert.rs index f16888a44..aa2c2339d 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -196,6 +196,7 @@ pub(crate) fn append_insert_attribute_wires( ins_pat: [f32; 8], ins_lw_px: f32, ins_layer: render::InheritStyle, + ins_layer_plottable: bool, bg_color: [f32; 4], is_xref: bool, pslt_factor: f32, @@ -212,11 +213,8 @@ pub(crate) fn append_insert_attribute_wires( if attmode == 0 { return; } - // An annotative block scales as ONE uniform unit about its insertion point; - // its attributes are carried by that scale (AutoCAD even forbids annotative - // attributes inside annotative blocks, to avoid double-scaling). So pre-scale - // the annotative attribute about the insertion point and tessellate it at a - // neutral 1.0 — the text path must never scale it a second time. + // Attributes inherit the annotative block scale. Pre-scale them here so + // text tessellation stays at a neutral scale. let annotative = ins .common .extended_data @@ -260,6 +258,16 @@ pub(crate) fn append_insert_attribute_wires( } else { sub_color }; + let attr_plottable = if render::is_effective_layer_zero(&attr.common.layer) { + ins_layer_plottable + } else { + document + .layers + .get(&attr.common.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true) + }; + let attr_plottable = ins_layer_plottable && attr_plottable; let sub_aabb = crate::scene::entity_aabb(&attr_entity); let mut attr_wires = tessellate::tessellate( document, @@ -284,6 +292,7 @@ pub(crate) fn append_insert_attribute_wires( w.name = insert_handle.value().to_string(); w.aci = sub_aci; w.aabb = sub_aabb; + w.plot_visible &= attr_plottable; } wires.extend(attr_wires); } diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index d8f79c6cd..b3d5cab7c 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -74,6 +74,7 @@ pub struct LocalWire { pub world_width: f32, pub plinegen: bool, pub plot_visible: bool, + pub plot_l0: bool, pub hide_unselected: bool, /// Set at construction; used to discriminate fill-only GPU batches from /// stroke batches in [`StyleKey`]. Derived from @@ -108,6 +109,8 @@ pub struct NestedRef { pub xform: Transform, pub style: crate::scene::render_graph::InsertStyleSpec, pub instance_offsets: Vec<[f64; 3]>, + pub plot_visible: bool, + pub plot_l0: bool, /// XCLIP boundary for this nested insert, in the parent defn's local frame /// (`None` = unclipped). Baked at build time because the clip's spatial /// filter lives in `doc.objects`, which isn't reachable at expand time; on @@ -520,6 +523,14 @@ fn build_defn( if let Some(record) = baked.filter(|record| { !record.entity_handles.is_empty() }) { + let table_plot_l0 = crate::scene::view::render::is_effective_layer_zero( + &table.common.layer, + ); + let table_plot_visible = doc + .layers + .get(&table.common.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true); let mut insert = acadrust::entities::Insert::new( record.name.clone(), table.insertion_point, @@ -544,7 +555,7 @@ fn build_defn( |leaf, context| { let mut placed = leaf.clone(); placed.apply_transform(&context.transform); - for wire in tessellate_sub_local( + for mut wire in tessellate_sub_local( doc, &placed, anno_scale, @@ -553,6 +564,8 @@ fn build_defn( viewport, depth_map, ) { + wire.plot_visible &= table_plot_l0 || table_plot_visible; + wire.plot_l0 |= table_plot_l0; subs.push(LocalSub::Wire(wire)); } }, @@ -628,12 +641,22 @@ fn build_nested_ref( .map(|filter| { crate::scene::pick::xclip::world_clip_polygon_for_transform(filter, &xform) }); + let plot_l0 = crate::scene::view::render::is_effective_layer_zero( + &nested_ins.common.layer, + ); + let plot_visible = doc + .layers + .get(&nested_ins.common.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true); NestedRef { block_name: nested_ins.block_name.clone(), xform, style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins, viewport), instance_offsets: crate::scene::render_graph::array_offsets(nested_ins), + plot_visible, + plot_l0, clip_poly, local_rank: depth_map .get(&nested_ins.common.handle.value()) @@ -679,6 +702,11 @@ fn tessellate_sub_local( // INSERT's layer at expand time. Flag each ByLayer property so emit_wire // can override the cached (layer-0-resolved) value with the insert layer's. let on_l0 = crate::scene::view::render::is_effective_layer_zero(&sub.common().layer); + let layer_plottable = doc + .layers + .get(&sub.common().layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true); let color_l0 = !has_book_color && on_l0 && sub.common().color == AcadColor::ByLayer; let transparency_l0 = on_l0 && sub.common().transparency.alpha() == 0; @@ -808,7 +836,9 @@ fn tessellate_sub_local( line_weight_px: lw_px, world_width: wire.world_width, plinegen: wire.plinegen, - plot_visible: frame_mode.is_none_or(|mode| mode == 1), + plot_visible: frame_mode.is_none_or(|mode| mode == 1) + && (on_l0 || layer_plottable), + plot_l0: on_l0, hide_unselected: frame_mode == Some(0), is_fill_only, color_is_byblock: color_is_byblock && wire_on_base_color, @@ -921,6 +951,7 @@ pub fn expand_insert( // The INSERT's own layer style — layer-0 inheritance target for children. ins_layer: crate::scene::view::render::InheritStyle, ins_layer_aci: u8, + ins_layer_plottable: bool, selected: bool, pslt_factor: f32, // World-space XY view AABB (with world_offset already subtracted, so the @@ -972,6 +1003,7 @@ pub fn expand_insert( ins_pat, ins_lw_px, ins_layer, + ins_layer_plottable, selected, pslt_factor, is_xref, @@ -1051,6 +1083,8 @@ pub fn expand_insert( ins_lw_px, l0: ins_layer, l0_aci: ins_layer_aci, + l0_plottable: ins_layer_plottable, + plot_visible: ins_layer_plottable, selected, pslt_factor, view_aabb: None, @@ -1154,6 +1188,7 @@ fn expansion_prototype_key( ins_pat: [f32; 8], ins_lw_px: f32, ins_layer: crate::scene::view::render::InheritStyle, + ins_layer_plottable: bool, selected: bool, pslt_factor: f32, is_xref: bool, @@ -1181,6 +1216,7 @@ fn expansion_prototype_key( insert_style.push(ins_layer.pat_len.to_bits()); insert_style.extend(ins_layer.pat.map(f32::to_bits)); insert_style.push(ins_layer.lw_px.to_bits()); + insert_style.push(ins_layer_plottable as u32); insert_style.push(pslt_factor.to_bits()); insert_style.extend(bg_color.map(f32::to_bits)); insert_style.push(anno_scale.to_bits()); @@ -1283,6 +1319,8 @@ struct ExpandCtx<'a> { /// for child wires on layer "0" whose properties are ByLayer. l0: crate::scene::view::render::InheritStyle, l0_aci: u8, + l0_plottable: bool, + plot_visible: bool, selected: bool, pslt_factor: f32, // World-space XY view AABB (post world_offset). `None` = no culling. @@ -1317,6 +1355,8 @@ fn nested_prototype_key( style.push(ctx.l0.pat_len.to_bits()); style.extend(ctx.l0.pat.map(f32::to_bits)); style.push(ctx.l0.lw_px.to_bits()); + style.push(ctx.l0_plottable as u32); + style.push(ctx.plot_visible as u32); style.push(ctx.pslt_factor.to_bits()); style.extend(ctx.bg_color.map(f32::to_bits)); NestedPrototypeKey { @@ -1724,6 +1764,11 @@ fn expand_defn( layer0_aci: ctx.l0_aci, }; let nested_style = nref.style.resolve(parent_style); + let nested_layer_plottable = if nref.plot_l0 { + ctx.l0_plottable + } else { + nref.plot_visible + }; let inner_ctx = ExpandCtx { cache: ctx.cache, ins_color: nested_style.insert.0, @@ -1733,6 +1778,8 @@ fn expand_defn( ins_lw_px: nested_style.insert.3, l0: nested_style.layer0, l0_aci: nested_style.layer0_aci, + l0_plottable: nested_layer_plottable, + plot_visible: ctx.plot_visible && nested_layer_plottable, selected: ctx.selected, pslt_factor: ctx.pslt_factor, view_aabb: ctx.view_aabb, @@ -2023,6 +2070,9 @@ fn emit_wire( // batches stay merged. let local_depth = (lw.world_width > 0.0) .then(|| d_range.0 + lw.local_rank * d_range.1); + let plot_visible = ctx.plot_visible + && lw.plot_visible + && (!lw.plot_l0 || ctx.l0_plottable); let key = style_key( final_color, @@ -2038,7 +2088,7 @@ fn emit_wire( lw.is_fill_only, lw.fill_is_2d_solid, lw.fill_is_3d, - lw.plot_visible, + plot_visible, lw.hide_unselected, local_depth, ); @@ -2067,7 +2117,7 @@ fn emit_wire( lw.is_fill_only, lw.fill_is_2d_solid, lw.fill_is_3d, - lw.plot_visible, + plot_visible, lw.hide_unselected, ) }); diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 0d4a02ac7..fe3d3ca94 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1096,6 +1096,11 @@ fn tessellate_entity_inner( _ => None, }) .unwrap_or(0); + let ins_layer_plottable = document + .layers + .get(&ins.common.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true); let ip = glam::Vec3::new( (ins.insert_point.x) as f32, (ins.insert_point.y) as f32, @@ -1167,6 +1172,7 @@ fn tessellate_entity_inner( ins_lw_px, ins_layer, ins_layer_aci, + ins_layer_plottable, sel, pslt_factor, view_aabb, @@ -1221,6 +1227,7 @@ fn tessellate_entity_inner( ins_pat, ins_lw_px, ins_layer, + ins_layer_plottable, bg_color, is_xref, pslt_factor, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 87c0ebc04..160ee2345 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1147,6 +1147,33 @@ impl Scene { false, ) } + + fn layer_plottable_in_context( + &self, + entity: &EntityType, + context: &crate::scene::render_graph::RenderContext, + ) -> bool { + let common = entity.common(); + let layer = if crate::scene::view::render::is_effective_layer_zero(&common.layer) { + context + .insert_path + .iter() + .rev() + .find(|insert| { + !crate::scene::view::render::is_effective_layer_zero(&insert.common.layer) + }) + .map(|insert| insert.common.layer.as_str()) + .unwrap_or(common.layer.as_str()) + } else { + common.layer.as_str() + }; + self.document + .layers + .get(layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true) + } + pub(super) fn instanced_plot_hatch_models( &self, layout_block: Handle, @@ -1239,13 +1266,7 @@ impl Scene { |entity, context| { let common = entity.common(); - if plot_only - && self - .document - .layers - .get(&common.layer) - .is_some_and(|layer| !layer.is_plottable) - { + if plot_only && !self.layer_plottable_in_context(entity, context) { return false; } @@ -1289,13 +1310,7 @@ impl Scene { let EntityType::Hatch(source_hatch) = entity else { return; }; - if plot_only - && self - .document - .layers - .get(&source_hatch.common.layer) - .is_some_and(|layer| !layer.is_plottable) - { + if plot_only && !self.layer_plottable_in_context(entity, context) { return; } let style = context.style_for(&self.document, entity); @@ -1396,6 +1411,7 @@ impl Scene { all_visible, bg_color, false, + false, ) } @@ -1407,6 +1423,7 @@ impl Scene { all_visible: bool, bg_color: [f32; 4], tint_insert_selection: bool, + plot_only: bool, ) -> Vec { let depth_map = self.draw_depth_map(); let graph = crate::scene::render_graph::RenderSceneGraph::new( @@ -1421,8 +1438,11 @@ impl Scene { graph.walk_root( self.render_scene_root(target_block), |entity, context| { - context.is_instanced() - || !self.entity_temporarily_hidden(entity.common().handle) + let common = entity.common(); + if plot_only && !self.layer_plottable_in_context(entity, context) { + return false; + } + context.is_instanced() || !self.entity_temporarily_hidden(common.handle) }, |entity, context| { let EntityType::Wipeout(source) = entity else { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 045480035..feb0bec3f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8676,6 +8676,7 @@ impl Scene { } EntityType::Insert(insert) => { for attribute in &insert.attributes { + add(&mut index.layers, &attribute.common.layer); add(&mut index.text_styles, &attribute.text_style); } } diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 46285baf6..a3c11612f 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -490,6 +490,7 @@ impl Scene { None, self.paper_annotation_scale_handle(), self.annotation_all_visible(), + false, )) } @@ -502,6 +503,7 @@ impl Scene { frozen: Option<&rustc_hash::FxHashSet>, annotation_scale_handle: Option, all_visible: bool, + include_solids: bool, ) -> Vec { let layer_hidden = |layer: &str| { self.document @@ -528,6 +530,9 @@ impl Scene { annotation_scale_handle, ); let entity = contextual.as_ref(); + if !include_solids && matches!(entity, EntityType::Solid(_)) { + continue; + } let common = entity.common(); if common.invisible || self.entity_temporarily_hidden(handle) @@ -600,6 +605,7 @@ impl Scene { all_visible, self.paper_bg_color, highlight_selection, + true, ) } @@ -609,13 +615,26 @@ impl Scene { /// content viewport's GPU pipeline) don't get a second mis-projected /// copy on the paper sheet. pub fn paper_canvas_wipeouts(&self) -> Arc> { + let layout_block = self.current_layout_block_handle(); + Arc::new(self.wipeout_models_for_block_graph( + layout_block, + None, + self.paper_annotation_scale_handle(), + self.annotation_all_visible(), + self.paper_bg_color, + true, + false, + )) + } + + pub fn paper_plot_wipeouts(&self) -> Arc> { let layout_block = self.current_layout_block_handle(); Arc::new(self.plot_wipeouts_for_block( layout_block, None, self.paper_annotation_scale_handle(), self.annotation_all_visible(), - true, + false, )) } diff --git a/src/scene/project.rs b/src/scene/project.rs index c975ff2cb..e1b4ff775 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -436,6 +436,7 @@ impl Scene { Some(&frozen), self.viewport_scale_handle(viewport.common.handle), self.annotation_all_visible(), + true, ); for hatch in hatches { if matches!(&hatch.pattern, HatchPattern::Pattern(_)) { diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index 6798b73ea..cffcfa4ab 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -253,6 +253,10 @@ impl LayerPanel { self.apply_sort(); } + pub fn refresh_sort(&mut self) { + self.apply_sort(); + } + /// Reorder `self.layers` by the active sort column, preserving the current /// selection by name. No-op in document order (`sort_col == None`). fn apply_sort(&mut self) {