mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 20:40:30 -06:00
XCLIP: write an inverted clip as AutoCAD does in a file (no group 290)
AutoCAD 2024 writes no group 290 on a SPATIAL_FILTER in a DXF and discards a file whose filter has one (measured by 2ez4gcx, #2756). An inverted clip's filter now gets an extension dictionary with the ACAD_XREC_ROUNDTRIP XRECORD (ACAD_INVERTEDCLIP_ROUNDTRIP, then ..._COMPARE). Reading takes the ROUNDTRIP list as the boundary while COMPARE matches the filter's vertices, so AutoCAD's keyhole filters read as the clip drawn; 290 is still read when present. The record survives DWG, so CADCraft's own CADCRAFT_XCLIP flag is gone.
This commit is contained in:
+194
-94
@@ -4,11 +4,15 @@
|
||||
//! 10/20 the vertices), whether clipping is on (71), the front and back clip (72/73, 40/41) and
|
||||
//! two 3×4 matrices (group 40, row by row): the inverse of the reference's transform, which takes
|
||||
//! 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.
|
||||
//! An inverted clip ("Invert clip") is marked as the reference app writes it in a file: the
|
||||
//! filter has an extension dictionary whose `ACAD_XREC_ROUNDTRIP` entry is an XRECORD holding
|
||||
//! `102 ACAD_INVERTEDCLIP_ROUNDTRIP` then the boundary (10/20/30), and
|
||||
//! `102 ACAD_INVERTEDCLIP_ROUNDTRIP_COMPARE` then the filter's own vertices. While the filter
|
||||
//! still holds those vertices the clip is inverted, with the first list as its boundary. The
|
||||
//! reference app shows the flag as group 290 on the object but never writes 290 in a DXF file,
|
||||
//! and discards a file whose filter has one; 290 is read when another program wrote it, never
|
||||
//! written. The record goes through DWG as it is.
|
||||
//!
|
||||
//! 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
|
||||
@@ -23,8 +27,10 @@ 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";
|
||||
/// The filter's extension-dictionary entry of the reference app's inverted-clip record.
|
||||
const ROUNDTRIP_KEY: &str = "ACAD_XREC_ROUNDTRIP";
|
||||
const ROUNDTRIP: &str = "ACAD_INVERTEDCLIP_ROUNDTRIP";
|
||||
const COMPARE: &str = "ACAD_INVERTEDCLIP_ROUNDTRIP_COMPARE";
|
||||
/// Handles above this are not handed out (the DXF handle space is 64-bit, keep a margin).
|
||||
const MAX_HANDLE: u64 = 1 << 48;
|
||||
|
||||
@@ -94,9 +100,70 @@ fn matrix_values(m: &Mat3) -> [f64; 12] {
|
||||
[m.a, m.c, 0.0, m.e, m.b, m.d, 0.0, m.f, 0.0, 0.0, 1.0, 0.0]
|
||||
}
|
||||
|
||||
/// The boundary a planar SPATIAL_FILTER holds, in the block's coordinates. `None` for anything
|
||||
/// The `count` vertices (10/20) a SPATIAL_FILTER holds, as written.
|
||||
fn vertices(tags: &[RawTag], count: usize) -> Option<Vec<Vec2>> {
|
||||
let mut points = Vec::with_capacity(count);
|
||||
for w in tags.windows(2) {
|
||||
let [x, y] = w else { continue };
|
||||
if x.code != 10 {
|
||||
continue;
|
||||
}
|
||||
if y.code != 20 || points.len() >= count {
|
||||
return None;
|
||||
}
|
||||
points.push(Vec2::new(x.value.trim().parse().ok()?, y.value.trim().parse().ok()?));
|
||||
}
|
||||
(points.len() == count).then_some(points)
|
||||
}
|
||||
|
||||
/// The handle of an object's own extension dictionary (`102 {ACAD_XDICTIONARY`, 360).
|
||||
fn own_xdict(tags: &[RawTag]) -> Option<Handle> {
|
||||
tags.windows(2)
|
||||
.find(|w| matches!(w, [k, v] if k.code == 102 && k.value.trim().eq_ignore_ascii_case("{ACAD_XDICTIONARY") && v.code == 360))
|
||||
.and_then(|w| w.get(1))
|
||||
.and_then(|v| Handle::parse_hex(&v.value))
|
||||
}
|
||||
|
||||
/// The boundary of an inverted clip as the reference app marks it on `filter` (see above), when
|
||||
/// the filter still holds the vertices the mark was made for.
|
||||
fn roundtrip(f: &Foreign, idx: &Index, filter: &ForeignObject) -> Option<Vec<Vec2>> {
|
||||
let rec = object(f, idx, entry(&object(f, idx, own_xdict(&filter.tags)?)?.tags, ROUNDTRIP_KEY)?)?;
|
||||
if !rec.kind.eq_ignore_ascii_case("XRECORD") {
|
||||
return None;
|
||||
}
|
||||
let (mut boundary, mut compare) = (Vec::new(), Vec::new());
|
||||
// 0 before either list, 1 in the boundary, 2 in the filter's vertices.
|
||||
let mut list = 0;
|
||||
for w in rec.tags.windows(2) {
|
||||
let [x, y] = w else { continue };
|
||||
match x.code {
|
||||
102 => {
|
||||
list = match x.value.trim() {
|
||||
v if v.eq_ignore_ascii_case(ROUNDTRIP) => 1,
|
||||
v if v.eq_ignore_ascii_case(COMPARE) => 2,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
10 if list != 0 && y.code == 20 => {
|
||||
let into = if list == 1 { &mut boundary } else { &mut compare };
|
||||
if into.len() >= MAX_CLIP_VERTICES {
|
||||
return None;
|
||||
}
|
||||
into.push(Vec2::new(x.value.trim().parse().ok()?, y.value.trim().parse().ok()?));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let own = vertices(&filter.tags, usize::try_from(number(&filter.tags, 70)? as i64).ok()?)?;
|
||||
let near = |a: f64, b: f64| (a - b).abs() <= 1e-9 * (1.0 + a.abs().max(b.abs()));
|
||||
let same = own.len() == compare.len() && own.iter().zip(&compare).all(|(p, q)| near(p.x, q.x) && near(p.y, q.y));
|
||||
(same && boundary.len() >= 2).then_some(boundary)
|
||||
}
|
||||
|
||||
/// The boundary a planar SPATIAL_FILTER holds, in the block's coordinates; with `boundary`, the
|
||||
/// clip is inverted and that is its outline (in the filter's coordinates). `None` for anything
|
||||
/// the model can't hold as it is (it then stays foreign).
|
||||
fn parse_filter(o: &ForeignObject) -> Option<ClipBoundary> {
|
||||
fn parse_filter(o: &ForeignObject, boundary: Option<&[Vec2]>) -> Option<ClipBoundary> {
|
||||
let count = usize::try_from(number(&o.tags, 70)? as i64).ok()?;
|
||||
if !(2..=MAX_CLIP_VERTICES).contains(&count) {
|
||||
return None;
|
||||
@@ -124,31 +191,18 @@ fn parse_filter(o: &ForeignObject) -> Option<ClipBoundary> {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut points = Vec::with_capacity(count);
|
||||
for w in o.tags.windows(2) {
|
||||
let [x, y] = w else { continue };
|
||||
if x.code != 10 {
|
||||
continue;
|
||||
}
|
||||
if y.code != 20 || points.len() >= count {
|
||||
return None;
|
||||
}
|
||||
let p = inverse.apply(Vec2::new(x.value.trim().parse().ok()?, y.value.trim().parse().ok()?));
|
||||
if !p.is_finite() {
|
||||
return None;
|
||||
}
|
||||
points.push(p);
|
||||
let own = vertices(&o.tags, count)?;
|
||||
let outline = boundary.unwrap_or(&own);
|
||||
if !(2..=MAX_CLIP_VERTICES).contains(&outline.len()) {
|
||||
return None;
|
||||
}
|
||||
let clip = ClipBoundary { points, enabled: number(&o.tags, 71).unwrap_or(1.0) != 0.0, inverted: number(&o.tags, 290).is_some_and(|v| v != 0.0) };
|
||||
(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, idx: &Index, xd: Handle) -> bool {
|
||||
object(f, idx, xd)
|
||||
.and_then(|o| entry(&o.tags, INVERT_KEY))
|
||||
.and_then(|h| object(f, idx, h))
|
||||
.is_some_and(|o| o.kind.eq_ignore_ascii_case("XRECORD") && number(&o.tags, 70).is_some_and(|v| v != 0.0))
|
||||
let points: Vec<Vec2> = outline.iter().map(|p| inverse.apply(*p)).collect();
|
||||
if !points.iter().all(|p| p.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let inverted = boundary.is_some() || number(&o.tags, 290).is_some_and(|v| v != 0.0);
|
||||
let clip = ClipBoundary { points, enabled: number(&o.tags, 71).unwrap_or(1.0) != 0.0, inverted };
|
||||
cadcraft_render::clip::PolyClip::new(&clip.polygon()).is_some().then_some(clip)
|
||||
}
|
||||
|
||||
/// Put the planar filters of the file read into `d` on their references, taking them out of
|
||||
@@ -163,12 +217,9 @@ pub(crate) fn read(d: &mut Drawing) {
|
||||
.entities
|
||||
.keys()
|
||||
.filter_map(|h| {
|
||||
let (xd, _, o) = chain(&d.foreign, &idx, *h)?;
|
||||
let mut clip = parse_filter(o)?;
|
||||
if number(&o.tags, 290).is_none() {
|
||||
clip.inverted = inverted_record(&d.foreign, &idx, xd);
|
||||
}
|
||||
Some((*h, clip))
|
||||
let (_, _, o) = chain(&d.foreign, &idx, *h)?;
|
||||
let boundary = roundtrip(&d.foreign, &idx, o);
|
||||
Some((*h, parse_filter(o, boundary.as_deref())?))
|
||||
})
|
||||
.collect();
|
||||
if found.is_empty() {
|
||||
@@ -207,7 +258,7 @@ 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,
|
||||
/// CADCraft's inverted flag, and the extension dictionary when nothing else is left in it. The
|
||||
/// and the extension dictionary when nothing else is left in it. The
|
||||
/// objects to remove are added to `gone`; the caller removes them all at once.
|
||||
fn forget(f: &mut Foreign, idx: &Index, h: Handle, all_gone: &mut HashSet<Handle>) {
|
||||
let Some(xd) = f.entities.get(&h).and_then(|x| x.xdict) else { return };
|
||||
@@ -215,19 +266,19 @@ fn forget(f: &mut Foreign, idx: &Index, h: Handle, all_gone: &mut HashSet<Handle
|
||||
let mut empty = false;
|
||||
if let Some(o) = idx.get(&xd).copied().and_then(|i| f.objects.get_mut(i)) {
|
||||
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.
|
||||
// The filter dictionary's entries go with it, and the filter's own extension dictionary.
|
||||
let mut i = 0;
|
||||
while let Some(g) = gone.get(i).copied() {
|
||||
if let Some(o) = object(f, idx, g)
|
||||
&& o.kind.eq_ignore_ascii_case("DICTIONARY")
|
||||
{
|
||||
gone.extend(o.tags.windows(2).filter_map(|w| match w {
|
||||
[k, v] if k.code == 3 && matches!(v.code, 350 | 360) => Handle::parse_hex(&v.value),
|
||||
_ => None,
|
||||
}));
|
||||
if let Some(o) = object(f, idx, g) {
|
||||
if o.kind.eq_ignore_ascii_case("DICTIONARY") {
|
||||
gone.extend(o.tags.windows(2).filter_map(|w| match w {
|
||||
[k, v] if k.code == 3 && matches!(v.code, 350 | 360) => Handle::parse_hex(&v.value),
|
||||
_ => None,
|
||||
}));
|
||||
}
|
||||
gone.extend(own_xdict(&o.tags));
|
||||
}
|
||||
i += 1;
|
||||
if i > MAX_CLIP_VERTICES {
|
||||
@@ -263,17 +314,14 @@ fn dictionary(h: Handle, owner: Handle) -> ForeignObject {
|
||||
}
|
||||
}
|
||||
|
||||
fn spatial_filter(h: Handle, owner: Handle, clip: &ClipBoundary, to_world: &Mat3, inverse: &Mat3) -> ForeignObject {
|
||||
let mut tags = vec![
|
||||
raw(5, h.hex()),
|
||||
raw(102, "{ACAD_REACTORS"),
|
||||
raw(330, owner.hex()),
|
||||
raw(102, "}"),
|
||||
raw(330, owner.hex()),
|
||||
raw(100, "AcDbFilter"),
|
||||
raw(100, "AcDbSpatialFilter"),
|
||||
raw(70, clip.points.len()),
|
||||
];
|
||||
/// The SPATIAL_FILTER of `clip`; `xdict` is its extension dictionary (an inverted clip's).
|
||||
/// No group 290: the reference app discards a file whose filter has one.
|
||||
fn spatial_filter(h: Handle, owner: Handle, clip: &ClipBoundary, to_world: &Mat3, inverse: &Mat3, xdict: Option<Handle>) -> ForeignObject {
|
||||
let mut tags = vec![raw(5, h.hex()), raw(102, "{ACAD_REACTORS"), raw(330, owner.hex()), raw(102, "}")];
|
||||
if let Some(x) = xdict {
|
||||
tags.extend([raw(102, "{ACAD_XDICTIONARY"), raw(360, x.hex()), raw(102, "}")]);
|
||||
}
|
||||
tags.extend([raw(330, owner.hex()), raw(100, "AcDbFilter"), raw(100, "AcDbSpatialFilter"), raw(70, clip.points.len())]);
|
||||
for p in &clip.points {
|
||||
let q = to_world.apply(*p);
|
||||
tags.push(raw(10, q.x));
|
||||
@@ -284,25 +332,21 @@ fn spatial_filter(h: Handle, owner: Handle, clip: &ClipBoundary, to_world: &Mat3
|
||||
for v in matrix_values(inverse).into_iter().chain(matrix_values(&Mat3::IDENTITY)) {
|
||||
tags.push(raw(40, v));
|
||||
}
|
||||
tags.push(raw(290, i32::from(clip.inverted)));
|
||||
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),
|
||||
],
|
||||
/// The reference app's inverted-clip record for a filter whose vertices are `points` (drawing
|
||||
/// coordinates): the boundary and the filter's vertices are the same list.
|
||||
fn roundtrip_record(h: Handle, owner: Handle, points: &[Vec2]) -> ForeignObject {
|
||||
let mut tags = vec![raw(5, h.hex()), raw(102, "{ACAD_REACTORS"), raw(330, owner.hex()), raw(102, "}")];
|
||||
tags.extend([raw(330, owner.hex()), raw(100, "AcDbXrecord"), raw(280, 1)]);
|
||||
for name in [ROUNDTRIP, COMPARE] {
|
||||
tags.push(raw(102, name));
|
||||
for p in points {
|
||||
tags.extend([raw(10, p.x), raw(20, p.y), raw(30, 0.0)]);
|
||||
}
|
||||
}
|
||||
ForeignObject { handle: h, kind: "XRECORD".into(), tags }
|
||||
}
|
||||
|
||||
/// A copy of `d` whose foreign data holds the SPATIAL_FILTER of every clipped reference (any
|
||||
@@ -336,7 +380,7 @@ pub(crate) fn prepare(d: &Drawing) -> Option<Drawing> {
|
||||
let mut gone = HashSet::new();
|
||||
for (insert, clip, to_world) in clips {
|
||||
let Some(inverse) = to_world.inverse() else { continue };
|
||||
if next.saturating_add(5) >= MAX_HANDLE {
|
||||
if next.saturating_add(6) >= MAX_HANDLE {
|
||||
break;
|
||||
}
|
||||
// A filter kept from the file for this reference is replaced.
|
||||
@@ -352,12 +396,10 @@ 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));
|
||||
// An inverted clip's filter has an extension dictionary and the record in it.
|
||||
let marks = clip.inverted.then(|| (fresh(&mut next), fresh(&mut next)));
|
||||
if let Some(o) = idx.get(&xd).copied().and_then(|i| f.objects.get_mut(i)) {
|
||||
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())]);
|
||||
@@ -366,9 +408,13 @@ pub(crate) fn prepare(d: &Drawing) -> Option<Drawing> {
|
||||
f.objects.push(o);
|
||||
};
|
||||
add(filters);
|
||||
add(spatial_filter(sf, fd, &clip, &to_world, &inverse));
|
||||
if let Some(x) = flag {
|
||||
add(invert_record(x, xd));
|
||||
add(spatial_filter(sf, fd, &clip, &to_world, &inverse, marks.map(|(x, _)| x)));
|
||||
if let Some((x, r)) = marks {
|
||||
let mut own = dictionary(x, sf);
|
||||
own.tags.extend([raw(3, ROUNDTRIP_KEY), raw(360, r.hex())]);
|
||||
add(own);
|
||||
let world: Vec<Vec2> = clip.points.iter().map(|p| to_world.apply(*p)).collect();
|
||||
add(roundtrip_record(r, x, &world));
|
||||
}
|
||||
}
|
||||
f.objects.retain(|o| !gone.contains(&o.handle));
|
||||
@@ -395,11 +441,11 @@ mod tests {
|
||||
#[test]
|
||||
fn hostile_filters_stay_foreign() {
|
||||
let o = |tags: Vec<RawTag>| ForeignObject { handle: Handle(1), kind: "SPATIAL_FILTER".into(), tags };
|
||||
assert!(parse_filter(&o(vec![raw(70, 1_000_000_000)])).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 3), raw(10, 0), raw(20, 0)])).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 2), raw(10, "nan"), raw(20, 0), raw(10, 1), raw(20, 1)])).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 2), raw(10, 0), raw(20, 0), raw(10, 1), raw(20, 1), raw(72, 1)])).is_none());
|
||||
let ok = parse_filter(&o(vec![raw(70, 2), raw(10, 0), raw(20, 0), raw(10, 1), raw(20, 1)])).unwrap();
|
||||
assert!(parse_filter(&o(vec![raw(70, 1_000_000_000)]), None).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 3), raw(10, 0), raw(20, 0)]), None).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 2), raw(10, "nan"), raw(20, 0), raw(10, 1), raw(20, 1)]), None).is_none());
|
||||
assert!(parse_filter(&o(vec![raw(70, 2), raw(10, 0), raw(20, 0), raw(10, 1), raw(20, 1), raw(72, 1)]), None).is_none());
|
||||
let ok = parse_filter(&o(vec![raw(70, 2), raw(10, 0), raw(20, 0), raw(10, 1), raw(20, 1)]), None).unwrap();
|
||||
assert_eq!((ok.points.len(), ok.inverted), (2, false));
|
||||
}
|
||||
|
||||
@@ -418,15 +464,69 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_290_is_the_inverted_flag() {
|
||||
fn group_290_is_read_but_never_written() {
|
||||
let tags = |invert: &str| vec![raw(70, 2), raw(10, 0), raw(20, 0), raw(10, 1), raw(20, 1), raw(290, invert)];
|
||||
let o = |invert| ForeignObject { handle: Handle(1), kind: "SPATIAL_FILTER".into(), tags: tags(invert) };
|
||||
assert!(parse_filter(&o("1")).unwrap().inverted);
|
||||
assert!(!parse_filter(&o("0")).unwrap().inverted);
|
||||
assert!(!parse_filter(&o("junk")).unwrap().inverted);
|
||||
assert!(parse_filter(&o("1"), None).unwrap().inverted);
|
||||
assert!(!parse_filter(&o("0"), None).unwrap().inverted);
|
||||
assert!(!parse_filter(&o("junk"), None).unwrap().inverted);
|
||||
// The reference app discards a DXF whose filter has 290.
|
||||
let clip = ClipBoundary { inverted: true, ..ClipBoundary::new(vec![Vec2::ZERO, Vec2::new(1.0, 1.0)]) };
|
||||
let written = spatial_filter(Handle(2), Handle(1), &clip, &Mat3::IDENTITY, &Mat3::IDENTITY);
|
||||
assert!(written.tags.iter().any(|t| t.code == 290 && t.value == "1"));
|
||||
assert!(parse_filter(&written).unwrap().inverted);
|
||||
let written = spatial_filter(Handle(2), Handle(1), &clip, &Mat3::IDENTITY, &Mat3::IDENTITY, Some(Handle(3)));
|
||||
assert!(!written.tags.iter().any(|t| t.code == 290));
|
||||
assert_eq!(own_xdict(&written.tags), Some(Handle(3)));
|
||||
}
|
||||
|
||||
/// The filter, its extension dictionary and the reference app's record, as one foreign set.
|
||||
fn marked(filter_points: &[(f64, f64)], boundary: &[(f64, f64)], compare: &[(f64, f64)]) -> (Foreign, ForeignObject) {
|
||||
let mut tags = vec![raw(5, "10"), raw(102, "{ACAD_XDICTIONARY"), raw(360, "11"), raw(102, "}"), raw(70, filter_points.len())];
|
||||
for (x, y) in filter_points {
|
||||
tags.extend([raw(10, x), raw(20, y)]);
|
||||
}
|
||||
let filter = ForeignObject { handle: Handle(0x10), kind: "SPATIAL_FILTER".into(), tags };
|
||||
let mut f = Foreign::default();
|
||||
let mut dict = dictionary(Handle(0x11), Handle(0x10));
|
||||
dict.tags.extend([raw(3, ROUNDTRIP_KEY), raw(360, "12")]);
|
||||
f.objects.push(dict);
|
||||
let mut rec = vec![raw(100, "AcDbXrecord"), raw(280, 1)];
|
||||
for (name, list) in [(ROUNDTRIP, boundary), (COMPARE, compare)] {
|
||||
rec.push(raw(102, name));
|
||||
for (x, y) in list {
|
||||
rec.extend([raw(10, x), raw(20, y), raw(30, 0)]);
|
||||
}
|
||||
}
|
||||
f.objects.push(ForeignObject { handle: Handle(0x12), kind: "XRECORD".into(), tags: rec });
|
||||
f.objects.push(filter.clone());
|
||||
(f, filter)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reference_apps_record_marks_an_inverted_clip() {
|
||||
// As the reference app writes an inverted rectangle: a keyhole filter, the rectangle kept.
|
||||
let keyhole = [
|
||||
(0.0, 0.0),
|
||||
(100.0, 0.0),
|
||||
(100.0, 50.0),
|
||||
(0.0, 50.0),
|
||||
(0.0, 10.0),
|
||||
(20.0, 10.0),
|
||||
(20.0, 40.0),
|
||||
(80.0, 40.0),
|
||||
(80.0, 10.0),
|
||||
(20.0, 10.0),
|
||||
(0.0, 10.0),
|
||||
];
|
||||
let (f, filter) = marked(&keyhole, &[(20.0, 10.0), (80.0, 40.0)], &keyhole);
|
||||
let boundary = roundtrip(&f, &index(&f), &filter).unwrap();
|
||||
let clip = parse_filter(&filter, Some(&boundary)).unwrap();
|
||||
assert!(clip.inverted);
|
||||
assert_eq!(clip.points, vec![Vec2::new(20.0, 10.0), Vec2::new(80.0, 40.0)]);
|
||||
// The simpler form: the filter keeps the boundary itself.
|
||||
let rect = [(20.0, 10.0), (80.0, 40.0)];
|
||||
let (f, filter) = marked(&rect, &rect, &rect);
|
||||
assert!(roundtrip(&f, &index(&f), &filter).is_some());
|
||||
// A filter changed since the record was made (the clip edited elsewhere) isn't inverted.
|
||||
let (f, filter) = marked(&[(0.0, 0.0), (5.0, 5.0)], &rect, &rect);
|
||||
assert!(roundtrip(&f, &index(&f), &filter).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,15 @@ 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");
|
||||
// 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.
|
||||
assert!(text.contains("ACAD_FILTER"));
|
||||
// No filter has group 290: the reference app discards a DXF whose filter has one. The
|
||||
// inverted triangle's filter has its record instead, in the filter's extension dictionary.
|
||||
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.ends_with("\r\n290\r\n1")).count(), 1, "{filters:?}");
|
||||
assert!(!filters.iter().any(|f| f.contains("\r\n290\r\n")), "{filters:?}");
|
||||
assert_eq!(filters.iter().filter(|f| f.contains("{ACAD_XDICTIONARY")).count(), 1, "{filters:?}");
|
||||
assert_eq!(text.matches("ACAD_XREC_ROUNDTRIP").count(), 1);
|
||||
assert!(text.contains("ACAD_INVERTEDCLIP_ROUNDTRIP\r\n") && text.contains("ACAD_INVERTEDCLIP_ROUNDTRIP_COMPARE\r\n"));
|
||||
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.
|
||||
@@ -95,6 +97,7 @@ fn clips_round_trip_through_dxf() {
|
||||
assert!(!back.foreign.objects.iter().any(|o| o.kind == "SPATIAL_FILTER"), "{}", v.year());
|
||||
let again = String::from_utf8(save(&back, "a.dxf", v)).unwrap();
|
||||
assert_eq!(again.matches("SPATIAL_FILTER").count(), 3, "no duplicate filters on resave");
|
||||
assert_eq!(again.matches("ACAD_XREC_ROUNDTRIP").count(), 1, "no duplicate records on resave");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +127,7 @@ fn deleting_a_clip_read_from_a_file_removes_its_filter() {
|
||||
.unwrap();
|
||||
}
|
||||
let text = String::from_utf8(save(&d, "a.dxf", Version::R2018)).unwrap();
|
||||
assert!(!text.contains("SPATIAL_FILTER") && !text.contains("CADCRAFT_XCLIP"));
|
||||
assert!(!text.contains("SPATIAL_FILTER") && !text.contains("ACAD_XREC_ROUNDTRIP"));
|
||||
assert!(clips(&read(text.as_bytes(), "a.dxf").unwrap()).iter().all(Option::is_none));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user