diff --git a/src/app/properties.rs b/src/app/properties.rs index 20ec2137..82dfe43f 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -128,6 +128,64 @@ impl OpenCADStudio { } } + // Resolve MLEADER handle-backed rows (multileader style, text + // style, arrowhead block, leader linetype) to display names. + if let acadrust::EntityType::MultiLeader(ml) = entity { + let doc = &self.tabs[i].scene.document; + let mut set_named = |field: &str, name: String| { + for section in sections.iter_mut() { + if let Some(row) = + section.props.iter_mut().find(|p| p.field == field) + { + row.value = + crate::scene::model::object::PropValue::ReadOnly( + name.clone(), + ); + } + } + }; + if let Some(h) = ml.style_handle { + if let Some(name) = doc.objects.iter().find_map(|(oh, o)| match o { + acadrust::objects::ObjectType::MultiLeaderStyle(s) if *oh == h => { + Some(s.name.clone()) + } + _ => None, + }) { + set_named("mleader_style", name); + } + } + if let Some(h) = ml.text_style_handle { + if let Some(name) = doc + .text_styles + .iter() + .find(|s| s.handle == h) + .map(|s| s.name.clone()) + { + set_named("text_style_handle", name); + } + } + if let Some(h) = ml.arrowhead_handle { + if let Some(name) = doc + .block_records + .iter() + .find(|b| b.handle == h) + .map(|b| b.name.clone()) + { + set_named("arrowhead_handle", name); + } + } + if let Some(h) = ml.line_type_handle { + if let Some(name) = doc + .line_types + .iter() + .find(|l| l.handle == h) + .map(|l| l.name.clone()) + { + set_named("line_type_handle", name); + } + } + } + // Inject viewport-only properties that require doc access. if let acadrust::EntityType::Viewport(vp) = entity { let frozen_names: Vec = vp @@ -323,10 +381,50 @@ impl OpenCADStudio { // Leader: text style / vertical text placement / overall // scale come from its dimension style. acadrust::EntityType::Leader(ld) => { - if let Some(ds) = find_dim_style(doc, &ld.dimension_style) { - if !ds.dimtxsty.is_empty() { - set_row(&mut sections, "text_style", ds.dimtxsty.clone()); + // Dim style row → dropdown of the drawing's dim styles. + let names: Vec = doc + .dim_styles + .iter() + .map(|s| s.name.clone()) + .filter(|n| !n.is_empty()) + .collect(); + if !names.is_empty() { + for section in sections.iter_mut() { + if let Some(p) = section + .props + .iter_mut() + .find(|p| p.field == "dimension_style") + { + let cur = match &p.value { + crate::scene::model::object::PropValue::EditText(s) => { + s.clone() + } + _ => ld.dimension_style.clone(), + }; + p.value = crate::scene::model::object::PropValue::Choice { + selected: cur, + options: names.clone(), + }; + } } + } + // Lines & Arrows / Text / Fit are derived from the + // assigned dimension style (same source the leader + // tessellator uses), not stored on the entity. + if let Some(ds) = find_dim_style(doc, &ld.dimension_style) { + set_row( + &mut sections, + "arrow_block", + leader_arrow_label(doc, ds, ld.arrow_enabled), + ); + set_row(&mut sections, "arrow_size", format!("{:.4}", ds.dimasz)); + set_row(&mut sections, "dim_line_lw", dim_lineweight_label(ds.dimlwd)); + set_row( + &mut sections, + "dim_line_color", + dim_color_label(ds.dimclrd, &ld.override_color), + ); + set_row(&mut sections, "text_offset", format!("{:.4}", ds.dimgap)); set_row( &mut sections, "text_pos_vert", @@ -1102,6 +1200,79 @@ fn dimtad_label(dimtad: i16) -> &'static str { } } +/// Friendly arrowhead name from an arrowhead block-record name (the `_CLOSED…` +/// style internal names map to their palette labels; a null/empty name is the +/// closed-filled default). +fn arrowhead_label(name: &str) -> String { + let key = name.trim().trim_start_matches('_').to_ascii_uppercase(); + let label = match key.as_str() { + "" | "CLOSEDFILLED" => "Closed filled", + "CLOSED" => "Closed", + "CLOSEDBLANK" => "Closed blank", + "DOT" => "Dot", + "DOTSMALL" => "Dot small", + "DOTBLANK" => "Dot blank", + "SMALLDOTBLANK" => "Dot small blank", + "ORIGIN" => "Origin indicator", + "ORIGIN2" => "Origin indicator 2", + "OPEN" => "Open", + "OPEN90" => "Right angle", + "OPEN30" => "Open 30", + "NONE" => "None", + "OBLIQUE" => "Oblique", + "ARCHTICK" => "Architectural tick", + "BOXBLANK" => "Box", + "BOXFILLED" => "Box filled", + "DATUMBLANK" => "Datum triangle", + "DATUMFILLED" => "Datum triangle filled", + "INTEGRAL" => "Integral", + _ => return name.to_string(), + }; + label.to_string() +} + +/// The leader's arrowhead label, resolved from the dim style's DIMLDRBLK block. +fn leader_arrow_label( + doc: &acadrust::CadDocument, + ds: &acadrust::tables::DimStyle, + arrow_enabled: bool, +) -> String { + if !arrow_enabled { + return "None".to_string(); + } + if ds.dimldrblk.is_null() { + return "Closed filled".to_string(); + } + doc.block_records + .iter() + .find(|b| b.handle == ds.dimldrblk) + .map(|b| arrowhead_label(&b.name)) + .unwrap_or_else(|| "Closed filled".to_string()) +} + +/// DIMLWD lineweight enum → label. +fn dim_lineweight_label(dimlwd: i16) -> String { + match dimlwd { + -1 => "ByLayer".to_string(), + -2 => "ByBlock".to_string(), + -3 => "Default".to_string(), + v if v >= 0 => format!("{:.2} mm", v as f64 / 100.0), + _ => "Default".to_string(), + } +} + +/// DIMCLRD color (ACI) → label; ByBlock falls back to the leader's override. +fn dim_color_label(dimclrd: i16, override_color: &acadrust::types::Color) -> String { + match dimclrd { + 0 => match override_color.rgb() { + Some((r, g, b)) => format!("RGB({r},{g},{b})"), + None => "ByBlock".to_string(), + }, + 256 => "ByLayer".to_string(), + n => format!("Color {n}"), + } +} + /// Human-readable INSUNITS name (DXF group 70 unit codes). fn insunits_name(code: i16) -> &'static str { match code { diff --git a/src/entities/common.rs b/src/entities/common.rs index 36187b5b..e4e42812 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -193,6 +193,18 @@ pub fn ro_prop(label: &'static str, field: &'static str, value: impl Into Property { + if editable { + edit_prop(label, field, value) + } else { + ro_prop(label, field, format_length(value)) + } +} + /// A ◀ / ▶ index navigator row (e.g. a polyline's Current Vertex). `display` is /// the label shown between the arrows (e.g. "2 / 7"). pub fn stepper_prop( diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 335c420e..87db1176 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -3,7 +3,9 @@ use acadrust::Entity; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{ + center_grip, edit_prop as edit, ro_prop as ro, square_grip, stepper_prop as stepper, +}; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; @@ -149,14 +151,6 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) { // ── Properties ───────────────────────────────────────────────────────────── -fn bool_toggle(label: &str, field: &'static str, value: bool) -> Property { - Property { - label: label.into(), - field, - value: PropValue::BoolToggle { field, value }, - } -} - fn choice_prop(label: &str, field: &'static str, selected: &str, options: &[&str]) -> Property { Property { label: label.into(), @@ -168,92 +162,84 @@ fn choice_prop(label: &str, field: &'static str, selected: &str, options: &[&str } } -fn path_type_str(pt: &LeaderPathType) -> &'static str { - match pt { - LeaderPathType::StraightLine => "Straight", - LeaderPathType::Spline => "Spline", - } -} - -fn creation_type_str(ct: &LeaderCreationType) -> &'static str { - match ct { - LeaderCreationType::WithText => "With Text", - LeaderCreationType::WithTolerance => "With Tolerance", - LeaderCreationType::WithBlock => "With Block", - LeaderCreationType::NoAnnotation => "No Annotation", - } -} - -fn color_str(c: &acadrust::types::Color) -> String { - match c.rgb() { - Some((r, g, b)) => format!("RGB({},{},{})", r, g, b), - None => format!("{:?}", c), +/// Combined path/arrow "Type" value (path shape × arrowhead flag). +fn leader_type_str(path: &LeaderPathType, arrow: bool) -> &'static str { + match (path, arrow) { + (LeaderPathType::StraightLine, true) => "Line with arrow", + (LeaderPathType::StraightLine, false) => "Line without arrow", + (LeaderPathType::Spline, true) => "Spline with arrow", + (LeaderPathType::Spline, false) => "Spline without arrow", } } fn properties(leader: &Leader) -> Vec { let n = leader.vertices.len(); + // The panel's Current Vertex focus, clamped to this leader's range. + let vi = if n == 0 { + 0 + } else { + crate::scene::view::dispatch::prop_current_vertex().min(n - 1) + }; + let vertex_label = if n == 0 { + "—".to_string() + } else { + format!("{} / {}", vi + 1, n) + }; - let arrow_size = leader.text_height.max(1.0); - - let misc = vec![ - choice_prop( - "Type", - "path_type", - path_type_str(&leader.path_type), - &["Straight", "Spline"], - ), - choice_prop( - "Annotation type", - "creation_type", - creation_type_str(&leader.creation_type), - &["With Text", "With Tolerance", "With Block", "No Annotation"], - ), - Property { - label: "Dim style".into(), - field: "dimension_style", - value: PropValue::EditText(leader.dimension_style.clone()), - }, - ]; - - let lines_arrows = vec![ - ro("Dim line color", "override_color", color_str(&leader.override_color)), - ro( - "Dim line lineweight", - "line_weight", - format!("{:?}", leader.common.line_weight), - ), - ro("Dim line linetype", "linetype", leader.common.linetype.clone()), - bool_toggle("Arrowhead", "arrow_enabled", leader.arrow_enabled), - ro("Arrow size", "arrow_size", format!("{:.4}", arrow_size)), - ]; - - let text = vec![ - edit("Text height", "text_height", leader.text_height), - ro( - "Text offset", - "text_offset", - format!("{:.4}", leader.annotation_offset.length()), - ), - ro("Text style", "text_style", String::new()), - ro("Text color", "text_color", color_str(&leader.override_color)), - ro("Text position vert", "text_pos_vert", String::new()), - ]; - - let fit = vec![ro("Dim scale overall", "dim_scale_overall", String::new())]; - - let mut geometry = vec![ro("Vertex", "vertex_count", n.to_string())]; - if let Some(a) = leader.arrow_point() { - geometry.push(edit("Vertex X", "vertex_x", a.x)); - geometry.push(edit("Vertex Y", "vertex_y", a.y)); - geometry.push(edit("Vertex Z", "vertex_z", a.z)); + // Geometry sits right after General — a leader owns editable path vertices, + // navigated one at a time by the Current Vertex spinner. + let mut geometry = vec![stepper("Current Vertex", "current_vertex", vertex_label)]; + if let Some(v) = leader.vertices.get(vi) { + geometry.push(edit("Vertex X", "vertex_x", v.x)); + geometry.push(edit("Vertex Y", "vertex_y", v.y)); + geometry.push(edit("Vertex Z", "vertex_z", v.z)); } else { geometry.push(ro("Vertex X", "vertex_x", String::new())); geometry.push(ro("Vertex Y", "vertex_y", String::new())); geometry.push(ro("Vertex Z", "vertex_z", String::new())); } + // Misc: Dim style (upgraded to a dropdown by the panel builder), the + // combined path/arrow Type, and annotative state (from the dim style). + let misc = vec![ + Property { + label: "Dim style".into(), + field: "dimension_style", + value: PropValue::EditText(leader.dimension_style.clone()), + }, + choice_prop( + "Type", + "leader_type", + leader_type_str(&leader.path_type, leader.arrow_enabled), + &[ + "Line with arrow", + "Line without arrow", + "Spline with arrow", + "Spline without arrow", + ], + ), + ro("Annotative", "annotative", "No"), + ]; + + // Lines & Arrows / Text / Fit are dimension-style-derived; the panel builder + // resolves leader.dimension_style and fills these values from the DimStyle. + let lines_arrows = vec![ + ro("Arrow", "arrow_block", "Closed filled"), + ro("Arrow size", "arrow_size", String::new()), + ro("Dim line lineweight", "dim_line_lw", "ByLayer"), + ro("Dim line color", "dim_line_color", "ByLayer"), + ]; + let text = vec![ + ro("Text offset", "text_offset", String::new()), + ro("Text pos vert", "text_pos_vert", String::new()), + ]; + let fit = vec![ro("Dim scale overall", "dim_scale_overall", String::new())]; + vec![ + PropSection { + title: "Geometry".into(), + props: geometry, + }, PropSection { title: "Misc".into(), props: misc, @@ -270,18 +256,30 @@ fn properties(leader: &Leader) -> Vec { title: "Fit".into(), props: fit, }, - PropSection { - title: "Geometry".into(), - props: geometry, - }, ] } fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) { let f64 = |s: &str| -> Option { s.trim().parse().ok() }; + // Vertex X/Y/Z edit whichever vertex the Current Vertex navigator focuses. + let vi = if leader.vertices.is_empty() { + 0 + } else { + crate::scene::view::dispatch::prop_current_vertex().min(leader.vertices.len() - 1) + }; match field { "dimension_style" => leader.dimension_style = value.to_string(), + "leader_type" => { + let (p, a) = match value { + "Line without arrow" => (LeaderPathType::StraightLine, false), + "Spline with arrow" => (LeaderPathType::Spline, true), + "Spline without arrow" => (LeaderPathType::Spline, false), + _ => (LeaderPathType::StraightLine, true), + }; + leader.path_type = p; + leader.arrow_enabled = a; + } "path_type" => { leader.path_type = match value { "Spline" => LeaderPathType::Spline, @@ -387,17 +385,17 @@ fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) { } } "vertex_x" => { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { + if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) { vert.x = v; } } "vertex_y" => { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { + if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) { vert.y = v; } } "vertex_z" => { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { + if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) { vert.z = v; } } diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 238e0d66..6640abdf 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -1,7 +1,9 @@ use acadrust::entities::{AttachmentPoint, DrawingDirection, MText}; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip, triangle_grip}; +use crate::entities::common::{ + edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip, +}; use crate::entities::text_support::{ layout_mtext, resolve_text_style, GlyphBox, MTextRenderOpts, MTextVAnchor, }; @@ -10,66 +12,41 @@ use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; +/// Combined attachment point shown as a single justify dropdown value. fn attachment_str(a: &AttachmentPoint) -> &'static str { match a { - AttachmentPoint::TopLeft => "Top Left", - AttachmentPoint::TopCenter => "Top Center", - AttachmentPoint::TopRight => "Top Right", - AttachmentPoint::MiddleLeft => "Middle Left", - AttachmentPoint::MiddleCenter => "Middle Center", - AttachmentPoint::MiddleRight => "Middle Right", - AttachmentPoint::BottomLeft => "Bottom Left", - AttachmentPoint::BottomCenter => "Bottom Center", - AttachmentPoint::BottomRight => "Bottom Right", + AttachmentPoint::TopLeft => "Top left", + AttachmentPoint::TopCenter => "Top center", + AttachmentPoint::TopRight => "Top right", + AttachmentPoint::MiddleLeft => "Middle left", + AttachmentPoint::MiddleCenter => "Middle center", + AttachmentPoint::MiddleRight => "Middle right", + AttachmentPoint::BottomLeft => "Bottom left", + AttachmentPoint::BottomCenter => "Bottom center", + AttachmentPoint::BottomRight => "Bottom right", } } -fn mtext_halign_str(a: &AttachmentPoint) -> &'static str { - match a { - AttachmentPoint::TopLeft | AttachmentPoint::MiddleLeft | AttachmentPoint::BottomLeft => { - "Left" - } - AttachmentPoint::TopCenter - | AttachmentPoint::MiddleCenter - | AttachmentPoint::BottomCenter => "Center", - AttachmentPoint::TopRight | AttachmentPoint::MiddleRight | AttachmentPoint::BottomRight => { - "Right" - } - } -} - -fn mtext_valign_str(a: &AttachmentPoint) -> &'static str { - match a { - AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => "Top", - AttachmentPoint::MiddleLeft - | AttachmentPoint::MiddleCenter - | AttachmentPoint::MiddleRight => "Middle", - AttachmentPoint::BottomLeft - | AttachmentPoint::BottomCenter - | AttachmentPoint::BottomRight => "Bottom", - } -} - -fn mtext_attachment_from_align(h: &str, v: &str) -> Option { - Some(match (h, v) { - ("Left", "Top") => AttachmentPoint::TopLeft, - ("Center", "Top") => AttachmentPoint::TopCenter, - ("Right", "Top") => AttachmentPoint::TopRight, - ("Left", "Middle") => AttachmentPoint::MiddleLeft, - ("Center", "Middle") => AttachmentPoint::MiddleCenter, - ("Right", "Middle") => AttachmentPoint::MiddleRight, - ("Left", "Bottom") => AttachmentPoint::BottomLeft, - ("Center", "Bottom") => AttachmentPoint::BottomCenter, - ("Right", "Bottom") => AttachmentPoint::BottomRight, +fn attachment_from_justify(value: &str) -> Option { + Some(match value { + "Top left" => AttachmentPoint::TopLeft, + "Top center" => AttachmentPoint::TopCenter, + "Top right" => AttachmentPoint::TopRight, + "Middle left" => AttachmentPoint::MiddleLeft, + "Middle center" => AttachmentPoint::MiddleCenter, + "Middle right" => AttachmentPoint::MiddleRight, + "Bottom left" => AttachmentPoint::BottomLeft, + "Bottom center" => AttachmentPoint::BottomCenter, + "Bottom right" => AttachmentPoint::BottomRight, _ => return None, }) } fn drawing_dir_str(d: &DrawingDirection) -> &'static str { match d { - DrawingDirection::LeftToRight => "Left to Right", - DrawingDirection::TopToBottom => "Top to Bottom", - DrawingDirection::ByStyle => "By Style", + DrawingDirection::LeftToRight => "Left to right", + DrawingDirection::TopToBottom => "Top to bottom", + DrawingDirection::ByStyle => "By style", } } @@ -196,7 +173,7 @@ fn columns_str(c: &acadrust::entities::MTextColumnData) -> &'static str { match c.column_type { 1 => "Static", 2 => "Dynamic", - _ => "None", + _ => "No columns", } } @@ -205,6 +182,11 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { // by the line-spacing factor. let line_space_distance = t.height * 1.666_666_666_666_667 * t.line_spacing_factor; let text_frame_on = (t.background_fill_flags & 0x10) != 0; + // Defined width is only live without columns; defined height is live for + // static columns or manual-height dynamic columns, grayed otherwise. + let col_type = t.column_data.column_type; + let width_editable = col_type == 0; + let height_editable = col_type == 1 || (col_type == 2 && !t.column_data.auto_height); vec![ PropSection { title: "Text".into(), @@ -233,52 +215,55 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { ), Property { label: "Justify".into(), - field: "h_align", + field: "justify", value: PropValue::Choice { - selected: mtext_halign_str(&t.attachment_point).to_string(), - options: ["Left", "Center", "Right"] - .into_iter() - .map(str::to_string) - .collect(), + selected: attachment_str(&t.attachment_point).to_string(), + options: [ + "Top left", + "Top center", + "Top right", + "Middle left", + "Middle center", + "Middle right", + "Bottom left", + "Bottom center", + "Bottom right", + ] + .into_iter() + .map(str::to_string) + .collect(), }, }, Property { - label: "V-Align".into(), - field: "v_align", + label: "Direction".into(), + field: "direction", value: PropValue::Choice { - selected: mtext_valign_str(&t.attachment_point).to_string(), - options: ["Top", "Middle", "Bottom"] + selected: drawing_dir_str(&t.drawing_direction).to_string(), + options: ["By style", "Left to right", "Top to bottom"] .into_iter() .map(str::to_string) .collect(), }, }, - ro( - "Attachment", - "attachment", - attachment_str(&t.attachment_point).to_string(), - ), - ro( - "Direction", - "direction", - drawing_dir_str(&t.drawing_direction).to_string(), - ), edit("Text height", "height", t.height), edit("Rotation", "rotation", t.rotation.to_degrees()), edit("Line space factor", "line_spacing", t.line_spacing_factor), - ro( - "Line space distance", - "line_space_distance", - format!("{line_space_distance:.4}"), - ), - ro( - "Line space style", - "line_space_style", - match t.line_spacing_style { - acadrust::entities::LineSpacingStyle::Exactly => "Exactly", - _ => "At least", + edit("Line space distance", "line_space_distance", line_space_distance), + Property { + label: "Line space style".into(), + field: "line_space_style", + value: PropValue::Choice { + selected: match t.line_spacing_style { + acadrust::entities::LineSpacingStyle::Exactly => "Exactly", + _ => "At least", + } + .to_string(), + options: ["At least", "Exactly"] + .into_iter() + .map(str::to_string) + .collect(), }, - ), + }, Property { label: "Background mask".into(), field: "background_mask", @@ -296,13 +281,18 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { .collect(), }, }, - Property { - label: "Background color".into(), - field: "background_color", - value: PropValue::ColorChoice(t.background_color.clone()), - }, - edit("Defined width", "rect_w", t.rectangle_width), - edit("Defined height", "rect_h", t.rectangle_height.unwrap_or(0.0)), + num_row("Defined width", "rect_w", t.rectangle_width, width_editable), + num_row( + "Defined height", + "rect_h", + t.rectangle_height.unwrap_or(0.0), + height_editable, + ), + ro( + "Columns", + "columns", + columns_str(&t.column_data).to_string(), + ), Property { label: "Text frame".into(), field: "text_frame", @@ -311,11 +301,6 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { value: text_frame_on, }, }, - ro( - "Columns", - "columns", - columns_str(&t.column_data).to_string(), - ), ], }, PropSection { @@ -339,18 +324,8 @@ fn apply_geom_prop(t: &mut MText, field: &str, value: &str) { t.style = value.to_string(); return; } - "h_align" => { - if let Some(next) = - mtext_attachment_from_align(value, mtext_valign_str(&t.attachment_point)) - { - t.attachment_point = next; - } - return; - } - "v_align" => { - if let Some(next) = - mtext_attachment_from_align(mtext_halign_str(&t.attachment_point), value) - { + "justify" => { + if let Some(next) = attachment_from_justify(value) { t.attachment_point = next; } return; @@ -376,6 +351,23 @@ fn apply_geom_prop(t: &mut MText, field: &str, value: &str) { } return; } + "direction" => { + t.drawing_direction = match value { + "Left to right" => DrawingDirection::LeftToRight, + "Top to bottom" => DrawingDirection::TopToBottom, + "By style" => DrawingDirection::ByStyle, + _ => return, + }; + return; + } + "line_space_style" => { + t.line_spacing_style = match value { + "Exactly" => acadrust::entities::LineSpacingStyle::Exactly, + "At least" => acadrust::entities::LineSpacingStyle::AtLeast, + _ => return, + }; + return; + } _ => {} } let Some(v) = crate::entities::common::parse_f64(value) else { @@ -390,6 +382,14 @@ fn apply_geom_prop(t: &mut MText, field: &str, value: &str) { "rect_h" if v > 0.0 => t.rectangle_height = Some(v), "rotation" => t.rotation = v.to_radians(), "line_spacing" if v > 0.0 => t.line_spacing_factor = v, + // Editing the absolute distance back-solves the line-spacing factor so + // the two stay consistent (distance = height × 5/3 × factor). + "line_space_distance" if v > 0.0 => { + let denom = t.height * 1.666_666_666_666_667; + if denom > 0.0 { + t.line_spacing_factor = v / denom; + } + } _ => {} } } diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 663da74f..5f17d8e4 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1,4 +1,7 @@ -use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType, TextAttachmentType}; +use acadrust::entities::{ + FlowDirectionType, LeaderContentType, LineSpacingStyle, MultiLeader, MultiLeaderPathType, + TextAlignmentType, TextAttachmentDirectionType, TextAttachmentType, +}; use crate::entities::text_support::{ layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle, @@ -28,7 +31,7 @@ use glam::DVec3; use crate::command::EntityTransform; use crate::entities::common::{ - center_grip, edit_prop as edit, ro_prop as ro, square_grip, triangle_grip, + center_grip, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip, }; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; @@ -474,32 +477,71 @@ fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) { // ── Properties ───────────────────────────────────────────────────────────── -fn content_type_str(ct: &LeaderContentType) -> &'static str { - match ct { - LeaderContentType::None => "None", - LeaderContentType::Block => "Block", - LeaderContentType::MText => "MText", - LeaderContentType::Tolerance => "Tolerance", - } -} - -fn path_type_str(pt: &MultiLeaderPathType) -> &'static str { - match pt { - MultiLeaderPathType::Invisible => "Invisible", - MultiLeaderPathType::StraightLineSegments => "Straight", - MultiLeaderPathType::Spline => "Spline", - } -} +/// The nine horizontal text-attachment options, indexed 1:1 to +/// `TextAttachmentType` values 0–8 (values 9/10 are the vertical set). +const ATTACH_LABELS: [&str; 9] = [ + "Top of top line", + "Middle of top line", + "Middle of text", + "Middle of bottom line", + "Bottom of bottom line", + "Bottom of top line", + "Underline bottom line", + "Underline top line", + "Underline all", +]; fn attachment_str(a: &TextAttachmentType) -> &'static str { match a { - TextAttachmentType::TopOfTopLine => "Top of Top", - TextAttachmentType::MiddleOfTopLine => "Mid of Top", - TextAttachmentType::MiddleOfText => "Mid of Text", - TextAttachmentType::MiddleOfBottomLine => "Mid of Bot", - TextAttachmentType::BottomOfBottomLine => "Bot of Bot", - TextAttachmentType::BottomLine => "Bottom Line", - _ => "Other", + TextAttachmentType::TopOfTopLine => ATTACH_LABELS[0], + TextAttachmentType::MiddleOfTopLine => ATTACH_LABELS[1], + TextAttachmentType::MiddleOfText => ATTACH_LABELS[2], + TextAttachmentType::MiddleOfBottomLine => ATTACH_LABELS[3], + TextAttachmentType::BottomOfBottomLine => ATTACH_LABELS[4], + TextAttachmentType::BottomLine => ATTACH_LABELS[5], + TextAttachmentType::BottomOfTopLineUnderlineBottomLine => ATTACH_LABELS[6], + TextAttachmentType::BottomOfTopLineUnderlineTopLine => ATTACH_LABELS[7], + TextAttachmentType::BottomOfTopLineUnderlineAll => ATTACH_LABELS[8], + TextAttachmentType::CenterOfText => "Center of text", + TextAttachmentType::CenterOfTextOverline => "Center of text (overline)", + } +} + +fn leader_type_str(pt: &MultiLeaderPathType) -> &'static str { + match pt { + MultiLeaderPathType::Spline => "Spline", + MultiLeaderPathType::Invisible => "None", + MultiLeaderPathType::StraightLineSegments => "Straight", + } +} + +fn text_align_str(a: &TextAlignmentType) -> &'static str { + match a { + TextAlignmentType::Left => "Left", + TextAlignmentType::Center => "Center", + TextAlignmentType::Right => "Right", + } +} + +fn flow_dir_str(d: &FlowDirectionType) -> &'static str { + match d { + FlowDirectionType::Horizontal => "Left to right", + FlowDirectionType::Vertical => "Top to bottom", + FlowDirectionType::ByStyle => "By style", + } +} + +fn attach_dir_str(d: &TextAttachmentDirectionType) -> &'static str { + match d { + TextAttachmentDirectionType::Horizontal => "Horizontal", + TextAttachmentDirectionType::Vertical => "Vertical", + } +} + +fn line_style_str(s: &LineSpacingStyle) -> &'static str { + match s { + LineSpacingStyle::Exactly => "Exactly", + _ => "At least", } } @@ -531,130 +573,129 @@ fn hexh(h: Option) -> String { fn properties(ml: &MultiLeader) -> Vec { let ctx = &ml.context; - let first_root = ctx.leader_roots.first(); // ── Misc ───────────────────────────────────────────────────────────── let misc = PropSection { title: "Misc".into(), props: vec![ - ro("Leader type", "path_type", path_type_str(&ml.path_type)), - choice( - "Content type", - "content_type", - content_type_str(&ml.content_type), - &["None", "MText", "Block", "Tolerance"], + // Overall scale is grayed when annotative (annotation scale drives sizing). + num_row( + "Overall scale", + "scale_factor", + ml.scale_factor, + !ml.enable_annotation_scale, ), - edit("Overall scale", "scale_factor", ml.scale_factor), + // Style name is resolved from style_handle by the panel builder (needs doc). + ro("Multileader style", "mleader_style", "Standard"), bool_toggle( "Annotative", "enable_annotation_scale", ml.enable_annotation_scale, ), - // No stored annotative-scale name on the entity. - ro("Annotative scale", "annotative_scale", String::new()), ], }; // ── Leaders ────────────────────────────────────────────────────────── + // Landing rows are folded in here: the standalone "Leader Structure" group + // is a style-dialog tab, not a palette group. let leaders = PropSection { title: "Leaders".into(), props: vec![ - ro("Leader type", "path_type", path_type_str(&ml.path_type)), - ro("Leader color", "line_color", format!("{:?}", ml.line_color)), - ro( - "Leader linetype", - "line_type_handle", - hexh(ml.line_type_handle), + choice( + "Leader type", + "path_type", + leader_type_str(&ml.path_type), + &["Straight", "Spline", "None"], ), - ro( - "Leader linetype scale", - "leader_linetype_scale", - format!("{:.4}", ml.common.linetype_scale), - ), - ro( - "Leader lineweight", - "line_weight", - format!("{:?}", ml.line_weight), - ), - ro("Arrowhead", "arrowhead_handle", hexh(ml.arrowhead_handle)), + Property { + label: "Leader color".into(), + field: "line_color", + value: PropValue::ColorChoice(ml.line_color), + }, + // Linetype name resolved from line_type_handle by the panel builder. + ro("Leader linetype", "line_type_handle", "ByBlock"), + Property { + label: "Leader lineweight".into(), + field: "line_weight", + value: PropValue::LwChoice(ml.line_weight), + }, + // Arrowhead block name resolved by the panel builder (default "Closed filled"). + ro("Arrowhead", "arrowhead_handle", "Closed filled"), edit("Arrowhead size", "arrowhead_size", ml.arrowhead_size), - ], - }; - - // ── Leader Structure ───────────────────────────────────────────────── - let structure = PropSection { - title: "Leader Structure".into(), - props: vec![ - // Maximum leader points / segment-angle constraints are leader-style - // settings not stored on the entity instance. - ro("Maximum leader points", "max_leader_points", String::new()), - ro("First segment angle", "first_segment_angle", String::new()), - ro("Second segment angle", "second_segment_angle", String::new()), - bool_toggle("Landing", "enable_landing", ml.enable_landing), - edit( + bool_toggle("Horizontal Landing", "enable_dogleg", ml.enable_dogleg), + num_row( "Landing distance", "landing_distance", - first_root.map(|r| r.landing_distance).unwrap_or(0.0), + ml.dogleg_length, + ml.enable_dogleg, ), - edit("Dogleg length", "dogleg_length", ml.dogleg_length), + bool_toggle("Leader extension", "enable_landing", ml.enable_landing), ], }; - // ── Text ───────────────────────────────────────────────────────────── + // ── Text (shown only for MText content) ────────────────────────────── let text = PropSection { title: "Text".into(), props: vec![ - ro("Text style", "text_style_handle", hexh(ml.text_style_handle)), - ro( - "Text angle", - "text_angle_type", - format!("{:?}", ml.text_angle_type), - ), - ro("Text color", "text_color", format!("{:?}", ml.text_color)), - edit("Text height", "text_height", ml.text_height), - ro( - "Justification", + Property { + label: "Contents".into(), + field: "text_string", + value: PropValue::EditText(ctx.text_string.clone()), + }, + // Text-style name resolved from text_style_handle by the panel builder. + ro("Text style", "text_style_handle", "Standard"), + choice( + "Justify", "text_alignment", - format!("{:?}", ml.text_alignment), - ), - bool_toggle("Frame text", "text_frame", ml.text_frame), - choice( - "Left attachment type", - "text_left_attachment", - attachment_str(&ml.text_left_attachment), - &[ - "Top of Top", - "Mid of Top", - "Mid of Text", - "Mid of Bot", - "Bot of Bot", - "Bottom Line", - ], + text_align_str(&ml.text_alignment), + &["Left", "Center", "Right"], ), choice( - "Right attachment type", - "text_right_attachment", - attachment_str(&ml.text_right_attachment), - &[ - "Top of Top", - "Mid of Top", - "Mid of Text", - "Mid of Bot", - "Bot of Bot", - "Bottom Line", - ], + "Direction", + "text_flow_direction", + flow_dir_str(&ctx.text_flow_direction), + &["By style", "Left to right", "Top to bottom"], ), - ro( - "Text align type", - "text_attachment_direction", - format!("{:?}", ml.text_attachment_direction), + edit("Width", "text_width", ctx.text_width), + edit("Height", "text_height", ml.text_height), + edit("Rotation", "text_rotation", ctx.text_rotation.to_degrees()), + edit("Line space factor", "line_spacing", ctx.line_spacing_factor), + edit( + "Line space distance", + "line_space_distance", + ml.text_height * 1.666_666_666_666_667 * ctx.line_spacing_factor, + ), + choice( + "Line space style", + "line_space_style", + line_style_str(&ctx.line_spacing_style), + &["At least", "Exactly"], ), - edit("Landing gap", "landing_gap", ctx.landing_gap), bool_toggle( "Background mask", "background_fill_enabled", ctx.background_fill_enabled, ), + choice( + "Attachment type", + "text_attachment_direction", + attach_dir_str(&ml.text_attachment_direction), + &["Horizontal", "Vertical"], + ), + choice( + "Left Attachment", + "text_left_attachment", + attachment_str(&ml.text_left_attachment), + &ATTACH_LABELS, + ), + choice( + "Right Attachment", + "text_right_attachment", + attachment_str(&ml.text_right_attachment), + &ATTACH_LABELS, + ), + edit("Landing gap", "landing_gap", ctx.landing_gap), + bool_toggle("Text frame", "text_frame", ml.text_frame), ], }; @@ -668,7 +709,7 @@ fn properties(ml: &MultiLeader) -> Vec { hexh(ml.block_content_handle), ), ro( - "Block attachment", + "Block connection", "block_connection_type", format!("{:?}", ml.block_connection_type), ), @@ -693,7 +734,15 @@ fn properties(ml: &MultiLeader) -> Vec { ], }; - vec![misc, leaders, structure, text, block] + // Text and Block groups are mutually exclusive, keyed on the content type; + // only one is ever shown (neither for None/Tolerance). + let mut sections = vec![misc, leaders]; + match ml.content_type { + LeaderContentType::MText => sections.push(text), + LeaderContentType::Block => sections.push(block), + _ => {} + } + sections } fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { @@ -740,7 +789,7 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { "path_type" => { ml.path_type = match value { "Spline" => MultiLeaderPathType::Spline, - "Invisible" => MultiLeaderPathType::Invisible, + "None" => MultiLeaderPathType::Invisible, _ => MultiLeaderPathType::StraightLineSegments, }; } @@ -788,8 +837,11 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { } } "landing_distance" => { - if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) { - root.landing_distance = v; + if let Some(v) = f64(value) { + ml.dogleg_length = v; + if let Some(root) = ml.context.leader_roots.first_mut() { + root.landing_distance = v; + } } } "landing_gap" => { @@ -833,17 +885,76 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { ml.text_bottom_attachment = parse_attachment(value); ml.context.text_bottom_attachment = parse_attachment(value); } + "text_alignment" => { + ml.text_alignment = match value { + "Center" => TextAlignmentType::Center, + "Right" => TextAlignmentType::Right, + _ => TextAlignmentType::Left, + }; + ml.context.text_alignment = match value { + "Center" => TextAlignmentType::Center, + "Right" => TextAlignmentType::Right, + _ => TextAlignmentType::Left, + }; + } + "text_flow_direction" => { + ml.context.text_flow_direction = match value { + "Left to right" => FlowDirectionType::Horizontal, + "Top to bottom" => FlowDirectionType::Vertical, + _ => FlowDirectionType::ByStyle, + }; + } + "text_attachment_direction" => { + ml.text_attachment_direction = match value { + "Vertical" => TextAttachmentDirectionType::Vertical, + _ => TextAttachmentDirectionType::Horizontal, + }; + } + "line_space_style" => { + ml.context.line_spacing_style = match value { + "Exactly" => LineSpacingStyle::Exactly, + _ => LineSpacingStyle::AtLeast, + }; + } + "text_width" => { + if let Some(v) = f64(value) { + ml.context.text_width = v; + } + } + "text_rotation" => { + if let Some(v) = f64(value) { + ml.context.text_rotation = v.to_radians(); + } + } + "line_spacing" => { + if let Some(v) = f64(value) { + if v > 0.0 { + ml.context.line_spacing_factor = v; + } + } + } + "line_space_distance" => { + if let Some(v) = f64(value) { + let denom = ml.text_height * 1.666_666_666_666_667; + if v > 0.0 && denom > 0.0 { + ml.context.line_spacing_factor = v / denom; + } + } + } _ => {} } } fn parse_attachment(s: &str) -> TextAttachmentType { match s { - "Top of Top" => TextAttachmentType::TopOfTopLine, - "Mid of Top" => TextAttachmentType::MiddleOfTopLine, - "Mid of Bot" => TextAttachmentType::MiddleOfBottomLine, - "Bot of Bot" => TextAttachmentType::BottomOfBottomLine, - "Bottom Line" => TextAttachmentType::BottomLine, + "Top of top line" => TextAttachmentType::TopOfTopLine, + "Middle of top line" => TextAttachmentType::MiddleOfTopLine, + "Middle of bottom line" => TextAttachmentType::MiddleOfBottomLine, + "Bottom of bottom line" => TextAttachmentType::BottomOfBottomLine, + "Bottom of top line" => TextAttachmentType::BottomLine, + "Underline bottom line" => TextAttachmentType::BottomOfTopLineUnderlineBottomLine, + "Underline top line" => TextAttachmentType::BottomOfTopLineUnderlineTopLine, + "Underline all" => TextAttachmentType::BottomOfTopLineUnderlineAll, _ => TextAttachmentType::MiddleOfText, } } diff --git a/src/entities/text.rs b/src/entities/text.rs index 41daf433..2dea2ff0 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -1,7 +1,9 @@ use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA}; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; +use crate::entities::common::{ + edit_prop as edit, num_prop as num_row, parse_f64, ro_prop as ro, square_grip, +}; use crate::entities::text_support::{ resolve_dxf_special_chars, resolve_text_style, text_local_bounds, }; @@ -11,25 +13,26 @@ use crate::scene::text::lff; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; -fn text_halign_str(a: &acadrust::entities::TextHorizontalAlignment) -> &'static str { - use acadrust::entities::TextHorizontalAlignment::*; - match a { - Left => "Left", - Center => "Center", - Right => "Right", - Aligned => "Aligned", - Middle => "Middle", - Fit => "Fit", - } -} - -fn text_valign_str(a: &acadrust::entities::TextVerticalAlignment) -> &'static str { - use acadrust::entities::TextVerticalAlignment::*; - match a { - Baseline => "Baseline", - Bottom => "Bottom", - Middle => "Middle", - Top => "Top", +/// Combined single-line-text justification (horizontal × vertical) shown as one +/// dropdown value. Horizontal-only modes (Aligned/Middle/Fit) ignore the +/// vertical component the way the underlying alignment does. +fn text_justify_str(h: &HA, v: &VA) -> &'static str { + match (h, v) { + (HA::Aligned, _) => "Aligned", + (HA::Fit, _) => "Fit", + (HA::Middle, _) => "Middle", + (HA::Left, VA::Baseline) => "Left", + (HA::Center, VA::Baseline) => "Center", + (HA::Right, VA::Baseline) => "Right", + (HA::Left, VA::Top) => "Top left", + (HA::Center, VA::Top) => "Top center", + (HA::Right, VA::Top) => "Top right", + (HA::Left, VA::Middle) => "Middle left", + (HA::Center, VA::Middle) => "Middle center", + (HA::Right, VA::Middle) => "Middle right", + (HA::Left, VA::Bottom) => "Bottom left", + (HA::Center, VA::Bottom) => "Bottom center", + (HA::Right, VA::Bottom) => "Bottom right", } } @@ -251,9 +254,22 @@ fn grips(t: &Text) -> Vec { } fn properties(t: &Text, text_style_names: &[String]) -> Vec { - // Text alignment point (second alignment / justify point). Falls back to - // the insertion point for Left/Baseline text where no second point exists. + // Which geometry rows are live depends on justification. Plain Left text is + // anchored by the insertion point and has no second alignment point; every + // other justification anchors on the alignment point (and recomputes the + // insertion point), except Aligned/Fit which are true two-point spans where + // both points are live. + let is_plain_left = matches!(t.horizontal_alignment, HA::Left) + && matches!(t.vertical_alignment, VA::Baseline); + let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit); + let align_editable = !is_plain_left; + // The alignment point is meaningless (reset to the origin) for plain-Left text. let ap = t.alignment_point.unwrap_or(t.insertion_point); + let (ax, ay, az) = if align_editable { + (ap.x, ap.y, ap.z) + } else { + (0.0, 0.0, 0.0) + }; vec![ PropSection { title: "Text".into(), @@ -275,45 +291,53 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec { options: text_style_names.to_vec(), }, }, - ro("Annotative", "annotative", String::new()), - ro("Annotative scale", "annotative_scale", String::new()), + ro("Annotative", "annotative", "No"), Property { label: "Justify".into(), - field: "h_align", + field: "justify", value: PropValue::Choice { - selected: text_halign_str(&t.horizontal_alignment).to_string(), - options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"] - .into_iter() - .map(str::to_string) - .collect(), - }, - }, - Property { - label: "V-Align".into(), - field: "v_align", - value: PropValue::Choice { - selected: text_valign_str(&t.vertical_alignment).to_string(), - options: ["Baseline", "Bottom", "Middle", "Top"] - .into_iter() - .map(str::to_string) - .collect(), + selected: text_justify_str( + &t.horizontal_alignment, + &t.vertical_alignment, + ) + .to_string(), + options: [ + "Left", + "Center", + "Right", + "Aligned", + "Middle", + "Fit", + "Top left", + "Top center", + "Top right", + "Middle left", + "Middle center", + "Middle right", + "Bottom left", + "Bottom center", + "Bottom right", + ] + .into_iter() + .map(str::to_string) + .collect(), }, }, edit("Height", "height", t.height), edit("Rotation", "rotation", t.rotation.to_degrees()), edit("Width factor", "width_factor", t.width_factor), edit("Obliquing", "oblique_angle", t.oblique_angle.to_degrees()), - edit("Text alignment X", "align_x", ap.x), - edit("Text alignment Y", "align_y", ap.y), - edit("Text alignment Z", "align_z", ap.z), + num_row("Text alignment X", "align_x", ax, align_editable), + num_row("Text alignment Y", "align_y", ay, align_editable), + num_row("Text alignment Z", "align_z", az, align_editable), ], }, PropSection { title: "Geometry".into(), props: vec![ - edit("Position X", "ins_x", t.insertion_point.x), - edit("Position Y", "ins_y", t.insertion_point.y), - edit("Position Z", "ins_z", t.insertion_point.z), + num_row("Position X", "ins_x", t.insertion_point.x, pos_editable), + num_row("Position Y", "ins_y", t.insertion_point.y, pos_editable), + num_row("Position Z", "ins_z", t.insertion_point.z, pos_editable), ], }, PropSection { @@ -350,27 +374,27 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) { t.style = value.to_string(); return; } - "h_align" => { - t.horizontal_alignment = match value { - "Left" => HA::Left, - "Center" => HA::Center, - "Right" => HA::Right, - "Aligned" => HA::Aligned, - "Middle" => HA::Middle, - "Fit" => HA::Fit, - _ => return, - }; - sync_text_alignment_point(t); - return; - } - "v_align" => { - t.vertical_alignment = match value { - "Baseline" => VA::Baseline, - "Bottom" => VA::Bottom, - "Middle" => VA::Middle, - "Top" => VA::Top, + "justify" => { + let (h, v) = match value { + "Left" => (HA::Left, VA::Baseline), + "Center" => (HA::Center, VA::Baseline), + "Right" => (HA::Right, VA::Baseline), + "Aligned" => (HA::Aligned, VA::Baseline), + "Middle" => (HA::Middle, VA::Baseline), + "Fit" => (HA::Fit, VA::Baseline), + "Top left" => (HA::Left, VA::Top), + "Top center" => (HA::Center, VA::Top), + "Top right" => (HA::Right, VA::Top), + "Middle left" => (HA::Left, VA::Middle), + "Middle center" => (HA::Center, VA::Middle), + "Middle right" => (HA::Right, VA::Middle), + "Bottom left" => (HA::Left, VA::Bottom), + "Bottom center" => (HA::Center, VA::Bottom), + "Bottom right" => (HA::Right, VA::Bottom), _ => return, }; + t.horizontal_alignment = h; + t.vertical_alignment = v; sync_text_alignment_point(t); return; } diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index 0c494dad..35409d79 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -9,7 +9,16 @@ pub fn general_section(entity: &EntityType) -> PropSection { } else { common.linetype.clone() }; - let transp_pct = (common.transparency.alpha() as f64 / 255.0 * 100.0).round() as u32; + // Alpha 0 is the ByLayer default (Transparency::BY_LAYER); show it by name + // and fall back to a rounded percentage only for an explicit value. + let transp_display = if common.transparency.alpha() == 0 { + "ByLayer".to_string() + } else { + format!( + "{}", + (common.transparency.alpha() as f64 / 255.0 * 100.0).round() as u32 + ) + }; // Hyperlink is stored in XDATA under the "PE_URL" application. let hyperlink = common @@ -47,7 +56,7 @@ pub fn general_section(entity: &EntityType) -> PropSection { value: PropValue::LinetypeChoice(linetype_display), }, Property { - label: "LT Scale".into(), + label: "Linetype scale".into(), field: "linetype_scale", value: PropValue::EditText(format!("{:.4}", common.linetype_scale)), }, @@ -71,7 +80,7 @@ pub fn general_section(entity: &EntityType) -> PropSection { Property { label: "Transparency".into(), field: "transparency", - value: PropValue::EditText(format!("{transp_pct}")), + value: PropValue::EditText(transp_display), }, Property { label: "Hyperlink".into(), @@ -102,6 +111,7 @@ pub fn visualization_section(entity: &EntityType) -> Option { EntityType::Block(_) | EntityType::BlockEnd(_) | EntityType::Seqend(_) + | EntityType::Leader(_) | EntityType::Unknown(_) ) { return None;