Extents: ignore what draws nothing (rays, missing or empty blocks, hidden block geometry), as ZOOM Extents does (#288)
* Extents: ignore rays and references to missing or empty blocks, as ZOOM Extents does Opening real drawings in CADCraft often showed a tiny plan in a huge empty canvas, because the extents included things that draw nothing: - the base point of a RAY (Autodesk's own reference says infinite construction lines and rays do not affect ZOOM Extents; an office plan in our corpus carries two rays 1.98 million units from the building); - the insertion point of an INSERT whose block is missing (a purged definition, a bound xref that never came along) or empty (an unloaded xref): a floor plan had such references 125 000 and 3.4 million units away. XLINEs were already ignored; rays now are too, and an insert contributes only what its block draws (attributes still count). Two regression tests use the measured coordinates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Extents: skip invisible block entities, attdefs and blank texts - Entities in a block definition count for an insert's (and a dimension block's) extents only if they are not AttDefs, have common.visible set, and sit on layer "0" or on a layer that is on and thawed. Dynamic blocks keep hidden constraint geometry far from the visible part. - Text/MText that is empty, whitespace-only or only "\~" has empty bounds. - Tests cover each rule separately. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7336a3f597
commit
253f0c8497
|
|
@ -34,6 +34,22 @@ fn text_box(t: &crate::Text, len: usize) -> Bounds2 {
|
|||
Bounds2::from_points(local.map(|p| origin.xy() + p.rotate(t.rotation)))
|
||||
}
|
||||
|
||||
/// Whether an entity inside a block definition shows: its own invisible flag and its layer count,
|
||||
/// as when drawing. Dimensional constraints of dynamic blocks (layer `*ADSK_CONSTRAINTS`) and the
|
||||
/// entities a visibility state hides are invisible this way, and they sit far from the block's
|
||||
/// geometry in real drawings. Layer "0" takes the insert's layer, which the caller has checked.
|
||||
fn in_block_visible(d: &Drawing, e: &Entity) -> bool {
|
||||
// Attribute definitions are not drawn in an insert (its attribs are, see below).
|
||||
!matches!(e.kind, EntityKind::AttDef(_))
|
||||
&& e.common.visible
|
||||
&& (e.common.layer == "0" || d.layer(&e.common.layer).is_none_or(crate::Layer::visible))
|
||||
}
|
||||
|
||||
/// A text that draws no glyph (empty, or only blanks and non-breaking spaces) takes no room.
|
||||
fn blank(s: &str) -> bool {
|
||||
s.replace("\\~", " ").chars().all(char::is_whitespace)
|
||||
}
|
||||
|
||||
pub fn entity_bounds(d: &Drawing, e: &Entity, depth: usize) -> Bounds2 {
|
||||
bounds_in(d, e, depth, &mut HashMap::new())
|
||||
}
|
||||
|
|
@ -44,6 +60,8 @@ pub fn entity_bounds(d: &Drawing, e: &Entity, depth: usize) -> Bounds2 {
|
|||
/// per reference (which grows exponentially with the nesting depth).
|
||||
fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String, Bounds2>) -> Bounds2 {
|
||||
match &e.kind {
|
||||
EntityKind::Text(t) if blank(&t.value) => Bounds2::EMPTY,
|
||||
EntityKind::MText(t) if blank(&t.contents) => Bounds2::EMPTY,
|
||||
EntityKind::Text(t) => text_box(t, t.value.chars().count()),
|
||||
EntityKind::AttDef(a) => text_box(&a.text, a.tag.chars().count()),
|
||||
EntityKind::MText(t) => {
|
||||
|
|
@ -70,21 +88,23 @@ fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String,
|
|||
if depth >= MAX_BLOCK_DEPTH {
|
||||
return Bounds2::EMPTY;
|
||||
}
|
||||
let Some(blk) = d.block(&ins.block) else { return Bounds2::from_points([ins.insert.xy()]) };
|
||||
// A reference to a missing block (an unloaded xref, a purged definition) or to an empty
|
||||
// one draws nothing, so it contributes nothing: ZOOM Extents must not fly off to its
|
||||
// insertion point. Attributes below still count.
|
||||
let Some(blk) = d.block(&ins.block) else { return Bounds2::EMPTY };
|
||||
let m = ins.transform(blk.base.xy());
|
||||
let inner = match blocks.get(&blk.name) {
|
||||
Some(b) => *b,
|
||||
None => {
|
||||
let mut inner = Bounds2::EMPTY;
|
||||
for be in blk.entities.iter() {
|
||||
for be in blk.entities.iter().filter(|be| in_block_visible(d, be)) {
|
||||
inner = inner.union(&bounds_in(d, be, depth + 1, blocks));
|
||||
}
|
||||
blocks.insert(blk.name.clone(), inner);
|
||||
inner
|
||||
}
|
||||
};
|
||||
let mut b =
|
||||
if inner.is_empty() { Bounds2::from_points([ins.insert.xy()]) } else { Bounds2::from_points(inner.corners().map(|c| m.apply(c))) };
|
||||
let mut b = if inner.is_empty() { Bounds2::EMPTY } else { Bounds2::from_points(inner.corners().map(|c| m.apply(c))) };
|
||||
if ins.cols > 1 || ins.rows > 1 {
|
||||
let off = Vec2::new(ins.col_spacing * f64::from(ins.cols.saturating_sub(1)), ins.row_spacing * f64::from(ins.rows.saturating_sub(1)))
|
||||
.rotate(ins.rotation);
|
||||
|
|
@ -122,7 +142,7 @@ fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String,
|
|||
if let Some(blk) = dm.block.as_ref().and_then(|n| d.block(n))
|
||||
&& depth < MAX_BLOCK_DEPTH
|
||||
{
|
||||
for be in blk.entities.iter() {
|
||||
for be in blk.entities.iter().filter(|be| in_block_visible(d, be)) {
|
||||
b = b.union(&bounds_in(d, be, depth + 1, blocks));
|
||||
}
|
||||
}
|
||||
|
|
@ -156,14 +176,9 @@ fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String,
|
|||
Prim::Ellipse(e) => e.bounds(),
|
||||
Prim::Spline(s) => s.bounds(),
|
||||
Prim::Point(p) => Bounds2::from_points([p]),
|
||||
// Infinite lines don't contribute to extents (as in ZOOM Extents).
|
||||
Prim::Infinite { base, ray, .. } => {
|
||||
if ray {
|
||||
Bounds2::from_points([base])
|
||||
} else {
|
||||
Bounds2::EMPTY
|
||||
}
|
||||
}
|
||||
// Infinite lines don't contribute to extents, rays included (as in ZOOM Extents;
|
||||
// real drawings carry stray rays millions of units away).
|
||||
Prim::Infinite { .. } => Bounds2::EMPTY,
|
||||
Prim::Fill(pts) => Bounds2::from_points(pts),
|
||||
};
|
||||
b = b.union(&pb);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,49 @@ fn hidden_layers_excluded_from_extents() {
|
|||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(1.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rays_and_xlines_do_not_stretch_extents() {
|
||||
// ZOOM Extents ignores infinite lines; real drawings carry stray rays millions of units away.
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), line((0.0, 0.0), (1.0, 1.0))).unwrap();
|
||||
let far = Vec3::new(-1_977_989.0, -1_952_638.0, 0.0);
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Ray(RayLine { base: far, dir: Vec3::new(-0.7, -0.7, 0.0) })).unwrap();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::XLine(RayLine { base: far, dir: Vec3::new(1.0, 0.0, 0.0) })).unwrap();
|
||||
let b = d.extents(&Space::Model);
|
||||
assert_eq!((b.min, b.max), (Vec2::ZERO, Vec2::new(1.0, 1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserts_of_missing_or_empty_blocks_do_not_stretch_extents() {
|
||||
// An unloaded xref or a purged definition draws nothing, so its insertion point is not an extent.
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), line((0.0, 0.0), (1.0, 1.0))).unwrap();
|
||||
let ins = |block: &str, x: f64, y: f64| {
|
||||
EntityKind::Insert(Insert {
|
||||
block: block.into(),
|
||||
insert: Vec3::new(x, y, 0.0),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
})
|
||||
};
|
||||
d.blocks.insert("Empty".into(), std::sync::Arc::new(Block::new("Empty")));
|
||||
d.add(&Space::Model, Common::default(), ins("Missing", -125_319.0, -79_990.0)).unwrap();
|
||||
d.add(&Space::Model, Common::default(), ins("Empty", 55_652.0, -3_448_576.0)).unwrap();
|
||||
let b = d.extents(&Space::Model);
|
||||
assert_eq!((b.min, b.max), (Vec2::ZERO, Vec2::new(1.0, 1.0)));
|
||||
// A block with geometry still counts.
|
||||
let mut full = Block::new("Full");
|
||||
full.entities.push(Entity::new(Handle(1), line((0.0, 0.0), (2.0, 2.0))));
|
||||
d.blocks.insert("Full".into(), std::sync::Arc::new(full));
|
||||
d.add(&Space::Model, Common::default(), ins("Full", 10.0, 10.0)).unwrap();
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(12.0, 12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_arc_mirror_keeps_shape() {
|
||||
let mut k = EntityKind::Arc(Arc { center: Vec3::ZERO, radius: 1.0, start: 0.0, end: std::f64::consts::FRAC_PI_2 });
|
||||
|
|
@ -187,3 +230,235 @@ fn wide_arc_segment_extents_reach_the_outer_edge() {
|
|||
assert!(over > -1e-6 && over < 1e-3, "got {b:?}, exact {exact:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn plain_insert(block: &str) -> EntityKind {
|
||||
EntityKind::Insert(Insert {
|
||||
block: block.into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn on_layer(layer: &str, kind: EntityKind, h: u64) -> Entity {
|
||||
Entity { handle: Handle(h), common: Common { layer: layer.into(), ..Default::default() }, kind }
|
||||
}
|
||||
|
||||
/// Model space with one insert of a block holding a near line (0,0)-(1,1) plus `extra`.
|
||||
fn drawing_with_block(extra: Entity) -> Drawing {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut blk = Block::new("B");
|
||||
blk.entities.push(Entity::new(Handle(1), line((0.0, 0.0), (1.0, 1.0))));
|
||||
blk.entities.push(extra);
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
|
||||
d.add(&Space::Model, Common::default(), plain_insert("B")).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn far_line(layer: &str) -> Entity {
|
||||
on_layer(layer, line((5000.0, 5000.0), (6000.0, 6000.0)), 2)
|
||||
}
|
||||
|
||||
fn extents_pair(d: &Drawing) -> (Vec2, Vec2) {
|
||||
let b = d.extents(&Space::Model);
|
||||
(b.min, b.max)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_entity_with_invisible_flag_is_ignored_in_insert_extents() {
|
||||
let mut far = far_line("0");
|
||||
far.common.visible = false;
|
||||
let d = drawing_with_block(far);
|
||||
assert_eq!(extents_pair(&d), (Vec2::ZERO, Vec2::new(1.0, 1.0)));
|
||||
// Control: the same entity visible does count.
|
||||
let d = drawing_with_block(far_line("0"));
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_entity_on_off_or_frozen_layer_is_ignored() {
|
||||
for frozen in [false, true] {
|
||||
let mut d = drawing_with_block(far_line("Hidden"));
|
||||
assert!(d.layer("Hidden").is_none(), "block contents do not create layers");
|
||||
let mut layer = Layer::new("Hidden");
|
||||
if frozen {
|
||||
layer.frozen = true;
|
||||
} else {
|
||||
layer.on = false;
|
||||
}
|
||||
d.layers.push(layer);
|
||||
assert_eq!(extents_pair(&d), (Vec2::ZERO, Vec2::new(1.0, 1.0)), "frozen = {frozen}");
|
||||
// Turned back on, it counts again.
|
||||
let l = d.layer_mut("Hidden").unwrap();
|
||||
l.on = true;
|
||||
l.frozen = false;
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_entity_on_layer_zero_counts_even_if_layer_zero_is_off_or_missing() {
|
||||
// Layer "0" takes the insert's layer, which the caller has already checked; so it counts here.
|
||||
// The insert itself sits on layer "Vis".
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common { layer: "Vis".into(), ..Default::default() }, plain_insert("B")).unwrap();
|
||||
let mut blk = Block::new("B");
|
||||
blk.entities.push(far_line("0"));
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
|
||||
// Layer 0 switched off.
|
||||
d.layer_mut("0").unwrap().on = false;
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
// Layer 0 frozen.
|
||||
let l = d.layer_mut("0").unwrap();
|
||||
l.on = true;
|
||||
l.frozen = true;
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
// Layer 0 not defined at all.
|
||||
d.layers.retain(|l| l.name != "0");
|
||||
assert!(d.layer("0").is_none());
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_entity_on_undefined_layer_counts() {
|
||||
// An unknown layer name is not hidden (is_none_or).
|
||||
let d = drawing_with_block(far_line("Nowhere"));
|
||||
assert!(d.layer("Nowhere").is_none());
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attdef_in_block_is_ignored_in_insert_extents() {
|
||||
let attdef = Entity::new(
|
||||
Handle(2),
|
||||
EntityKind::AttDef(Attrib {
|
||||
tag: "TAG".into(),
|
||||
text: Text {
|
||||
insert: Vec3::new(5000.0, 5000.0, 0.0),
|
||||
align_pt: None,
|
||||
height: 1.0,
|
||||
value: "x".into(),
|
||||
rotation: 0.0,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Standard".into(),
|
||||
halign: HAlign::Left,
|
||||
valign: VAlign::Baseline,
|
||||
},
|
||||
invisible: false,
|
||||
constant: false,
|
||||
prompt: String::new(),
|
||||
}),
|
||||
);
|
||||
let d = drawing_with_block(attdef);
|
||||
assert_eq!(extents_pair(&d), (Vec2::ZERO, Vec2::new(1.0, 1.0)));
|
||||
}
|
||||
|
||||
fn text_kind(value: &str) -> EntityKind {
|
||||
EntityKind::Text(Text {
|
||||
insert: Vec3::new(10.0, 10.0, 0.0),
|
||||
align_pt: None,
|
||||
height: 2.0,
|
||||
value: value.into(),
|
||||
rotation: 0.0,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Standard".into(),
|
||||
halign: HAlign::Left,
|
||||
valign: VAlign::Baseline,
|
||||
})
|
||||
}
|
||||
|
||||
fn mtext_kind(contents: &str) -> EntityKind {
|
||||
EntityKind::MText(MText {
|
||||
insert: Vec3::new(10.0, 10.0, 0.0),
|
||||
height: 2.0,
|
||||
width: 0.0,
|
||||
attach: 1,
|
||||
rotation: 0.0,
|
||||
style: "Standard".into(),
|
||||
contents: contents.into(),
|
||||
line_spacing: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_text_and_mtext_have_no_bounds() {
|
||||
let d = Drawing::new_imperial();
|
||||
for s in ["", " ", " ", "\t \n", "\\~", " \\~ \\~", "\u{a0}"] {
|
||||
for kind in [text_kind(s), mtext_kind(s)] {
|
||||
let b = entity_bounds(&d, &Entity::new(Handle(1), kind), 0);
|
||||
assert!(b.is_empty(), "{s:?} should have empty bounds");
|
||||
}
|
||||
}
|
||||
// Blank texts do not stretch the drawing extents either.
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), line((0.0, 0.0), (1.0, 1.0))).unwrap();
|
||||
d.add(&Space::Model, Common::default(), text_kind(" \\~")).unwrap();
|
||||
d.add(&Space::Model, Common::default(), mtext_kind("")).unwrap();
|
||||
assert_eq!(extents_pair(&d), (Vec2::ZERO, Vec2::new(1.0, 1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_blank_text_and_mtext_have_bounds() {
|
||||
let d = Drawing::new_imperial();
|
||||
for kind in [text_kind("Hi"), text_kind(" a "), mtext_kind("Hi"), mtext_kind("a\\Pb"), mtext_kind("\\~x")] {
|
||||
let b = entity_bounds(&d, &Entity::new(Handle(1), kind.clone()), 0);
|
||||
assert!(!b.is_empty(), "{kind:?} should have bounds");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invisible_dimblock_entity_is_ignored_in_dimension_extents() {
|
||||
// Dynamic blocks keep dimensional constraints on layer *ADSK_CONSTRAINTS (group 60 = 1).
|
||||
let dim = |d: &mut Drawing, far: Entity| {
|
||||
let mut blk = Block::new("*D1");
|
||||
blk.entities.push(Entity::new(Handle(1), line((0.0, 0.0), (1.0, 0.0))));
|
||||
blk.entities.push(far);
|
||||
d.blocks.insert("*D1".into(), std::sync::Arc::new(blk));
|
||||
d.add(
|
||||
&Space::Model,
|
||||
Common::default(),
|
||||
EntityKind::Dimension(Dimension {
|
||||
kind: DimKind::Linear { rotation: 0.0 },
|
||||
defpt: Vec3::new(1.0, 1.0, 0.0),
|
||||
text_mid: Vec3::new(0.5, 1.0, 0.0),
|
||||
p13: Vec3::ZERO,
|
||||
p14: Vec3::new(1.0, 0.0, 0.0),
|
||||
p15: Vec3::ZERO,
|
||||
p16: Vec3::ZERO,
|
||||
text: String::new(),
|
||||
style: "Standard".into(),
|
||||
measurement: 1.0,
|
||||
text_rotation: 0.0,
|
||||
user_text_pos: false,
|
||||
block: Some("*D1".into()),
|
||||
overrides: Default::default(),
|
||||
assoc: vec![],
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
// Invisible flag.
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut hidden = on_layer("*ADSK_CONSTRAINTS", line((5000.0, 5000.0), (6000.0, 6000.0)), 2);
|
||||
hidden.common.visible = false;
|
||||
dim(&mut d, hidden);
|
||||
assert!(d.extents(&Space::Model).max.x < 100.0);
|
||||
// Hidden by its layer state.
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut l = Layer::new("*ADSK_CONSTRAINTS");
|
||||
l.on = false;
|
||||
d.layers.push(l);
|
||||
dim(&mut d, on_layer("*ADSK_CONSTRAINTS", line((5000.0, 5000.0), (6000.0, 6000.0)), 2));
|
||||
assert!(d.extents(&Space::Model).max.x < 100.0);
|
||||
// Control: a visible far entity does count.
|
||||
let mut d = Drawing::new_imperial();
|
||||
dim(&mut d, far_line("0"));
|
||||
assert_eq!(d.extents(&Space::Model).max, Vec2::new(6000.0, 6000.0));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue