* Render tests: drop the always-true assert in degenerate_input `assert!(... || true)` trips clippy's overly_complex_bool_expr on current stable (1.96), which fails `cargo xtask ci` at the clippy step. The check only proves that NaN input does not panic, so call the function and discard the result instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * DXF: keep layer transparency and description through save and reopen (#96, #97) The LAYER writer omitted both properties and the reader left them at their defaults, so a drawing reopened with every layer opaque and every description empty. Both are xdata on the LAYER record, as AutoCAD writes them: - transparency: AcCmTransparency, 1071 = 0x02000000 | alpha, with alpha = (100 - percent) * 255 / 100 (40 % is alpha 153); values that aren't "by alpha" read as opaque - description: AcAecLayerStandard, two 1000 strings (the layer standard's name, unused, then the description), cut to the 255 bytes a 1000 group may hold Both applications are registered in the APPID table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b7f83a6687
commit
0cdde077ef
|
|
@ -211,6 +211,36 @@ pub(crate) fn arrow_block_name(name: &str) -> Option<(String, Arrowhead)> {
|
|||
Some((if keep { n.to_string() } else { std_name }, kind))
|
||||
}
|
||||
|
||||
/// Applications whose xdata carries layer properties: transparency and description.
|
||||
pub(crate) const LAYER_TRANSPARENCY_APP: &str = "AcCmTransparency";
|
||||
pub(crate) const LAYER_DESCRIPTION_APP: &str = "AcAecLayerStandard";
|
||||
|
||||
/// A transparency percentage (0..=90) as an `AcCmTransparency` value: the alpha with the
|
||||
/// "by alpha" flag (0x02 in the top byte).
|
||||
pub(crate) fn transparency_to_dxf(percent: u8) -> i64 {
|
||||
let t = u32::from(percent.min(90));
|
||||
i64::from(0x0200_0000 | ((100 - t) * 255 / 100))
|
||||
}
|
||||
|
||||
/// The percentage of an `AcCmTransparency` value; `None` unless it is "by alpha".
|
||||
pub(crate) fn transparency_from_dxf(v: i64) -> Option<u8> {
|
||||
if (v >> 24) & 0xff != 2 {
|
||||
return None;
|
||||
}
|
||||
let alpha = u32::try_from(v & 0xff).ok()?;
|
||||
let opaque = (alpha * 100 + 127) / 255;
|
||||
u8::try_from(100u32.saturating_sub(opaque).min(90)).ok()
|
||||
}
|
||||
|
||||
/// An xdata string cut to the 255 bytes a `1000` group may hold, at a character boundary.
|
||||
pub(crate) fn xdata_str(s: &str) -> &str {
|
||||
let mut end = s.len().min(255);
|
||||
while !s.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
s.get(..end).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The extended data of one application: the groups after its `1001` up to the next `1001`.
|
||||
pub(crate) fn xdata<'a>(tags: &'a [Tag], app: &str) -> &'a [Tag] {
|
||||
let Some(start) = tags.iter().position(|t| t.code == 1001 && t.str().trim().eq_ignore_ascii_case(app)) else { return &[] };
|
||||
|
|
|
|||
|
|
@ -726,6 +726,13 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
|||
if let Some(tc) = t.i(420) {
|
||||
l.color = Color::True(Rgb::from_u32(tc as u32));
|
||||
}
|
||||
let alpha = crate::dxf_ext::xdata(&tg, crate::dxf_ext::LAYER_TRANSPARENCY_APP).iter().find(|x| x.code == 1071);
|
||||
if let Some(pct) = alpha.and_then(|x| crate::dxf_ext::transparency_from_dxf(x.i64())) {
|
||||
l.transparency = pct;
|
||||
}
|
||||
if let Some(desc) = crate::dxf_ext::xdata(&tg, crate::dxf_ext::LAYER_DESCRIPTION_APP).iter().filter(|x| x.code == 1000).nth(1) {
|
||||
l.description = desc.str();
|
||||
}
|
||||
match d.layer_mut(&name) {
|
||||
Some(x) => *x = l,
|
||||
None => d.layers.push(l),
|
||||
|
|
|
|||
|
|
@ -1181,6 +1181,16 @@ pub fn write(d: &Drawing) -> String {
|
|||
w.i(290, 0);
|
||||
}
|
||||
w.i(370, i64::from(l.lineweight.to_dxf()));
|
||||
if l.transparency > 0 {
|
||||
w.s(1001, dxf_ext::LAYER_TRANSPARENCY_APP);
|
||||
w.i(1071, dxf_ext::transparency_to_dxf(l.transparency));
|
||||
}
|
||||
if !l.description.is_empty() {
|
||||
// The first string is the layer standard's name, unused here.
|
||||
w.s(1001, dxf_ext::LAYER_DESCRIPTION_APP);
|
||||
w.s(1000, "");
|
||||
w.s(1000, dxf_ext::xdata_str(&l.description));
|
||||
}
|
||||
}
|
||||
w.s(0, "ENDTAB");
|
||||
// STYLE
|
||||
|
|
@ -1207,7 +1217,7 @@ pub fn write(d: &Drawing) -> String {
|
|||
table_head(&mut w, name, 0);
|
||||
w.s(0, "ENDTAB");
|
||||
}
|
||||
let apps = ["ACAD", dxf_ext::APP, "AcadAnnotative"];
|
||||
let apps = ["ACAD", dxf_ext::APP, "AcadAnnotative", dxf_ext::LAYER_TRANSPARENCY_APP, dxf_ext::LAYER_DESCRIPTION_APP];
|
||||
let th = table_head(&mut w, "APPID", apps.len());
|
||||
for app in apps {
|
||||
record_head(&mut w, "APPID", &th, "AcDbRegAppTableRecord");
|
||||
|
|
|
|||
|
|
@ -207,6 +207,43 @@ fn second_roundtrip_is_stable() {
|
|||
assert_eq!(d2.model.len(), d3.model.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_transparency_and_description_roundtrip() {
|
||||
// Issues #96 and #97: both are LAYER xdata (AcCmTransparency, AcAecLayerStandard).
|
||||
let layers = [("T0", 0, ""), ("T1", 1, "One"), ("T40", 40, "Existing brick walls to remain"), ("T90", 90, "Ångström – ünïcode")];
|
||||
let mut d = Drawing::new_imperial();
|
||||
for (name, transparency, description) in layers {
|
||||
d.layers.push(Layer {
|
||||
name: name.into(),
|
||||
transparency,
|
||||
description: description.into(),
|
||||
color: Color::Index(1),
|
||||
plot: false,
|
||||
..Layer::default()
|
||||
});
|
||||
}
|
||||
let long = "é".repeat(200);
|
||||
d.layers.push(Layer { name: "Long".into(), description: long.clone(), ..Layer::default() });
|
||||
let text = write_dxf(&d);
|
||||
let back = read_dxf(text.as_bytes()).unwrap();
|
||||
for (name, transparency, description) in layers {
|
||||
let l = back.layer(name).unwrap();
|
||||
assert_eq!((l.transparency, l.description.as_str(), l.color, l.plot), (transparency, description, Color::Index(1), false), "{name}");
|
||||
}
|
||||
// An xdata string holds at most 255 bytes; the cut falls on a character boundary.
|
||||
assert_eq!(back.layer("Long").unwrap().description, long[..254]);
|
||||
// AutoCAD's encoding: the alpha with the "by alpha" flag, under registered applications.
|
||||
assert!(text.contains("AcCmTransparency\r\n1071\r\n33554585\r\n"), "40% is alpha 153");
|
||||
assert_eq!(text.matches("AcDbRegAppTableRecord\r\n 2\r\nAcCmTransparency").count(), 1);
|
||||
assert!(text.contains("AcDbRegAppTableRecord\r\n 2\r\nAcAecLayerStandard"));
|
||||
// Values that aren't "by alpha" (ByLayer, ByBlock, junk) leave the layer opaque.
|
||||
for v in [0, 0x0100_0000, -1, i64::MAX] {
|
||||
assert_eq!(crate::dxf_ext::transparency_from_dxf(v), None, "{v:#x}");
|
||||
}
|
||||
assert_eq!(crate::dxf_ext::transparency_from_dxf(0x0200_00ff), Some(0));
|
||||
assert_eq!(crate::dxf_ext::transparency_from_dxf(0x0200_0000), Some(90));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_r12_style_polyline_and_paper_flag() {
|
||||
let text = "0\nSECTION\n2\nENTITIES\n0\nPOLYLINE\n8\n0\n66\n1\n70\n1\n0\nVERTEX\n8\n0\n10\n0\n20\n0\n0\nVERTEX\n8\n0\n10\n5\n20\n0\n42\n1\n0\nVERTEX\n8\n0\n10\n5\n20\n5\n0\nSEQEND\n0\nLINE\n67\n1\n8\n0\n10\n0\n20\n0\n11\n1\n21\n1\n0\nENDSEC\n0\nEOF\n";
|
||||
|
|
|
|||
Loading…
Reference in New Issue