From d54fb9e2aa0c2989b8280b87e082d17d6052e953 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Jul 2026 21:24:56 +0300 Subject: [PATCH] fix(render): annotative scale representations + model-space linetype scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two rendering defects surfaced on the budweiser benchmark drawings, both tied to a drawing's annotation scale. Annotative scale representations. A drawing may materialise each annotation scale as its own object (here a block insert per scale, on a "0 @ " layer, each carrying a per-object AcDbContextData bound to one scale). Nothing filtered them, so every scale drew at once — a 1× copy stacked under a 10× one. Add annotative::annotative_offscale and apply it in the three paths that expand block content: the wire block-defn cache, the resident/top-level visibility gate (model space only — paper viewports use their per-viewport frozen scale layers), and the exploded-insert hatch/fill walk (filtered before normalize strips the handle). An object is shown when a representation matches the current scale; the off-scale ones are hidden. When the current scale is unsupported by any representation, fall back to the base "1:1" copy instead of hiding everything (the object must not vanish) or showing everything (the copies must not stack). Model-space linetype scale. PSLTSCALE (a paper-space flag) was applied to model-space linetypes via the annotation scale, so a drawing saved at e.g. CANNOSCALE 10:1 drew its linetypes 10× off. Thread a `paper_space` flag through tessellate_entity and gate the PSLTSCALE factor on it, so it only scales linetypes for content shown inside a paper-space viewport; model space keeps LTSCALE × CELTSCALE. Co-Authored-By: Claude Opus 4.8 --- src/scene/annotative.rs | 53 ++++++++++++++++++++++++++++++++++ src/scene/cache/block_cache.rs | 6 ++++ src/scene/convert/tess.rs | 16 ++++++++-- src/scene/entity.rs | 23 +++++++++++++++ src/scene/mod.rs | 17 +++++++++++ 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index 23ecda2be..b97377dc0 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -222,6 +222,59 @@ pub fn create_annotation_context( true } +/// True when `common` belongs to an annotative object whose per-object scale +/// contexts are *all* bound to scales other than the current one — an +/// off-scale representation that must stay hidden. A drawing may materialise +/// each annotation scale as its own object (e.g. a block insert per scale on a +/// "0 @ " layer); only the current scale's representation should draw. +/// +/// False for non-annotative objects (no per-object context — the vast +/// majority) and for objects whose contexts include the current scale. Gated +/// on an extension dictionary so non-annotative entities skip the lookup. +pub fn annotative_offscale(doc: &CadDocument, common: &EntityCommon) -> bool { + if !common + .xdictionary_handle + .map(|h| !h.is_null()) + .unwrap_or(false) + { + return false; + } + let scales = object_scale_memberships(doc, common.handle); + if scales.is_empty() { + return false; + } + let cur = &doc.header.current_annotation_scale; + if scales.iter().any(|(name, _)| name.eq_ignore_ascii_case(cur)) { + return false; + } + // Off-scale (no context for the current scale). If some representation in + // the drawing DOES provide the current scale, hide this one — the matching + // representation is the one to show. + if current_scale_provided(doc) { + return true; + } + // The current scale is unsupported by any representation. Fall back to the + // base "1:1" representation: keep it, hide the enlarged copies — otherwise + // every scale representation stacks (or, if all were hidden, the object + // vanishes). Without this, opening at e.g. CANNOSCALE 10:1 shows both a 1× + // and a 10× copy of the same block. + !scales.iter().any(|(name, _)| name.eq_ignore_ascii_case("1:1")) +} + +/// Whether any annotative representation in the drawing targets the current +/// annotation scale. +fn current_scale_provided(doc: &CadDocument) -> bool { + let cur = &doc.header.current_annotation_scale; + doc.objects.values().any(|o| { + if let ObjectType::ObjectContextData(cd) = o { + if let Some(ObjectType::Scale(s)) = doc.objects.get(&cd.scale) { + return s.name.eq_ignore_ascii_case(cur); + } + } + false + }) +} + /// The annotation scales an object currently carries a per-object context for, /// as `(scale name, scale handle)` pairs (one per representation). Empty when /// the object has no per-object context chain. diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 9837ae5ee..6cf62b8de 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -342,6 +342,12 @@ fn build_defn( if layer_hidden(doc, &entity.common().layer) { continue; } + // Annotative scale representation: bake only the current scale's copy + // into the defn so off-scale representations don't stack (e.g. a 1× + // copy under a 10×). See `annotative::annotative_offscale`. + if crate::scene::annotative::annotative_offscale(doc, entity.common()) { + continue; + } match entity { EntityType::Block(_) | EntityType::BlockEnd(_) => continue, // A non-constant ATTDEF is only a template — the insert supplies an diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index bb9c13ba6..de6426725 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -325,6 +325,7 @@ pub(crate) fn tessellate_entity_dim_text( None, view_aabb, world_per_pixel, + false, ); for w in &mut wires { // Synth dim text carries no real entity colour — paint everything @@ -351,6 +352,11 @@ pub(crate) fn tessellate_entity( view_aabb: Option<[f32; 4]>, // World units per screen pixel for LOD culling. `None` = no LOD. world_per_pixel: Option, + // True only when tessellating content shown INSIDE a paper-space viewport, + // where PSLTSCALE scales linetypes by the viewport scale. Model-space (and + // paper-sheet) rendering passes false: a drawing's annotation scale must + // not resize model-space linetypes (that is MSLTSCALE, off here). + paper_space: bool, ) -> Vec { let h = e.common().handle; let sel = selected.contains(&h); @@ -501,8 +507,12 @@ pub(crate) fn tessellate_entity( let entity_color = fade_if_locked(document, e, entity_color, bg_color); let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32; let lt_name = view::render::linetype_name_for(document, e); - // PSLTSCALE: scale linetype dashes by viewport anno_scale so they appear uniform in paper space. - let pslt_factor = if document.header.paper_space_linetype_scaling { + // PSLTSCALE: scale linetype dashes by the viewport scale so they appear + // uniform in paper space. Only applies to content shown inside a paper-space + // viewport (`paper_space`); model space uses LTSCALE × CELTSCALE unscaled by + // the annotation scale, otherwise a drawing at e.g. CANNOSCALE 10:1 draws + // its linetypes 10× off. + let pslt_factor = if paper_space && document.header.paper_space_linetype_scaling { anno_scale } else { 1.0 @@ -760,6 +770,7 @@ pub(crate) fn tessellate_entity( block_cache, view_aabb, world_per_pixel, + paper_space, ); for mut w in sub_wires { w.name = h.value().to_string(); @@ -921,6 +932,7 @@ pub(crate) fn tessellate_entity( block_cache, view_aabb, world_per_pixel, + paper_space, ); for mut w in sub_wires { w.name = h.value().to_string(); diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 6835cfe67..5c589fd0c 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -912,6 +912,14 @@ impl Scene { ) { continue; } + // Off-scale annotative representation (model space only — paper + // viewports use their per-viewport frozen scale layers). Skips its + // whole fill subtree, matching the wire path. + if frozen.is_none() + && crate::scene::annotative::annotative_offscale(&self.document, &ins.common) + { + continue; + } if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo) && !self.block_has_wide_poly(&ins.block_name, &mut wide_block_memo) { @@ -942,6 +950,17 @@ impl Scene { .get(&ins.common.handle.value()) .copied() .unwrap_or([0.0, 0.0]); + // Drop off-scale annotative representations (e.g. a 10× copy on + // layer "0 @ 10" when the current scale is 1:1) before `normalize` + // strips their handle — otherwise their whole fill subtree stacks + // under the current-scale copy. Model space only; paper viewports + // use their per-viewport frozen scale layers. `explode` preserves + // the child handle, so the membership lookup still resolves here. + let offscale = |e: &EntityType| -> bool { + frozen.is_none() + && matches!(e, EntityType::Insert(ni) + if crate::scene::annotative::annotative_offscale(&self.document, &ni.common)) + }; let mut stack: Vec<( EntityType, usize, @@ -950,6 +969,7 @@ impl Scene { (f32, f32), )> = explode_including_dims(ins, &self.document) .into_iter() + .filter(|e| !offscale(e)) .map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap))) .collect(); while let Some((sub, depth, sub_ins_color, sub_l0, (d_base, d_half))) = stack.pop() { @@ -993,6 +1013,9 @@ impl Scene { * d_half; let nd_half = d_half / (block_count(&nins.block_name) as f32 + 1.0); for e in explode_including_dims(&nins, &self.document) { + if offscale(&e) { + continue; + } stack.push(( normalize(e), depth + 1, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1bcb73a49..494c02da2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3212,6 +3212,7 @@ impl Scene { Some(&blk), None, None, + anno_scale_override.is_some(), ); memo_updates.push((*h, Arc::new(raw.clone()))); let mut faded = raw; @@ -4541,6 +4542,16 @@ impl Scene { } } } + // Annotative scale representation: draw only the current scale's copy. + // Model-space only (`frozen_layers` is None): a paper-space viewport + // renders at its own annotation scale and already hides the off-scale + // representations through its per-viewport frozen "0 @ " layers, + // so applying the model-space scale here would fight that. + if frozen_layers.is_none() + && crate::scene::annotative::annotative_offscale(&self.document, c) + { + return false; + } self.belongs_to_visible_block(c.handle, c.owner_handle, block_handle) } @@ -4674,6 +4685,9 @@ impl Scene { } else { 1.0 }; + // Content shown inside a paper-space viewport carries a scale override; + // only there does PSLTSCALE resize linetypes by the viewport scale. + let paper = anno_scale_override.is_some(); let blk_cache = self.block_cache_arc(); let blk_ref: &cache::block_cache::BlockCache = &blk_cache; // Zoom-adaptive curve sampling for top-level Edge tessellation. Target @@ -4776,6 +4790,7 @@ impl Scene { let e: &EntityType = e; let w = tessellate_entity( doc, sel, avp, bg, anno, e, Some(blk_ref), view_aabb, wpp, + paper, ); (e.common().handle, Arc::new(w)) }) @@ -4795,6 +4810,7 @@ impl Scene { .flat_map(|e| { tessellate_entity( doc, sel, avp, bg, anno, e, Some(blk_ref), view_aabb, wpp, + paper, ) }) .collect() @@ -5055,6 +5071,7 @@ impl Scene { Some(&blk_cache), None, None, + self.current_layout != "Model", ) }