mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
* Write Wipeout entities to DXF so saving keeps them (#79) * Wipeout: clip vertices are in image pixel space (origin top-left, y down) Write and read the boundary with y flipped, so other programs show the same shape and wipeouts from other programs are no longer mirrored vertically. Tested with an asymmetric boundary. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Wipeout: collapse nested if (clippy) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
3 files changed
+79
-3
No files matched your search
@@ -313,7 +313,8 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
let o = t.p(10);
|
||||
let u = t.p(11);
|
||||
let v = t.p(12);
|
||||
let pts: Vec<Vec2> = t.pts(14).into_iter().map(|q| o.xy() + u.xy() * (q.x + 0.5) + v.xy() * (q.y + 0.5)).collect();
|
||||
// Clip vertices are in pixel space: origin at the image's top-left corner, y pointing down.
|
||||
let pts: Vec<Vec2> = t.pts(14).into_iter().map(|q| o.xy() + u.xy() * (q.x + 0.5) + v.xy() * (0.5 - q.y)).collect();
|
||||
EntityKind::Wipeout(Wipeout { boundary: pts })
|
||||
}
|
||||
"IMAGE" => {
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::HashMap;
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use cadcraft_geom::{Bounds2, Vec2, Vec3};
|
||||
|
||||
use crate::dxf_ext::{self, DimVal, K};
|
||||
|
||||
@@ -797,7 +797,40 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
||||
w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers));
|
||||
}
|
||||
}
|
||||
// Not yet written: images, wipeouts, tables, multileaders, unknown objects.
|
||||
EntityKind::Wipeout(wo) => {
|
||||
// A 1x1 image spanning the boundary's extents. Clip vertices are in pixel space, whose origin is
|
||||
// the image's top-left corner with y pointing down: the reader maps each vertex back through
|
||||
// insert + u * (x + 0.5) + v * (0.5 - y).
|
||||
// Polygonal clip boundaries are closed, so the first vertex is repeated when needed.
|
||||
let b = Bounds2::from_points(wo.boundary.iter().copied());
|
||||
let o = if b.is_empty() { Vec2::ZERO } else { b.min };
|
||||
let (sx, sy) = (if b.width() > 0.0 { b.width() } else { 1.0 }, if b.height() > 0.0 { b.height() } else { 1.0 });
|
||||
w.s(0, "WIPEOUT");
|
||||
common(w, e, owner, paper, "AcDbWipeout");
|
||||
w.i(90, 0);
|
||||
w.p(10, Vec3::new(o.x, o.y, 0.0));
|
||||
w.p(11, Vec3::new(sx, 0.0, 0.0));
|
||||
w.p(12, Vec3::new(0.0, sy, 0.0));
|
||||
w.p2(13, Vec2::new(1.0, 1.0));
|
||||
// Display flags 7 = show image, show unaligned, use clipping boundary; clipping on; default brightness/contrast/fade.
|
||||
w.i(70, 7);
|
||||
w.i(280, 1);
|
||||
w.i(281, 50);
|
||||
w.i(282, 50);
|
||||
w.i(283, 0);
|
||||
w.i(71, 2);
|
||||
let mut pts = wo.boundary.clone();
|
||||
if let (Some(first), Some(last)) = (pts.first().copied(), pts.last().copied())
|
||||
&& first != last
|
||||
{
|
||||
pts.push(first);
|
||||
}
|
||||
w.i(91, pts.len() as i64);
|
||||
for q in pts {
|
||||
w.p2(14, Vec2::new((q.x - o.x) / sx - 0.5, 0.5 - (q.y - o.y) / sy));
|
||||
}
|
||||
}
|
||||
// Not yet written: images, tables, multileaders, unknown objects.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1177,6 +1210,9 @@ pub fn write(d: &Drawing) -> String {
|
||||
if !cx.assoc.is_empty() {
|
||||
classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false));
|
||||
}
|
||||
if every.iter().any(|e| matches!(e.kind, EntityKind::Wipeout(_))) {
|
||||
classes.push(("WIPEOUT", "AcDbWipeout", 127, true));
|
||||
}
|
||||
for (dxf_name, cpp, proxy, is_entity) in classes {
|
||||
w.s(0, "CLASS");
|
||||
w.s(1, dxf_name);
|
||||
|
||||
@@ -1285,6 +1285,32 @@ fn dimension_text_rotation_roundtrips() {
|
||||
assert!(rot.is_some_and(|r| (r - dm.text_rotation).abs() < 1e-9), "block text rotation {rot:?}");
|
||||
}
|
||||
|
||||
/// Wipeout clip vertices are in image pixel space (origin top-left, y down): an asymmetric
|
||||
/// boundary must keep its orientation, both through our reader and in the file itself.
|
||||
#[test]
|
||||
fn wipeout_boundary_keeps_its_orientation() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
// A triangle with its apex at the top.
|
||||
let boundary = vec![Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(0.0, 4.0)];
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Wipeout(Wipeout { boundary: boundary.clone() })).unwrap();
|
||||
let text = write_dxf(&d);
|
||||
// The apex (0, 4) is at the top-left corner of the image: pixel (-0.5, -0.5).
|
||||
let pts: Vec<(f64, f64)> = {
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||||
let mut v = Vec::new();
|
||||
for i in 0..lines.len().saturating_sub(3) {
|
||||
if lines[i] == "14" && lines[i + 2] == "24" {
|
||||
v.push((lines[i + 1].parse().unwrap(), lines[i + 3].parse().unwrap()));
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
assert!(pts.iter().any(|&(x, y)| (x + 0.5).abs() < 1e-9 && (y + 0.5).abs() < 1e-9), "{pts:?}");
|
||||
let back = roundtrip(&d);
|
||||
let got = first(&back, |k| if let EntityKind::Wipeout(w) = k { Some(w.boundary.clone()) } else { None });
|
||||
assert_eq!(&got[..3], &boundary[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_length_dimension_keeps_its_kind_and_centre_on_roundtrip() {
|
||||
let mut d = Drawing::new_metric();
|
||||
@@ -1475,3 +1501,16 @@ fn long_gradient_name_is_cut_only_in_xdata() {
|
||||
let back = read_dxf(text.as_bytes()).unwrap();
|
||||
assert_eq!(hatches(&back).into_iter().next().and_then(|h| h.gradient).unwrap().name, name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wipeout_survives_dxf_roundtrip() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let boundary = vec![Vec2::new(2.0, 3.0), Vec2::new(12.0, 3.0), Vec2::new(12.0, 13.0), Vec2::new(2.0, 13.0)];
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Wipeout(Wipeout { boundary: boundary.clone() })).unwrap();
|
||||
let back = roundtrip(&d);
|
||||
let got = first(&back, |k| if let EntityKind::Wipeout(w) = k { Some(w.boundary.clone()) } else { None });
|
||||
// The DXF polygon is closed, so the first vertex is repeated at the end.
|
||||
assert_eq!(got.len(), 5);
|
||||
assert_eq!(&got[..4], &boundary[..]);
|
||||
assert_eq!(got[4], boundary[0]);
|
||||
}
|
||||
Reference in new issue
Block a user