feat(properties): rework Text/MText/MLeader/Leader panels to standard layout

Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:

- Text: combined Justify (horizontal x vertical) dropdown; Position and
  Text-alignment rows become editable/grayed by justification; drop the
  Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
  (back-solves the factor) and Line space style; Defined width/height
  gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
  group (fold landing rows into Leaders), gate Text vs Block on content
  type, full 9-value attachment label set, resolve handle-backed rows
  (style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
  style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
  vertical text pos, overall scale); Current Vertex navigator; combined
  Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
  transparency by name. Add shared num_prop helper for conditionally
  editable numeric rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-12 18:01:44 +03:00
commit e3d5edf9b7
7 changed files with 699 additions and 373 deletions

View file

@ -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<String> = 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<String> = 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 {

View file

@ -193,6 +193,18 @@ pub fn ro_prop(label: &'static str, field: &'static str, value: impl Into<String
}
}
/// A numeric row that is an editable box when `editable`, otherwise a grayed
/// read-only value using the same length formatting. Used where a field's
/// editability depends on entity state (e.g. a text point that is only live for
/// certain justifications, or an MText column dimension).
pub fn num_prop(label: &'static str, field: &'static str, value: f64, editable: bool) -> 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(

View file

@ -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<PropSection> {
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<PropSection> {
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<f64> { 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;
}
}

View file

@ -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<AttachmentPoint> {
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<AttachmentPoint> {
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<PropSection> {
// 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<PropSection> {
),
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<PropSection> {
.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<PropSection> {
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;
}
}
_ => {}
}
}

View file

@ -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 08 (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<acadrust::Handle>) -> String {
fn properties(ml: &MultiLeader) -> Vec<PropSection> {
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<PropSection> {
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<PropSection> {
],
};
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,
}
}

View file

@ -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<GripDef> {
}
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
// 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<PropSection> {
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;
}

View file

@ -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<PropSection> {
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::Seqend(_)
| EntityType::Leader(_)
| EntityType::Unknown(_)
) {
return None;