From 970ee5a09f189cec2ae893a35ccca527e0101702 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 30 Jun 2026 17:24:04 +0300 Subject: [PATCH] fix(blocks): layer-0 children inherit the block instance layer (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entities inside a block definition that sit on layer "0" must take on the properties of the layer the block reference is inserted on; every other layer is "sticky" and keeps its own identity. OpenCADStudio resolved a block child's ByLayer color/linetype/lineweight against the child's own layer in all cases, so layer-0 content stayed layer-0 (white) instead of inheriting the instance layer. Resolution now follows the standard rule for block children — explicit wins, then ByBlock → the insert's own style, then layer-0 + ByLayer → the insert's *layer* style, else the child's own layer. The insert's layer style threads through the whole block-expansion chain (and chains correctly through nested inserts), with the child's own transparency preserved. Covered across every block-render path: - wires (line/arc/circle/polyline/text) via the block cache + the explode fallback + insert attributes - hatch / solid fills (also fixes the PDF/print export path) - 3D solid block children (per-instance mesh recolor) - Dimension / Table baked-block sub-entities New shared helpers in scene::view::render: InheritStyle, layer_render_style, lineweight_to_px; render_style_for_block_sub gained the layer-0 branch. Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and transparency cases. Co-Authored-By: Claude Opus 4.8 --- src/entities/insert.rs | 2 + src/scene/cache/block_cache.rs | 139 ++++++++++++++++++++------- src/scene/convert/tess.rs | 52 +++++++++- src/scene/entity.rs | 71 +++++++++++++- src/scene/mod.rs | 127 ++++++++++++++++++++++-- src/scene/view/render.rs | 170 ++++++++++++++++++++++++++++++--- 6 files changed, 497 insertions(+), 64 deletions(-) diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 0eb0c52ad..acf359609 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -133,6 +133,7 @@ pub(crate) fn append_insert_attribute_wires( ins_pat_len: f32, ins_pat: [f32; 8], ins_lw_px: f32, + ins_layer: render::InheritStyle, bg_color: [f32; 4], is_xref: bool, pslt_factor: f32, @@ -162,6 +163,7 @@ pub(crate) fn append_insert_attribute_wires( ins_pat_len, ins_pat, ins_lw_px, + ins_layer, ); let sub_color = render::adapt_to_bg(sub_color, bg_color); let sub_color = if is_xref && !sel { diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 9284946ef..f8751b219 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -59,6 +59,13 @@ pub struct LocalWire { pub color_is_byblock: bool, pub lt_is_byblock: bool, pub lw_is_byblock: bool, + /// Set when this child sits on layer "0" and the matching property is + /// ByLayer. At expand time the value is then taken from the INSERT's + /// *layer* (the layer-0 inheritance rule) instead of the cached layer-0 + /// value baked here. + pub color_l0: bool, + pub lt_l0: bool, + pub lw_l0: bool, /// XY bounding box of this wire in block-local coordinates. /// `[min_x, min_y, max_x, max_y]`. Used for view-frustum culling at /// expand-time: transform corners by the Insert transform → world AABB @@ -89,6 +96,18 @@ pub struct NestedRef { pub color_is_byblock: bool, pub lt_is_byblock: bool, pub lw_is_byblock: bool, + /// The nested INSERT's own properties are ByLayer. Combined with + /// `layer_is_zero` these drive the layer-0 rule for the nested insert + /// *itself* (so its ByBlock leaves inherit the outer layer, not layer 0). + pub color_is_bylayer: bool, + pub lt_is_bylayer: bool, + pub lw_is_bylayer: bool, + /// The nested INSERT itself sits on layer "0": its own layer-0 children + /// chain up to the outer insert's layer rather than resolving to `l0`. + pub layer_is_zero: bool, + /// The nested INSERT's own layer style — the layer-0 inheritance target + /// for its children when the nested insert is not itself on layer 0. + pub l0: crate::scene::view::render::InheritStyle, pub instance_offsets: Vec<[f64; 3]>, /// XCLIP boundary for this nested insert, in the parent defn's local frame /// (`None` = unclipped). Baked at build time because the clip's spatial @@ -322,6 +341,9 @@ fn build_nested_ref( // against different backgrounds without rebuilding. let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = crate::scene::view::render::render_style_for(doc, &EntityType::Insert(nested_ins.clone())); + // The nested insert's own layer style — the layer-0 target for its + // children (raw colour; adapted at emit like `ins_color`). + let l0 = crate::scene::view::render::layer_render_style(doc, &nested_ins.common.layer); let _ = bg_color; // Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps @@ -340,6 +362,17 @@ fn build_nested_ref( color_is_byblock: nested_ins.common.color == AcadColor::ByBlock, lt_is_byblock: nested_ins.common.linetype.eq_ignore_ascii_case("byblock"), lw_is_byblock: matches!(nested_ins.common.line_weight, LineWeight::ByBlock), + color_is_bylayer: nested_ins.common.color == AcadColor::ByLayer, + lt_is_bylayer: { + let lt = &nested_ins.common.linetype; + lt.is_empty() || lt.eq_ignore_ascii_case("bylayer") + }, + lw_is_bylayer: matches!( + nested_ins.common.line_weight, + LineWeight::ByLayer | LineWeight::Default + ), + layer_is_zero: nested_ins.common.layer == "0", + l0, instance_offsets: array_offsets(nested_ins), clip_poly, } @@ -372,6 +405,21 @@ fn tessellate_sub_local( let lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock"); let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::ByBlock); + // Layer-0 rule: a child on layer "0" with ByLayer properties inherits the + // 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 = sub.common().layer == "0"; + let color_l0 = on_l0 && sub.common().color == AcadColor::ByLayer; + let lt_l0 = on_l0 && { + let lt = &sub.common().linetype; + lt.is_empty() || lt.eq_ignore_ascii_case("bylayer") + }; + let lw_l0 = on_l0 + && matches!( + sub.common().line_weight, + LineWeight::ByLayer | LineWeight::Default + ); + // Pass `local_offset` as the f64 world-offset so tessellate subtracts it // before casting to f32 — same precision-preservation trick used for // top-level entities, applied per-defn. @@ -437,6 +485,9 @@ fn tessellate_sub_local( color_is_byblock, lt_is_byblock, lw_is_byblock, + color_l0, + lt_l0, + lw_l0, aabb_local, text_height_local, text_obb_local, @@ -537,6 +588,8 @@ pub fn expand_insert( ins_pat_len: f32, ins_pat: [f32; 8], ins_lw_px: f32, + // The INSERT's own layer style — layer-0 inheritance target for children. + ins_layer: crate::scene::view::render::InheritStyle, selected: bool, pslt_factor: f32, // World-space XY view AABB (with world_offset already subtracted, so the @@ -598,6 +651,7 @@ pub fn expand_insert( ins_pat_len, ins_pat, ins_lw_px, + l0: ins_layer, selected, pslt_factor, view_aabb, @@ -693,18 +747,7 @@ fn emit_greeked_text( return; } - let final_color = if ctx.selected { - WireModel::SELECTED - } else if lw.color_is_byblock { - ctx.ins_color - } else { - lw.color - }; - let final_color = if ctx.is_xref && !ctx.selected { - fade_toward_bg(final_color, ctx.bg_color) - } else { - final_color - }; + let final_color = resolve_wire_color(lw, ctx); let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, true); let entry = out @@ -790,18 +833,7 @@ fn emit_text_baseline( (ux / ulen, uy / ulen, uz / ulen) }; - let final_color = if ctx.selected { - WireModel::SELECTED - } else if lw.color_is_byblock { - ctx.ins_color - } else { - lw.color - }; - let final_color = if ctx.is_xref && !ctx.selected { - fade_toward_bg(final_color, ctx.bg_color) - } else { - final_color - }; + let final_color = resolve_wire_color(lw, ctx); let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, false); let entry = out @@ -868,6 +900,9 @@ struct ExpandCtx<'a> { ins_pat_len: f32, ins_pat: [f32; 8], ins_lw_px: f32, + /// Layer-0 inheritance target — the current INSERT's *layer* style, used + /// for child wires on layer "0" whose properties are ByLayer. + l0: crate::scene::view::render::InheritStyle, selected: bool, pslt_factor: f32, // World-space XY view AABB (post world_offset). `None` = no culling. @@ -1202,28 +1237,43 @@ fn expand_defn( continue; } } - // Resolve ByBlock for this nested ref against the outer ctx. + // Resolve the nested insert's own style against the outer ctx: + // ByBlock inherits the outer insert; a nested insert that is + // itself on layer "0" with ByLayer props inherits the outer + // layer-0 target (so its ByBlock leaves resolve to that layer, + // not layer 0). Mirrors the leaf resolution in emit_wire. let nested_color = if nref.color_is_byblock { ctx.ins_color + } else if nref.layer_is_zero && nref.color_is_bylayer { + ctx.l0.color } else { nref.ins_color }; let (nested_pat_len, nested_pat) = if nref.lt_is_byblock { (ctx.ins_pat_len, ctx.ins_pat) + } else if nref.layer_is_zero && nref.lt_is_bylayer { + (ctx.l0.pat_len, ctx.l0.pat) } else { (nref.ins_pat_len, nref.ins_pat) }; let nested_lw_px = if nref.lw_is_byblock { ctx.ins_lw_px + } else if nref.layer_is_zero && nref.lw_is_bylayer { + ctx.l0.lw_px } else { nref.ins_lw_px }; + // Layer-0 target for the nested expansion: a nested insert that + // is itself on layer 0 chains up to the outer target; otherwise + // its layer-0 children resolve to the nested insert's own layer. + let nested_l0 = if nref.layer_is_zero { ctx.l0 } else { nref.l0 }; let inner_ctx = ExpandCtx { cache: ctx.cache, ins_color: nested_color, ins_pat_len: nested_pat_len, ins_pat: nested_pat, ins_lw_px: nested_lw_px, + l0: nested_l0, selected: ctx.selected, pslt_factor: ctx.pslt_factor, view_aabb: ctx.view_aabb, @@ -1279,6 +1329,28 @@ fn expand_defn( } } +/// Resolve a cached LocalWire's final colour against the current expansion +/// context: selection override first, then ByBlock → insert colour, then the +/// layer-0 rule → insert-layer colour, else the cached colour; finally xref +/// fade. Shared by the stroke, fill, and greeked-text emit paths. +fn resolve_wire_color(lw: &LocalWire, ctx: &ExpandCtx) -> [f32; 4] { + let c = if ctx.selected { + WireModel::SELECTED + } else if lw.color_is_byblock { + ctx.ins_color + } else if lw.color_l0 { + // Inherit the insert layer's RGB but keep the child's own transparency. + [ctx.l0.color[0], ctx.l0.color[1], ctx.l0.color[2], lw.color[3]] + } else { + lw.color + }; + if ctx.is_xref && !ctx.selected { + fade_toward_bg(c, ctx.bg_color) + } else { + c + } +} + fn emit_wire( lw: &LocalWire, accum_xform: &Transform, @@ -1291,25 +1363,18 @@ fn emit_wire( // Resolve final style for this LocalWire against the outer Insert ctx // before we hash it into a batch. - let final_color = if ctx.selected { - WireModel::SELECTED - } else if lw.color_is_byblock { - ctx.ins_color - } else { - lw.color - }; - let final_color = if ctx.is_xref && !ctx.selected { - fade_toward_bg(final_color, ctx.bg_color) - } else { - final_color - }; + let final_color = resolve_wire_color(lw, ctx); let (final_pat_len, final_pat) = if lw.lt_is_byblock { (ctx.ins_pat_len, ctx.ins_pat) + } else if lw.lt_l0 { + (ctx.l0.pat_len, ctx.l0.pat) } else { (lw.pattern_length, lw.pattern) }; let final_lw_px = if lw.lw_is_byblock { ctx.ins_lw_px + } else if lw.lw_l0 { + ctx.l0.lw_px } else { lw.line_weight_px }; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 4a4a49aba..fdefc45e4 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -220,6 +220,20 @@ pub(crate) fn tessellate_entity( if !br.entity_handles.is_empty() { let mut wires: Vec = Vec::with_capacity(br.entity_handles.len()); + // The Dimension's own layer style — layer-0 inheritance + // target for baked sub-entities on layer "0" (#221). + let dim_l0_color = view::render::adapt_to_bg( + view::render::layer_render_style(document, &e.common().layer).color, + bg_color, + ); + let dim_l0_aci = document + .layers + .get(&e.common().layer) + .map(|l| match &l.color { + acadrust::types::Color::Index(i) => *i, + _ => 0, + }) + .unwrap_or(0); for &eh in &br.entity_handles { let Some(sub) = document.get_entity(eh) else { continue }; // Sub-entities inside *D### / DIMBLOCK## blocks @@ -227,6 +241,8 @@ pub(crate) fn tessellate_entity( // they should inherit from the Dimension entity. let sub_color_is_byblock = sub.common().color == acadrust::types::Color::ByBlock; + let sub_is_l0_bylayer = sub.common().layer == "0" + && sub.common().color == acadrust::types::Color::ByLayer; let sub_wires = tessellate_entity( document, selected, active_viewport, bg_color, // Block contents are baked at the final WCS size — @@ -238,10 +254,14 @@ pub(crate) fn tessellate_entity( // Override ByBlock colour with the dim's resolved // colour so text matches `DIMCLRT`-style behaviour // (or layer colour) instead of the raw ByBlock - // fallback that render_style_for produces. + // fallback that render_style_for produces. A layer-0 + // sub inherits the dim's layer colour instead. if sub_color_is_byblock { w.color = if sel { WireModel::SELECTED } else { entity_color }; w.aci = aci; + } else if sub_is_l0_bylayer && !sel { + w.color = dim_l0_color; + w.aci = dim_l0_aci; } wires.push(w); } @@ -320,10 +340,26 @@ pub(crate) fn tessellate_entity( if !br.entity_handles.is_empty() { let mut wires: Vec = Vec::with_capacity(br.entity_handles.len()); + // The Table's own layer style — layer-0 inheritance target + // for baked sub-entities on layer "0" (#221). + let tab_l0_color = view::render::adapt_to_bg( + view::render::layer_render_style(document, &e.common().layer).color, + bg_color, + ); + let tab_l0_aci = document + .layers + .get(&e.common().layer) + .map(|l| match &l.color { + acadrust::types::Color::Index(i) => *i, + _ => 0, + }) + .unwrap_or(0); for &eh in &br.entity_handles { let Some(sub) = document.get_entity(eh) else { continue }; let sub_color_is_byblock = sub.common().color == acadrust::types::Color::ByBlock; + let sub_is_l0_bylayer = sub.common().layer == "0" + && sub.common().color == acadrust::types::Color::ByLayer; let sub_wires = tessellate_entity( document, selected, active_viewport, bg_color, anno_scale, sub, block_cache, view_aabb, world_per_pixel, @@ -333,6 +369,9 @@ pub(crate) fn tessellate_entity( if sub_color_is_byblock { w.color = if sel { WireModel::SELECTED } else { entity_color }; w.aci = aci; + } else if sub_is_l0_bylayer && !sel { + w.color = tab_l0_color; + w.aci = tab_l0_aci; } wires.push(w); } @@ -367,6 +406,13 @@ pub(crate) fn tessellate_entity( // Resolve the INSERT's own style so ByBlock sub-entities can inherit it. let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = view::render::render_style_for(document, e); let ins_color = view::render::adapt_to_bg(ins_color, bg_color); + // Resolve the INSERT's *layer* style — the layer-0 inheritance target + // for sub-entities on layer "0" with ByLayer properties (#221). + let ins_layer = { + let mut s = view::render::layer_render_style(document, &ins.common.layer); + s.color = view::render::adapt_to_bg(s.color, bg_color); + s + }; let ip = glam::Vec3::new( (ins.insert_point.x) as f32, (ins.insert_point.y) as f32, @@ -408,6 +454,7 @@ pub(crate) fn tessellate_entity( ins_pat_len, ins_pat, ins_lw_px, + ins_layer, sel, pslt_factor, view_aabb, @@ -438,6 +485,7 @@ pub(crate) fn tessellate_entity( ins_pat_len, ins_pat, ins_lw_px, + ins_layer, bg_color, is_xref, pslt_factor, @@ -469,6 +517,7 @@ pub(crate) fn tessellate_entity( ins_pat_len, ins_pat, ins_lw_px, + ins_layer, ); let sub_color = view::render::adapt_to_bg(sub_color, bg_color); let sub_color = if is_xref && !sel { @@ -509,6 +558,7 @@ pub(crate) fn tessellate_entity( ins_pat_len, ins_pat, ins_lw_px, + ins_layer, bg_color, is_xref, pslt_factor, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 534031e4f..e7fa1282a 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -377,6 +377,12 @@ impl Scene { }) .collect(); + // Background for adapting block-child hatch colours at the leaf (#221). + let hatch_bg: [f32; 4] = if self.current_layout != "Model" { + self.paper_bg_color + } else { + self.bg_color + }; for entity in self.document.entities() { let EntityType::Insert(ins) = entity else { continue; @@ -402,26 +408,81 @@ impl Scene { // hatches land in the correct world position. A depth guard keeps // a malformed cyclic block reference from looping forever. let normalize = crate::modules::draw::modify::explode::normalize_insert_entity; - let mut stack: Vec<(EntityType, usize)> = ins + // Block-child colour inheritance sources (#221), kept RAW and + // adapted at the leaf. `ins_color` feeds ByBlock; `l0` (the + // INSERT's *layer* style) feeds the layer-0 rule. Both chain + // through nested inserts, mirroring expand_insert. + let ins_color = + crate::scene::view::render::render_style_for(&self.document, entity).0; + let l0 = crate::scene::view::render::layer_render_style( + &self.document, + &ins.common.layer, + ); + let mut stack: Vec<( + EntityType, + usize, + [f32; 4], + crate::scene::view::render::InheritStyle, + )> = ins .explode_from_document(&self.document) .into_iter() - .map(|e| (normalize(e), 0usize)) + .map(|e| (normalize(e), 0usize, ins_color, l0)) .collect(); - while let Some((sub, depth)) = stack.pop() { + while let Some((sub, depth, sub_ins_color, sub_l0)) = stack.pop() { match sub { EntityType::Insert(nins) => { if depth >= 32 { continue; } + use acadrust::types::Color; + // Chain the inheritance sources into the nested insert, + // mirroring expand_insert's nested resolution: ByBlock → + // parent source; layer-0 + ByLayer → parent layer-0 + // target; else the nested insert's own resolved style. + let child_ins_color = if nins.common.color == Color::ByBlock { + sub_ins_color + } else if nins.common.layer == "0" + && nins.common.color == Color::ByLayer + { + sub_l0.color + } else { + crate::scene::view::render::render_style_for( + &self.document, + &EntityType::Insert(nins.clone()), + ) + .0 + }; + let child_l0 = if nins.common.layer == "0" { + sub_l0 + } else { + crate::scene::view::render::layer_render_style( + &self.document, + &nins.common.layer, + ) + }; for e in nins.explode_from_document(&self.document) { - stack.push((normalize(e), depth + 1)); + stack.push((normalize(e), depth + 1, child_ins_color, child_l0)); } } EntityType::Hatch(dxf) => { if dxf.common.invisible || layer_hidden(&dxf.common.layer) { continue; } - let color = self.render_style(&EntityType::Hatch(dxf.clone())).0; + // Resolve ByBlock / layer-0 inheritance for this block + // child, then adapt to the background (#221). Pattern / + // lineweight args are unused by a hatch's colour. + let color = crate::scene::view::render::render_style_for_block_sub( + &self.document, + &EntityType::Hatch(dxf.clone()), + sub_ins_color, + 0.0, + [0.0; 8], + 0.0, + sub_l0, + ) + .0; + let color = + crate::scene::view::render::adapt_to_bg(color, hatch_bg); if let Some(mut model) = Self::hatch_model_from_dxf(&dxf, color) { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0632fb9d7..6d4519abe 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2321,8 +2321,28 @@ impl Scene { if !self.mesh_entity_visible(ins.common.handle) { continue; } + // Colour inheritance sources for block-internal solids (#221). + let bg = self.current_bg(); + let ins_color = crate::scene::view::render::adapt_to_bg( + crate::scene::view::render::render_style_for(&self.document, e).0, + bg, + ); + let l0 = crate::scene::view::render::adapt_to_bg( + crate::scene::view::render::layer_render_style( + &self.document, + &ins.common.layer, + ) + .color, + bg, + ); let start = out.len(); - self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, &mut out); + self.expand_block_meshes( + &ins.block_name, + &ins.get_transform(), + 0, + Some((ins_color, l0)), + &mut out, + ); // Tag the instanced meshes with the parent INSERT handle so the // hover / selection highlight (keyed on the mesh name) tints the // block, not the inner solid's own handle which nothing selects. @@ -2344,6 +2364,11 @@ impl Scene { block_name: &str, accum: &acadrust::types::Transform, depth: usize, + // Block-child colour inheritance sources, bg-adapted: + // `(insert_color, insert_layer_color)`. `Some` only on the render path; + // pick paths pass `None` (colour irrelevant). Drives the ByBlock / + // layer-0 overrides for block-internal solids (#221). + inherit: Option<([f32; 4], [f32; 4])>, out: &mut Vec, ) { if depth > 16 { @@ -2364,13 +2389,103 @@ impl Scene { } if let EntityType::Insert(ins) = e { let composed = ins.get_transform().then(accum); - self.expand_block_meshes(&ins.block_name, &composed, depth + 1, out); + let child = inherit.map(|(pc, pl0)| self.chain_mesh_inherit(ins, pc, pl0)); + self.expand_block_meshes(&ins.block_name, &composed, depth + 1, child, out); } else if let Some(set) = self.block_meshes.get(&h) { - out.push(transform_block_mesh_lod_set(set, accum)); + // The solid's own transparency (baked into the cached colour). + let own_alpha = set.lods.first().map(|m| m.color[3]).unwrap_or(1.0); + let mut ts = transform_block_mesh_lod_set(set, accum); + // Re-resolve colour against the INSERT context: a block-internal + // solid that is ByBlock or on layer "0" + ByLayer can't be + // coloured at cache-build time (no insert context there). (#221) + if let Some(c) = self.block_mesh_override_color(e, h, inherit, own_alpha) { + for lod in &mut ts.lods { + lod.color = c; + } + } + out.push(ts); } } } + /// Background colour for the current layout (model vs paper). + fn current_bg(&self) -> [f32; 4] { + if self.current_layout != "Model" { + self.paper_bg_color + } else { + self.bg_color + } + } + + /// Chain block-child colour inheritance into a nested INSERT, mirroring + /// `expand_insert`: ByBlock keeps the parent source; a nested insert on + /// layer "0" with ByLayer colour adopts the parent layer-0 target; else it + /// uses its own resolved colour / layer. Returned colours are bg-adapted. + fn chain_mesh_inherit( + &self, + ins: &acadrust::entities::Insert, + parent_ins_color: [f32; 4], + parent_l0: [f32; 4], + ) -> ([f32; 4], [f32; 4]) { + use acadrust::types::Color; + let bg = self.current_bg(); + let child_ins_color = if ins.common.color == Color::ByBlock { + parent_ins_color + } else if ins.common.layer == "0" && ins.common.color == Color::ByLayer { + parent_l0 + } else { + crate::scene::view::render::adapt_to_bg( + crate::scene::view::render::render_style_for( + &self.document, + &EntityType::Insert(ins.clone()), + ) + .0, + bg, + ) + }; + let child_l0 = if ins.common.layer == "0" { + parent_l0 + } else { + crate::scene::view::render::adapt_to_bg( + crate::scene::view::render::layer_render_style(&self.document, &ins.common.layer) + .color, + bg, + ) + }; + (child_ins_color, child_l0) + } + + /// Per-instance colour override for a block-internal solid mesh: ByBlock → + /// insert colour, layer-0 + ByLayer → insert-layer colour, else `None` + /// (keep the cached own-layer colour). Applies the same REFEDIT fade as + /// `recolor_meshes`, keyed on the inner solid handle. (#221) + fn block_mesh_override_color( + &self, + e: &EntityType, + h: Handle, + inherit: Option<([f32; 4], [f32; 4])>, + own_alpha: f32, + ) -> Option<[f32; 4]> { + let (ins_color, l0_color) = inherit?; + use acadrust::types::Color; + let common = e.common(); + let mut c = if common.color == Color::ByBlock { + ins_color + } else if common.layer == "0" && common.color == Color::ByLayer { + // Inherit the insert layer's RGB but keep the solid's own alpha, + // matching the wire/hatch path (render_style_for_block_sub). + [l0_color[0], l0_color[1], l0_color[2], own_alpha] + } else { + return None; + }; + if let Some(keep) = &self.refedit_keep { + if !keep.contains(&h) { + c = crate::scene::cache::block_cache::fade_toward_bg(c, self.current_bg()); + } + } + Some(c) + } + /// Hatches eligible for click / box / lasso hit-testing in the current /// layout. Filters out block-internal source hatches (stored in /// `self.hatches` at block-local coords for the block-defn position, @@ -2572,7 +2687,7 @@ impl Scene { continue; } let mut sets = Vec::new(); - self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, &mut sets); + self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, None, &mut sets); for set in sets { if let Some(m) = set.lods.into_iter().next() { block_owned.push((ins.common.handle, m)); @@ -2615,7 +2730,7 @@ impl Scene { continue; } let mut sets = Vec::new(); - self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, &mut sets); + self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, None, &mut sets); let hit = sets.iter().any(|set| { set.lods.first().map_or(false, |m| { !pick::hit_test::mesh_box_hit( @@ -2660,7 +2775,7 @@ impl Scene { continue; } let mut sets = Vec::new(); - self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, &mut sets); + self.expand_block_meshes(&ins.block_name, &ins.get_transform(), 0, None, &mut sets); let hit = sets.iter().any(|set| { set.lods.first().map_or(false, |m| { !pick::hit_test::mesh_poly_hit( diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 342159c59..784b66596 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -533,26 +533,65 @@ pub(in crate::scene) fn render_style_for( .unwrap_or(&LineWeight::Default), _ => ew, }; - const MM_TO_PX: f32 = 96.0 / 25.4; - // CAD apps display model-space lineweights larger than their true - // physical size so the gradations stay legible on screen — at true - // scale a 0.5 mm line is ~2 px and is indistinguishable from thinner - // weights (which all floor to 1 px). Apply the same legibility boost so - // weights are pronounced and tell apart, matching other DWG editors. - // (#147) - const LWT_DISPLAY_BOOST: f32 = 2.0; - resolved - .millimeters() - .map(|mm| (mm as f32 * MM_TO_PX * LWT_DISPLAY_BOOST).max(1.0)) - .unwrap_or(1.0) + lineweight_to_px(resolved) }; (entity_color, pattern_length, pattern, line_weight_px, aci) } -/// Like `render_style_for` but resolves ByBlock properties by inheriting from -/// the INSERT entity's already-resolved style. Call this for exploded block -/// sub-entities so that ByBlock color/linetype/lineweight propagate correctly. +/// Resolved render style used as the inheritance source for a block child's +/// ByBlock properties (the INSERT's own style) or its layer-0 properties (the +/// INSERT's *layer* style). Bundled so it threads through the block-expansion +/// call chain as a single value. +#[derive(Clone, Copy, Debug)] +pub struct InheritStyle { + pub color: [f32; 4], + pub pat_len: f32, + pub pat: [f32; 8], + pub lw_px: f32, +} + +/// Convert a concrete (already layer-resolved) lineweight to display pixels. +pub(crate) fn lineweight_to_px(lw: &LineWeight) -> f32 { + const MM_TO_PX: f32 = 96.0 / 25.4; + // CAD apps display model-space lineweights larger than their true physical + // size so the gradations stay legible on screen — at true scale a 0.5 mm + // line is ~2 px and is indistinguishable from thinner weights (which all + // floor to 1 px). Apply the same legibility boost so weights are pronounced + // and tell apart, matching other DWG editors. (#147) + const LWT_DISPLAY_BOOST: f32 = 2.0; + lw.millimeters() + .map(|mm| (mm as f32 * MM_TO_PX * LWT_DISPLAY_BOOST).max(1.0)) + .unwrap_or(1.0) +} + +/// Resolve a layer's own color / linetype / lineweight to concrete render +/// values — what a fully-ByLayer entity on that layer would draw as. Used for +/// the layer-0 block rule: a block child on layer "0" inherits the block +/// reference's layer through this. Color is returned RAW (background adaptation +/// happens at emit time). Falls back to white / Continuous / 1 px when the +/// layer is missing. +pub(crate) fn layer_render_style(document: &CadDocument, layer_name: &str) -> InheritStyle { + let layer = document.layers.get(layer_name); + let color = layer.map(|l| &l.color).unwrap_or(&AcadColor::WHITE); + let [r, g, b, _] = tess_util::aci_to_rgba(color); + let lt_name = layer.map(|l| l.line_type.as_str()).unwrap_or("Continuous"); + let lt_scale = document.header.linetype_scale as f32; + let (pat_len, pat) = resolve_pattern(&document.line_types, lt_name, lt_scale); + let lw = layer.map(|l| &l.line_weight).unwrap_or(&LineWeight::Default); + InheritStyle { + color: [r, g, b, 1.0], + pat_len, + pat, + lw_px: lineweight_to_px(lw), + } +} + +/// Like `render_style_for` but resolves a block sub-entity's inherited +/// properties: ByBlock inherits the INSERT's style, and (the layer-0 rule) a +/// sub-entity on layer "0" with ByLayer properties inherits the INSERT's +/// *layer* style (`l0`). Explicit properties always win. Call this for +/// exploded block sub-entities so color/linetype/lineweight propagate right. pub(crate) fn render_style_for_block_sub( document: &CadDocument, e: &EntityType, @@ -560,24 +599,35 @@ pub(crate) fn render_style_for_block_sub( insert_pat_len: f32, insert_pat: [f32; 8], insert_lw_px: f32, + l0: InheritStyle, ) -> ([f32; 4], f32, [f32; 8], f32, u8) { let (color, pat_len, pat, lw_px, aci) = render_style_for(document, e); let common = e.common(); + let on_l0 = common.layer == "0"; let final_color = if common.color == AcadColor::ByBlock { insert_color + } else if on_l0 && common.color == AcadColor::ByLayer { + // Inherit the insert layer's RGB but keep the child's own transparency. + [l0.color[0], l0.color[1], l0.color[2], color[3]] } else { color }; + let lt_bylayer = + common.linetype.is_empty() || common.linetype.eq_ignore_ascii_case("bylayer"); let (final_pat_len, final_pat) = if common.linetype.eq_ignore_ascii_case("byblock") { (insert_pat_len, insert_pat) + } else if on_l0 && lt_bylayer { + (l0.pat_len, l0.pat) } else { (pat_len, pat) }; let final_lw = if matches!(common.line_weight, LineWeight::ByBlock) { insert_lw_px + } else if on_l0 && matches!(common.line_weight, LineWeight::ByLayer | LineWeight::Default) { + l0.lw_px } else { lw_px }; @@ -1013,3 +1063,93 @@ fn split_face3d_wires( } (face3d, others) } + +// ── Layer-0 block inheritance (#221) ────────────────────────────────────── +// A block child on layer "0" with ByLayer properties inherits the block +// reference's *layer*; every other layer is "sticky" (keeps its own layer); +// ByBlock inherits the insert's own style; explicit properties always win. +#[cfg(test)] +mod layer0_inherit_tests { + use super::*; + use acadrust::entities::Line; + use acadrust::tables::Layer; + use acadrust::types::{Color, Transparency}; + + // ACI: 1 = red, 3 = green, 7 = white. Distinct, so the assertions below + // can tell "inherited the insert layer" from "kept layer 0". + fn doc() -> CadDocument { + let mut d = CadDocument::new(); + let mut walls = Layer::new("Walls"); + walls.color = Color::Index(1); // red + d.layers.add_or_replace(walls); + let mut zero = Layer::new("0"); + zero.color = Color::Index(7); // white + d.layers.add_or_replace(zero); + let mut other = Layer::new("Other"); + other.color = Color::Index(3); // green + d.layers.add_or_replace(other); + d + } + + fn child(layer: &str, color: Color) -> EntityType { + let mut l = Line::new(); + l.common.layer = layer.to_string(); + l.common.color = color; + EntityType::Line(l) + } + + fn resolve(d: &CadDocument, e: &EntityType, ins: [f32; 4]) -> [f32; 4] { + // Insert sits on "Walls"; its layer style is the layer-0 target. + let l0 = layer_render_style(d, "Walls"); + render_style_for_block_sub(d, e, ins, l0.pat_len, l0.pat, l0.lw_px, l0).0 + } + + #[test] + fn layer0_bylayer_inherits_insert_layer() { + let d = doc(); + let walls = layer_render_style(&d, "Walls").color; + let zero = layer_render_style(&d, "0").color; + let c = resolve(&d, &child("0", Color::ByLayer), walls); + assert_eq!(&c[..3], &walls[..3], "layer-0 child must show the insert's layer (Walls)"); + assert_ne!(&c[..3], &zero[..3], "layer-0 child must NOT show layer 0's own color"); + } + + #[test] + fn nonzero_layer_is_sticky() { + let d = doc(); + let walls = layer_render_style(&d, "Walls").color; + let other = layer_render_style(&d, "Other").color; + let c = resolve(&d, &child("Other", Color::ByLayer), walls); + assert_eq!(&c[..3], &other[..3], "a child on a normal layer keeps its own layer"); + } + + #[test] + fn byblock_inherits_insert_color() { + let d = doc(); + let ins = [0.2, 0.4, 0.6, 1.0]; + let c = resolve(&d, &child("0", Color::ByBlock), ins); + assert_eq!(&c[..3], &ins[..3], "ByBlock child uses the insert's color"); + } + + #[test] + fn explicit_color_wins_even_on_layer0() { + let d = doc(); + let walls = layer_render_style(&d, "Walls").color; + let green = tess_util::aci_to_rgba(&Color::Index(3)); + let c = resolve(&d, &child("0", Color::Index(3)), walls); + assert_eq!(&c[..3], &green[..3], "an explicit color must win even on layer 0"); + } + + #[test] + fn layer0_preserves_child_transparency() { + let d = doc(); + let walls = layer_render_style(&d, "Walls").color; + let mut l = Line::new(); + l.common.layer = "0".to_string(); + l.common.color = Color::ByLayer; + l.common.transparency = Transparency::from_percent(0.5); // 50% transparent + let c = resolve(&d, &EntityType::Line(l), walls); + assert_eq!(&c[..3], &walls[..3], "RGB inherited from the insert layer"); + assert!((c[3] - 0.5).abs() < 0.02, "child's own 50% transparency is kept, got {}", c[3]); + } +}