Entity transparency: set it, report it, round-trip DXF group 440 (#174)
properties.set documented `transparency` but never read it, nothing reported it, and DXF dropped group 440. Fixes #132. - properties.set takes "ByLayer", "ByBlock" or a percentage 0..90 (a number or "50"/"50%"); anything else (out of range, NaN, junk) is an error and leaves the selection unchanged. - properties and drawing.inspect / drawing.entities report it in the same form. - Properties palette: the Transparency row is a menu (ByLayer, ByBlock, a 0..90 percentage) that acts through properties.set. - DXF: entities write 440 unless ByLayer (ByBlock 0x01000000, a percentage its alpha | 0x02000000, the alpha mapping layers use); the reader parses it back, any other value reads as ByLayer. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0cdde077ef
commit
3fe93806b7
|
|
@ -70,6 +70,17 @@ pub enum Transparency {
|
|||
Percent(u8),
|
||||
}
|
||||
|
||||
impl Transparency {
|
||||
/// "ByLayer", "ByBlock" or the percentage ("50").
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Transparency::ByLayer => "ByLayer".into(),
|
||||
Transparency::ByBlock => "ByBlock".into(),
|
||||
Transparency::Percent(p) => p.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties every entity has.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ fn run_status(s: &mut Session, _p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
fn entity_summary(e: &cadcraft_doc::Entity) -> Value {
|
||||
json!({ "handle": e.handle.hex(), "type": e.kind.type_name(), "layer": e.common.layer, "color": e.common.color.name(), "geometry": e.kind })
|
||||
json!({ "handle": e.handle.hex(), "type": e.kind.type_name(), "layer": e.common.layer, "color": e.common.color.name(), "transparency": super::props::transparency_value(e.common.transparency), "geometry": e.kind })
|
||||
}
|
||||
|
||||
fn run_inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Properties: per-object property edits, current properties, MATCHPROP, linetypes, units.
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{EntityKind, Lineweight};
|
||||
use cadcraft_doc::{EntityKind, Lineweight, Transparency};
|
||||
use cadcraft_geom::Vec3;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ use crate::{Result, Session};
|
|||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("properties", "Properties", run_get).menu(&["Modify", "Properties"]).alias(&["pr", "props", "ch"]).params("{handles?} → properties of the selection").noundo(),
|
||||
CommandSpec::new("properties.set", "Set Properties", run_set).params("{handles?, layer?, color?, linetype?, lineweight?, ltscale?, transparency?, visible?, <geometry fields: radius, center, start, end, text, height, rotation…>}"),
|
||||
CommandSpec::new("properties.set", "Set Properties", run_set).params("{handles?, layer?, color?, linetype?, lineweight?, ltscale?, transparency?: \"ByLayer\" | \"ByBlock\" | 0..90, visible?, <geometry fields: radius, center, start, end, text, height, rotation…>}"),
|
||||
CommandSpec::new("matchprop", "Match Properties", run_matchprop).menu(&["Modify", "Match Properties"]).alias(&["ma", "painter"]).params("{source, targets: [hex]}"),
|
||||
CommandSpec::new("color", "Color...", run_color).menu(&["Format", "Color..."]).alias(&["col", "colour"]).params("{color: \"ByLayer\" | \"red\" | 1..255 | \"r,g,b\"}"),
|
||||
CommandSpec::new("linetype", "Linetype...", run_linetype).menu(&["Format", "Linetype..."]).alias(&["lt", "ltype"]).params("{current?: name, load?: name | \"*\"}"),
|
||||
|
|
@ -60,6 +60,7 @@ pub(super) fn entity_props(d: &cadcraft_doc::Drawing, e: &cadcraft_doc::Entity)
|
|||
"linetype": e.common.linetype,
|
||||
"lineweight": e.common.lineweight.name(),
|
||||
"ltscale": e.common.ltscale,
|
||||
"transparency": transparency_value(e.common.transparency),
|
||||
"visible": e.common.visible,
|
||||
});
|
||||
let geo = serde_json::to_value(&e.kind).unwrap_or(Value::Null);
|
||||
|
|
@ -121,6 +122,25 @@ fn parse_color(v: &Value) -> Option<Color> {
|
|||
v.as_str().and_then(Color::parse).or_else(|| v.as_i64().and_then(|i| i16::try_from(i).ok()).map(Color::from_aci))
|
||||
}
|
||||
|
||||
/// A transparency as JSON, in the form `properties.set` takes: "ByLayer", "ByBlock" or a percentage.
|
||||
pub(super) fn transparency_value(t: Transparency) -> Value {
|
||||
match t {
|
||||
Transparency::Percent(p) => json!(p),
|
||||
other => json!(other.name()),
|
||||
}
|
||||
}
|
||||
|
||||
/// "ByLayer", "ByBlock" or a percentage 0..=90 (a number, or a string such as "50" or "50%").
|
||||
fn parse_transparency(v: &Value) -> Option<Transparency> {
|
||||
let pct = match v.as_str() {
|
||||
Some(s) if s.trim().eq_ignore_ascii_case("bylayer") => return Some(Transparency::ByLayer),
|
||||
Some(s) if s.trim().eq_ignore_ascii_case("byblock") => return Some(Transparency::ByBlock),
|
||||
Some(s) => s.trim().trim_end_matches('%').trim_end().parse::<f64>().ok()?,
|
||||
None => v.as_f64()?,
|
||||
};
|
||||
(pct.is_finite() && (0.0..=90.0).contains(&pct)).then(|| Transparency::Percent(pct.round() as u8))
|
||||
}
|
||||
|
||||
fn set_xy(p: &mut Vec3, v: &Value) {
|
||||
if let Some(q) = point_value(v) {
|
||||
p.x = q.x;
|
||||
|
|
@ -134,6 +154,10 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
return Err(bad("properties.set", "nothing selected"));
|
||||
}
|
||||
let color = p.get("color").map(|c| parse_color(c).ok_or_else(|| bad("properties.set", "bad colour"))).transpose()?;
|
||||
let transparency = p
|
||||
.get("transparency")
|
||||
.map(|v| parse_transparency(v).ok_or_else(|| bad("properties.set", "transparency must be ByLayer, ByBlock or a percentage from 0 to 90")))
|
||||
.transpose()?;
|
||||
let lw = p.get("lineweight").map(|v| match v.as_str().map(str::to_ascii_lowercase).as_deref() {
|
||||
Some("bylayer") => Lineweight::ByLayer,
|
||||
Some("byblock") => Lineweight::ByBlock,
|
||||
|
|
@ -165,6 +189,9 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(x) = p.get("ltscale").and_then(Value::as_f64).filter(|x| *x > 0.0) {
|
||||
e.common.ltscale = x;
|
||||
}
|
||||
if let Some(t) = transparency {
|
||||
e.common.transparency = t;
|
||||
}
|
||||
if let Some(v) = p.get("visible").and_then(Value::as_bool) {
|
||||
e.common.visible = v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,39 @@ fn layers_and_properties() {
|
|||
assert!(s.execute("layer.new", &json!({"name": "bad/name"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_set_transparency() {
|
||||
use cadcraft_doc::Transparency;
|
||||
// Issue #132: `properties.set` used to ignore `transparency`.
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
||||
let h = r["handles"][0].as_str().unwrap().to_string();
|
||||
let hh = cadcraft_doc::Handle::parse_hex(&h).unwrap();
|
||||
let tr = |s: &Session| s.doc().unwrap().entity(hh).unwrap().common.transparency;
|
||||
for (v, want) in [
|
||||
(json!(50), Transparency::Percent(50)),
|
||||
(json!("ByBlock"), Transparency::ByBlock),
|
||||
(json!("25%"), Transparency::Percent(25)),
|
||||
(json!(0), Transparency::Percent(0)),
|
||||
(json!("bylayer"), Transparency::ByLayer),
|
||||
(json!(90), Transparency::Percent(90)),
|
||||
] {
|
||||
s.execute("properties.set", &json!({"handles": [h], "transparency": v})).unwrap();
|
||||
assert_eq!(tr(&s), want, "{v}");
|
||||
}
|
||||
// Out of range or not a transparency: an error, and the entity is unchanged.
|
||||
for v in [json!(91), json!(-1), json!("NaN"), json!("inf"), json!("half"), json!(null), json!([50])] {
|
||||
assert!(s.execute("properties.set", &json!({"handles": [h], "transparency": v})).is_err(), "{v}");
|
||||
assert_eq!(tr(&s), Transparency::Percent(90), "{v}");
|
||||
}
|
||||
// `properties` and `drawing.inspect` report it in the form `properties.set` takes.
|
||||
let p = s.execute("properties", &json!({"handles": [h]})).unwrap();
|
||||
assert_eq!(p["objects"][0]["transparency"], json!(90));
|
||||
s.execute("properties.set", &json!({"handles": [h], "transparency": "ByBlock"})).unwrap();
|
||||
let i = s.execute("drawing.inspect", &json!({"entities": true})).unwrap();
|
||||
assert_eq!(i["entities"][0]["transparency"], json!("ByBlock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_extents_fits_drawing() {
|
||||
let mut s = Session::new();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ impl T<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// An entity's group 440: `0x01` in the top byte is ByBlock, `0x02` a fixed alpha (mapped to a
|
||||
/// percentage as for layers), anything else ByLayer.
|
||||
fn transparency_440(v: i64) -> Transparency {
|
||||
match (v >> 24) & 0xff {
|
||||
1 => Transparency::ByBlock,
|
||||
2 => {
|
||||
let alpha = u32::try_from(v & 0xff).unwrap_or(255);
|
||||
let opaque = (alpha * 100 + 127) / 255;
|
||||
Transparency::Percent(u8::try_from(100u32.saturating_sub(opaque).min(90)).unwrap_or(90))
|
||||
}
|
||||
_ => Transparency::ByLayer,
|
||||
}
|
||||
}
|
||||
|
||||
fn common(t: &T) -> Common {
|
||||
let mut c = Common { layer: t.s(8).unwrap_or_else(|| "0".into()), linetype: t.s(6).unwrap_or_else(|| "ByLayer".into()), ..Common::default() };
|
||||
if let Some(v) = t.i(62) {
|
||||
|
|
@ -59,6 +73,9 @@ fn common(t: &T) -> Common {
|
|||
if t.i(60) == Some(1) {
|
||||
c.visible = false;
|
||||
}
|
||||
if let Some(v) = t.i(440) {
|
||||
c.transparency = transparency_440(v);
|
||||
}
|
||||
c.thickness = t.fd(39, 0.0);
|
||||
c.extrusion = Vec3::new(t.fd(210, 0.0), t.fd(220, 0.0), t.fd(230, 1.0));
|
||||
c
|
||||
|
|
|
|||
|
|
@ -244,6 +244,16 @@ fn header_vars(w: &mut W, d: &Drawing) {
|
|||
}
|
||||
}
|
||||
|
||||
/// An entity's group 440 (DXF Reference): ByBlock is `0x01000000`, a fixed transparency its alpha
|
||||
/// with the "by alpha" flag `0x02000000` (the alpha mapping layers use). ByLayer writes nothing.
|
||||
fn transparency_440(t: Transparency) -> Option<i64> {
|
||||
match t {
|
||||
Transparency::ByLayer => None,
|
||||
Transparency::ByBlock => Some(0x0100_0000),
|
||||
Transparency::Percent(p) => Some(i64::from(0x0200_0000 | ((100 - u32::from(p.min(90))) * 255 / 100))),
|
||||
}
|
||||
}
|
||||
|
||||
fn common(w: &mut W, e: &Entity, owner: &str, paper: bool, subclass: &str) {
|
||||
common_x(w, e, owner, paper, subclass, None);
|
||||
}
|
||||
|
|
@ -281,6 +291,9 @@ fn common_x(w: &mut W, e: &Entity, owner: &str, paper: bool, subclass: &str, ass
|
|||
if !e.common.visible {
|
||||
w.i(60, 1);
|
||||
}
|
||||
if let Some(v) = transparency_440(e.common.transparency) {
|
||||
w.i(440, v);
|
||||
}
|
||||
if !subclass.is_empty() {
|
||||
w.s(100, subclass);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,28 @@ fn reads_r12_style_polyline_and_paper_flag() {
|
|||
assert_eq!(d.layouts[0].entities.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_transparency_roundtrips_as_group_440() {
|
||||
// Issue #132: ByLayer writes nothing, ByBlock 0x01000000, a percentage its alpha | 0x02000000.
|
||||
let ts = [Transparency::ByLayer, Transparency::ByBlock, Transparency::Percent(0), Transparency::Percent(40), Transparency::Percent(90)];
|
||||
let mut d = Drawing::new_imperial();
|
||||
for (i, t) in ts.iter().enumerate() {
|
||||
let y = i as f64;
|
||||
let line = EntityKind::Line(Line { a: Vec3::new(0.0, y, 0.0), b: Vec3::new(1.0, y, 0.0) });
|
||||
d.add(&Space::Model, Common { transparency: *t, ..Common::default() }, line).unwrap();
|
||||
}
|
||||
let text = write_dxf(&d);
|
||||
assert_eq!(text.matches("\r\n440\r\n").count(), 4, "ByLayer writes no 440");
|
||||
assert!(text.contains("\r\n440\r\n16777216\r\n"), "ByBlock");
|
||||
assert!(text.contains("\r\n440\r\n33554585\r\n"), "40% is alpha 153");
|
||||
let back = read_dxf(text.as_bytes()).unwrap();
|
||||
assert_eq!(back.model.iter().map(|e| e.common.transparency).collect::<Vec<_>>(), ts);
|
||||
// Values that are neither ByBlock nor "by alpha" read as ByLayer.
|
||||
let text = "0\nSECTION\n2\nENTITIES\n0\nLINE\n8\n0\n440\n-1\n10\n0\n20\n0\n11\n1\n21\n1\n0\nLINE\n8\n0\n440\n33554432\n10\n0\n20\n0\n11\n1\n21\n1\n0\nENDSEC\n0\nEOF\n";
|
||||
let d = read(text.as_bytes(), "a.dxf").unwrap();
|
||||
assert_eq!(d.model.iter().map(|e| e.common.transparency).collect::<Vec<_>>(), [Transparency::ByLayer, Transparency::Percent(90)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_dxf_does_not_panic() {
|
||||
for t in [
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
|
|||
let linetype = same(&|e| e.common.linetype.clone());
|
||||
let lts = same(&|e| format!("{:.4}", e.common.ltscale));
|
||||
let lw = same(&|e| e.common.lineweight.name());
|
||||
let tr = same(&|e| e.common.transparency.name());
|
||||
let layers: Vec<String> = d.layers.iter().map(|l| l.name.clone()).collect();
|
||||
let linetypes: Vec<String> = d.linetypes.iter().map(|l| l.name.clone()).collect();
|
||||
let ids: Vec<String> = sel.iter().map(|h| h.hex()).collect();
|
||||
|
|
@ -653,7 +654,32 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
});
|
||||
prop_row(ui, "Transparency", |ui| {
|
||||
value_box(ui, "ByLayer", true, true);
|
||||
ui.menu_button(tr.clone(), |ui| {
|
||||
for v in ["ByLayer", "ByBlock"] {
|
||||
if ui.button(v).clicked() {
|
||||
set = Some(json!({ "handles": ids, "transparency": v }));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
// A fixed percentage, committed when the drag or edit ends.
|
||||
let tid = egui::Id::new(("prop_tr", &ids));
|
||||
let cur = tr.parse::<u8>().ok();
|
||||
let mut pct = ui.data_mut(|d| d.get_temp::<u8>(tid)).or(cur).unwrap_or(0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Percent");
|
||||
let r = ui.add(egui::DragValue::new(&mut pct).range(0..=90).speed(0.5));
|
||||
if r.dragged() || r.has_focus() {
|
||||
ui.data_mut(|d| d.insert_temp(tid, pct));
|
||||
}
|
||||
if r.drag_stopped() || r.lost_focus() {
|
||||
if cur != Some(pct) {
|
||||
set = Some(json!({ "handles": ids, "transparency": pct }));
|
||||
}
|
||||
ui.data_mut(|d| d.remove::<u8>(tid));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if sel.len() == 1 {
|
||||
group(ui, "Geometry");
|
||||
|
|
|
|||
Loading…
Reference in New Issue