fix(plot): respect non-printing layers

Filter nested block, attribute, table, hatch, and wipeout geometry during export while preserving canvas visibility.
This commit is contained in:
Hakan Seven 2026-08-21 18:57:23 +03:00
commit 77e1f1b876
10 changed files with 140 additions and 27 deletions

View file

@ -172,7 +172,7 @@ fn plot_scene_content(
paper_wires.retain(|wire| wire.plot_visible); paper_wires.retain(|wire| wire.plot_visible);
model_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_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" { if scene.current_layout == "Model" {
let splits = crate::io::pdf_export::PlotGroupSplits { let splits = crate::io::pdf_export::PlotGroupSplits {
wires: paper_wires.len(), wires: paper_wires.len(),

View file

@ -2457,6 +2457,8 @@ impl OpenCADStudio {
} }
} }
self.tabs[i].layers.refresh_sort();
self.tabs[i] self.tabs[i]
.scene .scene
.invalidate_layer_dependencies(&targets); .invalidate_layer_dependencies(&targets);

View file

@ -196,6 +196,7 @@ pub(crate) fn append_insert_attribute_wires(
ins_pat: [f32; 8], ins_pat: [f32; 8],
ins_lw_px: f32, ins_lw_px: f32,
ins_layer: render::InheritStyle, ins_layer: render::InheritStyle,
ins_layer_plottable: bool,
bg_color: [f32; 4], bg_color: [f32; 4],
is_xref: bool, is_xref: bool,
pslt_factor: f32, pslt_factor: f32,
@ -212,11 +213,8 @@ pub(crate) fn append_insert_attribute_wires(
if attmode == 0 { if attmode == 0 {
return; return;
} }
// An annotative block scales as ONE uniform unit about its insertion point; // Attributes inherit the annotative block scale. Pre-scale them here so
// its attributes are carried by that scale (AutoCAD even forbids annotative // text tessellation stays at a neutral scale.
// 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.
let annotative = ins let annotative = ins
.common .common
.extended_data .extended_data
@ -260,6 +258,16 @@ pub(crate) fn append_insert_attribute_wires(
} else { } else {
sub_color 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 sub_aabb = crate::scene::entity_aabb(&attr_entity);
let mut attr_wires = tessellate::tessellate( let mut attr_wires = tessellate::tessellate(
document, document,
@ -284,6 +292,7 @@ pub(crate) fn append_insert_attribute_wires(
w.name = insert_handle.value().to_string(); w.name = insert_handle.value().to_string();
w.aci = sub_aci; w.aci = sub_aci;
w.aabb = sub_aabb; w.aabb = sub_aabb;
w.plot_visible &= attr_plottable;
} }
wires.extend(attr_wires); wires.extend(attr_wires);
} }

View file

@ -74,6 +74,7 @@ pub struct LocalWire {
pub world_width: f32, pub world_width: f32,
pub plinegen: bool, pub plinegen: bool,
pub plot_visible: bool, pub plot_visible: bool,
pub plot_l0: bool,
pub hide_unselected: bool, pub hide_unselected: bool,
/// Set at construction; used to discriminate fill-only GPU batches from /// Set at construction; used to discriminate fill-only GPU batches from
/// stroke batches in [`StyleKey`]. Derived from /// stroke batches in [`StyleKey`]. Derived from
@ -108,6 +109,8 @@ pub struct NestedRef {
pub xform: Transform, pub xform: Transform,
pub style: crate::scene::render_graph::InsertStyleSpec, pub style: crate::scene::render_graph::InsertStyleSpec,
pub instance_offsets: Vec<[f64; 3]>, 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 /// XCLIP boundary for this nested insert, in the parent defn's local frame
/// (`None` = unclipped). Baked at build time because the clip's spatial /// (`None` = unclipped). Baked at build time because the clip's spatial
/// filter lives in `doc.objects`, which isn't reachable at expand time; on /// 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| { if let Some(record) = baked.filter(|record| {
!record.entity_handles.is_empty() !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( let mut insert = acadrust::entities::Insert::new(
record.name.clone(), record.name.clone(),
table.insertion_point, table.insertion_point,
@ -544,7 +555,7 @@ fn build_defn(
|leaf, context| { |leaf, context| {
let mut placed = leaf.clone(); let mut placed = leaf.clone();
placed.apply_transform(&context.transform); placed.apply_transform(&context.transform);
for wire in tessellate_sub_local( for mut wire in tessellate_sub_local(
doc, doc,
&placed, &placed,
anno_scale, anno_scale,
@ -553,6 +564,8 @@ fn build_defn(
viewport, viewport,
depth_map, depth_map,
) { ) {
wire.plot_visible &= table_plot_l0 || table_plot_visible;
wire.plot_l0 |= table_plot_l0;
subs.push(LocalSub::Wire(wire)); subs.push(LocalSub::Wire(wire));
} }
}, },
@ -628,12 +641,22 @@ fn build_nested_ref(
.map(|filter| { .map(|filter| {
crate::scene::pick::xclip::world_clip_polygon_for_transform(filter, &xform) 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 { NestedRef {
block_name: nested_ins.block_name.clone(), block_name: nested_ins.block_name.clone(),
xform, xform,
style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins, viewport), style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins, viewport),
instance_offsets: crate::scene::render_graph::array_offsets(nested_ins), instance_offsets: crate::scene::render_graph::array_offsets(nested_ins),
plot_visible,
plot_l0,
clip_poly, clip_poly,
local_rank: depth_map local_rank: depth_map
.get(&nested_ins.common.handle.value()) .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 // 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. // 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 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 = let color_l0 =
!has_book_color && on_l0 && sub.common().color == AcadColor::ByLayer; !has_book_color && on_l0 && sub.common().color == AcadColor::ByLayer;
let transparency_l0 = on_l0 && sub.common().transparency.alpha() == 0; let transparency_l0 = on_l0 && sub.common().transparency.alpha() == 0;
@ -808,7 +836,9 @@ fn tessellate_sub_local(
line_weight_px: lw_px, line_weight_px: lw_px,
world_width: wire.world_width, world_width: wire.world_width,
plinegen: wire.plinegen, 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), hide_unselected: frame_mode == Some(0),
is_fill_only, is_fill_only,
color_is_byblock: color_is_byblock && wire_on_base_color, 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. // The INSERT's own layer style — layer-0 inheritance target for children.
ins_layer: crate::scene::view::render::InheritStyle, ins_layer: crate::scene::view::render::InheritStyle,
ins_layer_aci: u8, ins_layer_aci: u8,
ins_layer_plottable: bool,
selected: bool, selected: bool,
pslt_factor: f32, pslt_factor: f32,
// World-space XY view AABB (with world_offset already subtracted, so the // World-space XY view AABB (with world_offset already subtracted, so the
@ -972,6 +1003,7 @@ pub fn expand_insert(
ins_pat, ins_pat,
ins_lw_px, ins_lw_px,
ins_layer, ins_layer,
ins_layer_plottable,
selected, selected,
pslt_factor, pslt_factor,
is_xref, is_xref,
@ -1051,6 +1083,8 @@ pub fn expand_insert(
ins_lw_px, ins_lw_px,
l0: ins_layer, l0: ins_layer,
l0_aci: ins_layer_aci, l0_aci: ins_layer_aci,
l0_plottable: ins_layer_plottable,
plot_visible: ins_layer_plottable,
selected, selected,
pslt_factor, pslt_factor,
view_aabb: None, view_aabb: None,
@ -1154,6 +1188,7 @@ fn expansion_prototype_key(
ins_pat: [f32; 8], ins_pat: [f32; 8],
ins_lw_px: f32, ins_lw_px: f32,
ins_layer: crate::scene::view::render::InheritStyle, ins_layer: crate::scene::view::render::InheritStyle,
ins_layer_plottable: bool,
selected: bool, selected: bool,
pslt_factor: f32, pslt_factor: f32,
is_xref: bool, is_xref: bool,
@ -1181,6 +1216,7 @@ fn expansion_prototype_key(
insert_style.push(ins_layer.pat_len.to_bits()); insert_style.push(ins_layer.pat_len.to_bits());
insert_style.extend(ins_layer.pat.map(f32::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.lw_px.to_bits());
insert_style.push(ins_layer_plottable as u32);
insert_style.push(pslt_factor.to_bits()); insert_style.push(pslt_factor.to_bits());
insert_style.extend(bg_color.map(f32::to_bits)); insert_style.extend(bg_color.map(f32::to_bits));
insert_style.push(anno_scale.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. /// for child wires on layer "0" whose properties are ByLayer.
l0: crate::scene::view::render::InheritStyle, l0: crate::scene::view::render::InheritStyle,
l0_aci: u8, l0_aci: u8,
l0_plottable: bool,
plot_visible: bool,
selected: bool, selected: bool,
pslt_factor: f32, pslt_factor: f32,
// World-space XY view AABB (post world_offset). `None` = no culling. // 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.push(ctx.l0.pat_len.to_bits());
style.extend(ctx.l0.pat.map(f32::to_bits)); style.extend(ctx.l0.pat.map(f32::to_bits));
style.push(ctx.l0.lw_px.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.push(ctx.pslt_factor.to_bits());
style.extend(ctx.bg_color.map(f32::to_bits)); style.extend(ctx.bg_color.map(f32::to_bits));
NestedPrototypeKey { NestedPrototypeKey {
@ -1724,6 +1764,11 @@ fn expand_defn(
layer0_aci: ctx.l0_aci, layer0_aci: ctx.l0_aci,
}; };
let nested_style = nref.style.resolve(parent_style); 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 { let inner_ctx = ExpandCtx {
cache: ctx.cache, cache: ctx.cache,
ins_color: nested_style.insert.0, ins_color: nested_style.insert.0,
@ -1733,6 +1778,8 @@ fn expand_defn(
ins_lw_px: nested_style.insert.3, ins_lw_px: nested_style.insert.3,
l0: nested_style.layer0, l0: nested_style.layer0,
l0_aci: nested_style.layer0_aci, l0_aci: nested_style.layer0_aci,
l0_plottable: nested_layer_plottable,
plot_visible: ctx.plot_visible && nested_layer_plottable,
selected: ctx.selected, selected: ctx.selected,
pslt_factor: ctx.pslt_factor, pslt_factor: ctx.pslt_factor,
view_aabb: ctx.view_aabb, view_aabb: ctx.view_aabb,
@ -2023,6 +2070,9 @@ fn emit_wire(
// batches stay merged. // batches stay merged.
let local_depth = (lw.world_width > 0.0) let local_depth = (lw.world_width > 0.0)
.then(|| d_range.0 + lw.local_rank * d_range.1); .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( let key = style_key(
final_color, final_color,
@ -2038,7 +2088,7 @@ fn emit_wire(
lw.is_fill_only, lw.is_fill_only,
lw.fill_is_2d_solid, lw.fill_is_2d_solid,
lw.fill_is_3d, lw.fill_is_3d,
lw.plot_visible, plot_visible,
lw.hide_unselected, lw.hide_unselected,
local_depth, local_depth,
); );
@ -2067,7 +2117,7 @@ fn emit_wire(
lw.is_fill_only, lw.is_fill_only,
lw.fill_is_2d_solid, lw.fill_is_2d_solid,
lw.fill_is_3d, lw.fill_is_3d,
lw.plot_visible, plot_visible,
lw.hide_unselected, lw.hide_unselected,
) )
}); });

View file

@ -1096,6 +1096,11 @@ fn tessellate_entity_inner(
_ => None, _ => None,
}) })
.unwrap_or(0); .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( let ip = glam::Vec3::new(
(ins.insert_point.x) as f32, (ins.insert_point.x) as f32,
(ins.insert_point.y) as f32, (ins.insert_point.y) as f32,
@ -1167,6 +1172,7 @@ fn tessellate_entity_inner(
ins_lw_px, ins_lw_px,
ins_layer, ins_layer,
ins_layer_aci, ins_layer_aci,
ins_layer_plottable,
sel, sel,
pslt_factor, pslt_factor,
view_aabb, view_aabb,
@ -1221,6 +1227,7 @@ fn tessellate_entity_inner(
ins_pat, ins_pat,
ins_lw_px, ins_lw_px,
ins_layer, ins_layer,
ins_layer_plottable,
bg_color, bg_color,
is_xref, is_xref,
pslt_factor, pslt_factor,

View file

@ -1147,6 +1147,33 @@ impl Scene {
false, 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( pub(super) fn instanced_plot_hatch_models(
&self, &self,
layout_block: Handle, layout_block: Handle,
@ -1239,13 +1266,7 @@ impl Scene {
|entity, context| { |entity, context| {
let common = entity.common(); let common = entity.common();
if plot_only if plot_only && !self.layer_plottable_in_context(entity, context) {
&& self
.document
.layers
.get(&common.layer)
.is_some_and(|layer| !layer.is_plottable)
{
return false; return false;
} }
@ -1289,13 +1310,7 @@ impl Scene {
let EntityType::Hatch(source_hatch) = entity else { let EntityType::Hatch(source_hatch) = entity else {
return; return;
}; };
if plot_only if plot_only && !self.layer_plottable_in_context(entity, context) {
&& self
.document
.layers
.get(&source_hatch.common.layer)
.is_some_and(|layer| !layer.is_plottable)
{
return; return;
} }
let style = context.style_for(&self.document, entity); let style = context.style_for(&self.document, entity);
@ -1396,6 +1411,7 @@ impl Scene {
all_visible, all_visible,
bg_color, bg_color,
false, false,
false,
) )
} }
@ -1407,6 +1423,7 @@ impl Scene {
all_visible: bool, all_visible: bool,
bg_color: [f32; 4], bg_color: [f32; 4],
tint_insert_selection: bool, tint_insert_selection: bool,
plot_only: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map(); let depth_map = self.draw_depth_map();
let graph = crate::scene::render_graph::RenderSceneGraph::new( let graph = crate::scene::render_graph::RenderSceneGraph::new(
@ -1421,8 +1438,11 @@ impl Scene {
graph.walk_root( graph.walk_root(
self.render_scene_root(target_block), self.render_scene_root(target_block),
|entity, context| { |entity, context| {
context.is_instanced() let common = entity.common();
|| !self.entity_temporarily_hidden(entity.common().handle) if plot_only && !self.layer_plottable_in_context(entity, context) {
return false;
}
context.is_instanced() || !self.entity_temporarily_hidden(common.handle)
}, },
|entity, context| { |entity, context| {
let EntityType::Wipeout(source) = entity else { let EntityType::Wipeout(source) = entity else {

View file

@ -8676,6 +8676,7 @@ impl Scene {
} }
EntityType::Insert(insert) => { EntityType::Insert(insert) => {
for attribute in &insert.attributes { for attribute in &insert.attributes {
add(&mut index.layers, &attribute.common.layer);
add(&mut index.text_styles, &attribute.text_style); add(&mut index.text_styles, &attribute.text_style);
} }
} }

View file

@ -490,6 +490,7 @@ impl Scene {
None, None,
self.paper_annotation_scale_handle(), self.paper_annotation_scale_handle(),
self.annotation_all_visible(), self.annotation_all_visible(),
false,
)) ))
} }
@ -502,6 +503,7 @@ impl Scene {
frozen: Option<&rustc_hash::FxHashSet<Handle>>, frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>, annotation_scale_handle: Option<Handle>,
all_visible: bool, all_visible: bool,
include_solids: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
let layer_hidden = |layer: &str| { let layer_hidden = |layer: &str| {
self.document self.document
@ -528,6 +530,9 @@ impl Scene {
annotation_scale_handle, annotation_scale_handle,
); );
let entity = contextual.as_ref(); let entity = contextual.as_ref();
if !include_solids && matches!(entity, EntityType::Solid(_)) {
continue;
}
let common = entity.common(); let common = entity.common();
if common.invisible if common.invisible
|| self.entity_temporarily_hidden(handle) || self.entity_temporarily_hidden(handle)
@ -600,6 +605,7 @@ impl Scene {
all_visible, all_visible,
self.paper_bg_color, self.paper_bg_color,
highlight_selection, highlight_selection,
true,
) )
} }
@ -609,13 +615,26 @@ impl Scene {
/// content viewport's GPU pipeline) don't get a second mis-projected /// content viewport's GPU pipeline) don't get a second mis-projected
/// copy on the paper sheet. /// copy on the paper sheet.
pub fn paper_canvas_wipeouts(&self) -> Arc<Vec<HatchModel>> { pub fn paper_canvas_wipeouts(&self) -> Arc<Vec<HatchModel>> {
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<Vec<HatchModel>> {
let layout_block = self.current_layout_block_handle(); let layout_block = self.current_layout_block_handle();
Arc::new(self.plot_wipeouts_for_block( Arc::new(self.plot_wipeouts_for_block(
layout_block, layout_block,
None, None,
self.paper_annotation_scale_handle(), self.paper_annotation_scale_handle(),
self.annotation_all_visible(), self.annotation_all_visible(),
true, false,
)) ))
} }

View file

@ -436,6 +436,7 @@ impl Scene {
Some(&frozen), Some(&frozen),
self.viewport_scale_handle(viewport.common.handle), self.viewport_scale_handle(viewport.common.handle),
self.annotation_all_visible(), self.annotation_all_visible(),
true,
); );
for hatch in hatches { for hatch in hatches {
if matches!(&hatch.pattern, HatchPattern::Pattern(_)) { if matches!(&hatch.pattern, HatchPattern::Pattern(_)) {

View file

@ -253,6 +253,10 @@ impl LayerPanel {
self.apply_sort(); self.apply_sort();
} }
pub fn refresh_sort(&mut self) {
self.apply_sort();
}
/// Reorder `self.layers` by the active sort column, preserving the current /// Reorder `self.layers` by the active sort column, preserving the current
/// selection by name. No-op in document order (`sort_col == None`). /// selection by name. No-op in document order (`sort_col == None`).
fn apply_sort(&mut self) { fn apply_sort(&mut self) {