mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 03:50:04 -06:00
XCLIP: keep CADCraft's inverted flag beside group 290 for DWG, whose bridge drops 290; the flag is read only when 290 is absent
This commit is contained in:
@@ -90,11 +90,11 @@ fn extents_are_the_boundary_box_as_in_the_reference_app() {
|
||||
// A boundary reaching past the block (0..100 x -50..50): its own box, not the overlap.
|
||||
s.execute("xclip", &json!({ "handle": r1, "rectangle": [[50, -20], [150, 30]] })).unwrap();
|
||||
let b = bounds(&s);
|
||||
assert!((b.min - Vec2::new(50.0, -20.0)).length() < 1e-9 && (b.max - Vec2::new(150.0, 30.0)).length() < 1e-9, "{b:?}");
|
||||
assert!((b.min - Vec2::new(50.0, -20.0)).len() < 1e-9 && (b.max - Vec2::new(150.0, 30.0)).len() < 1e-9, "{b:?}");
|
||||
// One that misses the block entirely: still its box.
|
||||
s.execute("xclip", &json!({ "handle": r1, "rectangle": [[200, 200], [300, 300]] })).unwrap();
|
||||
let b = bounds(&s);
|
||||
assert!((b.min - Vec2::new(200.0, 200.0)).length() < 1e-9 && (b.max - Vec2::new(300.0, 300.0)).length() < 1e-9, "{b:?}");
|
||||
assert!((b.min - Vec2::new(200.0, 200.0)).len() < 1e-9 && (b.max - Vec2::new(300.0, 300.0)).len() < 1e-9, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
//! the boundary into the block's coordinates, and the boundary's own transform (identity here).
|
||||
//! Group 290 is 1 for an inverted clip ("Invert clip"), as the reference app writes it.
|
||||
//!
|
||||
//! The DWG bridge drops group 290, so an inverted clip is also marked by CADCraft's own entry
|
||||
//! `CADCRAFT_XCLIP` beside `ACAD_FILTER` (an XRECORD with group 70 = 1), which a DWG keeps. It
|
||||
//! is read only when the filter has no group 290.
|
||||
//!
|
||||
//! Reading takes the planar filters the model can hold out of the kept foreign data and puts them
|
||||
//! on the references ([`read`]); others (front/back clipping, a skewed boundary transform) stay
|
||||
//! foreign and are written back untouched. Writing adds the dictionaries and the filter of every
|
||||
@@ -18,6 +22,8 @@ use cadcraft_doc::foreign::{Foreign, ForeignObject};
|
||||
use cadcraft_doc::{ClipBoundary, Drawing, EntityKind, Handle, MAX_CLIP_VERTICES, RawTag};
|
||||
use cadcraft_geom::{Mat3, Vec2};
|
||||
|
||||
/// The extension-dictionary entry of CADCraft's inverted-clip flag (for DWG, see above).
|
||||
const INVERT_KEY: &str = "CADCRAFT_XCLIP";
|
||||
/// Handles above this are not handed out (the DXF handle space is 64-bit, keep a margin).
|
||||
const MAX_HANDLE: u64 = 1 << 48;
|
||||
|
||||
@@ -124,6 +130,14 @@ fn parse_filter(o: &ForeignObject) -> Option<ClipBoundary> {
|
||||
(clip.points.len() == count && cadcraft_render::clip::PolyClip::new(&clip.polygon()).is_some()).then_some(clip)
|
||||
}
|
||||
|
||||
/// Whether the extension dictionary `xd` marks the clip inverted (CADCraft's own flag).
|
||||
fn inverted_record(f: &Foreign, xd: Handle) -> bool {
|
||||
object(f, xd)
|
||||
.and_then(|o| entry(&o.tags, INVERT_KEY))
|
||||
.and_then(|h| object(f, h))
|
||||
.is_some_and(|o| o.kind.eq_ignore_ascii_case("XRECORD") && number(&o.tags, 70).is_some_and(|v| v != 0.0))
|
||||
}
|
||||
|
||||
/// Put the planar filters of the file read into `d` on their references, taking them out of
|
||||
/// the kept foreign data (the model writes them from now on).
|
||||
pub(crate) fn read(d: &mut Drawing) {
|
||||
@@ -135,8 +149,12 @@ pub(crate) fn read(d: &mut Drawing) {
|
||||
.entities
|
||||
.keys()
|
||||
.filter_map(|h| {
|
||||
let (_, _, o) = chain(&d.foreign, *h)?;
|
||||
Some((*h, parse_filter(o)?))
|
||||
let (xd, _, o) = chain(&d.foreign, *h)?;
|
||||
let mut clip = parse_filter(o)?;
|
||||
if number(&o.tags, 290).is_none() {
|
||||
clip.inverted = inverted_record(&d.foreign, xd);
|
||||
}
|
||||
Some((*h, clip))
|
||||
})
|
||||
.collect();
|
||||
if found.is_empty() {
|
||||
@@ -173,13 +191,14 @@ pub(crate) fn read(d: &mut Drawing) {
|
||||
}
|
||||
|
||||
/// Take the clip of entity `h` out of the foreign data: the ACAD_FILTER entry and what it holds,
|
||||
/// and the extension dictionary when nothing else is left in it.
|
||||
/// CADCraft's inverted flag, and the extension dictionary when nothing else is left in it.
|
||||
fn forget(f: &mut Foreign, h: Handle) {
|
||||
let Some(xd) = f.entities.get(&h).and_then(|x| x.xdict) else { return };
|
||||
let mut gone: Vec<Handle> = Vec::new();
|
||||
let mut empty = false;
|
||||
if let Some(o) = f.objects.iter_mut().find(|o| o.handle == xd) {
|
||||
gone.extend(remove_entry(&mut o.tags, "ACAD_FILTER"));
|
||||
gone.extend(remove_entry(&mut o.tags, INVERT_KEY));
|
||||
empty = !o.tags.iter().any(|t| t.code == 3);
|
||||
}
|
||||
// The filter dictionary's entries go with it.
|
||||
@@ -252,6 +271,23 @@ fn spatial_filter(h: Handle, owner: Handle, clip: &ClipBoundary, to_world: &Mat3
|
||||
ForeignObject { handle: h, kind: "SPATIAL_FILTER".into(), tags }
|
||||
}
|
||||
|
||||
fn invert_record(h: Handle, owner: Handle) -> ForeignObject {
|
||||
ForeignObject {
|
||||
handle: h,
|
||||
kind: "XRECORD".into(),
|
||||
tags: vec![
|
||||
raw(5, h.hex()),
|
||||
raw(102, "{ACAD_REACTORS"),
|
||||
raw(330, owner.hex()),
|
||||
raw(102, "}"),
|
||||
raw(330, owner.hex()),
|
||||
raw(100, "AcDbXrecord"),
|
||||
raw(280, 1),
|
||||
raw(70, 1),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of `d` whose foreign data holds the SPATIAL_FILTER of every clipped reference (any
|
||||
/// filter kept from the file for it is replaced). `None` when no reference is clipped.
|
||||
pub(crate) fn prepare(d: &Drawing) -> Option<Drawing> {
|
||||
@@ -296,13 +332,20 @@ pub(crate) fn prepare(d: &Drawing) -> Option<Drawing> {
|
||||
}
|
||||
};
|
||||
let (fd, sf) = (fresh(&mut next), fresh(&mut next));
|
||||
let flag = clip.inverted.then(|| fresh(&mut next));
|
||||
if let Some(o) = f.objects.iter_mut().find(|o| o.handle == xd) {
|
||||
o.tags.extend([raw(3, "ACAD_FILTER"), raw(360, fd.hex())]);
|
||||
if let Some(x) = flag {
|
||||
o.tags.extend([raw(3, INVERT_KEY), raw(360, x.hex())]);
|
||||
}
|
||||
}
|
||||
let mut filters = dictionary(fd, xd);
|
||||
filters.tags.extend([raw(3, "SPATIAL"), raw(360, sf.hex())]);
|
||||
f.objects.push(filters);
|
||||
f.objects.push(spatial_filter(sf, fd, &clip, &to_world, &inverse));
|
||||
if let Some(x) = flag {
|
||||
f.objects.push(invert_record(x, xd));
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
@@ -77,11 +77,13 @@ fn clips_round_trip_through_dxf() {
|
||||
let bytes = save(&d, "a.dxf", Version::R2018);
|
||||
let text = String::from_utf8(bytes.clone()).unwrap();
|
||||
assert_eq!(text.matches("SPATIAL_FILTER").count(), 3, "one filter per clipped reference");
|
||||
assert!(text.contains("ACAD_FILTER") && !text.contains("CADCRAFT_XCLIP"));
|
||||
// The inverted triangle is marked on its filter (group 290), as the reference app writes it.
|
||||
// The inverted triangle also carries CADCraft's flag, which a DWG keeps.
|
||||
assert!(text.contains("ACAD_FILTER") && text.matches("CADCRAFT_XCLIP").count() == 1);
|
||||
// The inverted triangle is marked on its filter (group 290, last), as the reference app writes it.
|
||||
let filters: Vec<&str> = text.split("\r\nSPATIAL_FILTER\r\n").skip(1).map(|f| f.split("\r\n 0\r\n").next().unwrap_or("")).collect();
|
||||
assert_eq!(filters.len(), 3);
|
||||
assert!(filters.iter().all(|f| f.contains("\r\n290\r\n")), "every filter says whether it is inverted");
|
||||
assert_eq!(filters.iter().filter(|f| f.contains("\r\n290\r\n1\r\n")).count(), 1);
|
||||
assert_eq!(filters.iter().filter(|f| f.ends_with("\r\n290\r\n1")).count(), 1, "{filters:?}");
|
||||
let first = read(&bytes, "a.dxf").unwrap();
|
||||
same(&clips(&first), &clips(&d));
|
||||
// The model owns the filters now: nothing of them is left as foreign data.
|
||||
|
||||
@@ -309,6 +309,14 @@ fn push_clipped_image(dst: &mut DisplayList, p: &DPrim, img: &DImage, corners: &
|
||||
let (outline, area) = match clip {
|
||||
Clip::Rect(rect) => (vec![rect.min, Vec2::new(rect.max.x, rect.min.y), rect.max, Vec2::new(rect.min.x, rect.max.y)], *rect),
|
||||
Clip::Poly(c) => (c.polygon().to_vec(), c.bounds()),
|
||||
// An image can only be cut to the inside of an outline: under an inverted XCLIP it is
|
||||
// drawn whole, which shows too much rather than losing any of it.
|
||||
Clip::Outside(_) => {
|
||||
let Ok(index) = u32::try_from(dst.images.len()) else { return };
|
||||
dst.images.push(img.clone());
|
||||
push_raw(dst, p, Kind::Image { index }, corners);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let own = Bounds2::from_points(corners.iter().copied());
|
||||
if !own.intersects(&area) {
|
||||
|
||||
@@ -219,6 +219,7 @@ fn an_image_in_a_block_follows_the_block_reference() {
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
clip: None,
|
||||
};
|
||||
let h = d.add(&Space::Model, Common::default(), EntityKind::Insert(ins.clone())).unwrap();
|
||||
d.rasters.insert("map.png", pixels());
|
||||
|
||||
Reference in New Issue
Block a user