From 4d2471cafd6d8f545c65dfa33082276f90629e11 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 3 Jul 2026 08:06:00 +0300 Subject: [PATCH] feat(properties): audit editable vs read-only across all entities Reclassify every property row to match standard palette behaviour: a row is editable only when its value maps directly to a stored field AND its apply_geom_prop handler is wired; computed/derived/count/handle/ cross-object values stay read-only. Newly editable (all apply-wired): attribute tag/prompt; ellipse major radius; hatch Double; lwpolyline closed/linetype-gen; mline style; multileader annotative/landing/background-fill; polyline 2D+3D vertex coords/segment widths/linetype-gen; shape style; spline control/fit point coords, weight, fit tolerance, tangents; table break-option flags; text alignment point, upside-down, backward; underlay display flags. Corrections: spline Degree -> read-only (editing desynced the knot vector); removed a duplicate leader "Arrowhead" row. Co-Authored-By: Claude Opus 4.8 --- src/entities/attribute.rs | 30 +++++++++- src/entities/ellipse.rs | 11 +++- src/entities/hatch.rs | 17 +++++- src/entities/leader.rs | 1 - src/entities/lwpolyline.rs | 47 +++++++++++---- src/entities/mline.rs | 10 +++- src/entities/multileader.rs | 25 ++++---- src/entities/polyline.rs | 116 +++++++++++++++++++++++++++--------- src/entities/shape.rs | 17 +++++- src/entities/spline.rs | 89 +++++++++++++++++++-------- src/entities/table.rs | 64 ++++++++++++-------- src/entities/text.rs | 71 ++++++++++++++++------ src/entities/underlay.rs | 109 +++++++++++++++++++++++++++++---- 13 files changed, 464 insertions(+), 143 deletions(-) diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index 5c8b6220c..cb129822e 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -386,8 +386,16 @@ impl Grippable for AttributeDefinition { impl PropertyEditable for AttributeDefinition { fn geometry_properties(&self, text_style_names: &[String]) -> Vec { let mut text_props = vec![ - ro("Tag", "att_tag", self.tag.clone()), - ro("Prompt", "att_prompt", self.prompt.clone()), + Property { + label: "Tag".into(), + field: "att_tag", + value: PropValue::EditText(self.tag.clone()), + }, + Property { + label: "Prompt".into(), + field: "att_prompt", + value: PropValue::EditText(self.prompt.clone()), + }, Property { label: "Value".into(), field: "att_default", @@ -501,6 +509,14 @@ impl PropertyEditable for AttributeDefinition { fn apply_geom_prop(&mut self, field: &str, value: &str) { // String / enum fields first. + if field == "att_tag" { + self.tag = value.to_string(); + return; + } + if field == "att_prompt" { + self.prompt = value.to_string(); + return; + } if field == "att_default" { if !self.flags.constant { self.default_value = value.to_string(); @@ -645,7 +661,11 @@ impl PropertyEditable for AttributeEntity { PropSection { title: "Text".into(), props: vec![ - ro("Tag", "atte_tag", self.tag.clone()), + Property { + label: "Tag".into(), + field: "atte_tag", + value: PropValue::EditText(self.tag.clone()), + }, ro("Prompt", "atte_prompt", String::new()), Property { label: "Value".into(), @@ -746,6 +766,10 @@ impl PropertyEditable for AttributeEntity { } fn apply_geom_prop(&mut self, field: &str, value: &str) { + if field == "atte_tag" { + self.tag = value.to_string(); + return; + } if field == "atte_val" { if !self.flags.constant { self.value = value.to_string(); diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 8a0bb3175..bc7db2abe 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -197,7 +197,7 @@ fn properties(ell: &Ellipse) -> Vec { ro("End X", "end_x", format!("{:.4}", end.x)), ro("End Y", "end_y", format!("{:.4}", end.y)), ro("End Z", "end_z", format!("{:.4}", end.z)), - ro("Major radius", "major_r", format!("{r_major:.4}")), + edit("Major radius", "major_r", r_major), ro("Minor radius", "minor_r", format!("{r_minor:.4}")), edit("Radius ratio", "ratio", ell.minor_axis_ratio), ro("Start angle", "start_angle", format!("{start_angle:.2}")), @@ -227,6 +227,15 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) { "center_x" => ell.center.x = v, "center_y" => ell.center.y = v, "center_z" => ell.center.z = v, + "major_r" => { + let cur = ell.major_axis_length(); + if cur > 1e-12 && v > 0.0 { + let s = v / cur; + ell.major_axis.x *= s; + ell.major_axis.y *= s; + ell.major_axis.z *= s; + } + } "ratio" => ell.minor_axis_ratio = v, "start_param" => ell.start_parameter = v, "end_param" => ell.end_parameter = v, diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index c06f80dba..7c7f45af9 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -149,7 +149,14 @@ fn properties(h: &Hatch) -> Vec { if h.is_associative { "Yes" } else { "No" }, ), ro("Layer override", "layer_override", String::new()), - ro("Double", "double", if h.is_double { "Yes" } else { "No" }), + Property { + label: "Double".into(), + field: "double", + value: PropValue::BoolToggle { + field: "double", + value: h.is_double, + }, + }, ro( "Spacing", "spacing", @@ -186,6 +193,14 @@ fn properties(h: &Hatch) -> Vec { } fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { + if field == "double" { + h.is_double = if value == "toggle" { + !h.is_double + } else { + value == "true" + }; + return; + } let Some(v) = parse_f64(value) else { return; }; diff --git a/src/entities/leader.rs b/src/entities/leader.rs index d6f9d032b..77821f8ee 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -209,7 +209,6 @@ fn properties(leader: &Leader) -> Vec { creation_type_str(&leader.creation_type), &["With Text", "With Tolerance", "With Block", "No Annotation"], ), - bool_toggle("Arrowhead", "arrow_enabled", leader.arrow_enabled), Property { label: "Dim style".into(), field: "dimension_style", diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index a83b82693..ab1d6785b 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -7,7 +7,7 @@ use crate::entities::common::{ }; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; const TAU: f64 = std::f64::consts::TAU; @@ -344,22 +344,47 @@ fn properties(pline: &LwPolyline) -> Vec { PropSection { title: "Misc".into(), props: vec![ - ro( - "Closed", - "closed", - if pline.is_closed { "Yes" } else { "No" }, - ), - ro( - "Linetype generation", - "plinegen", - if pline.plinegen { "Yes" } else { "No" }, - ), + Property { + label: "Closed".into(), + field: "closed", + value: PropValue::BoolToggle { + field: "closed", + value: pline.is_closed, + }, + }, + Property { + label: "Linetype generation".into(), + field: "plinegen", + value: PropValue::BoolToggle { + field: "plinegen", + value: pline.plinegen, + }, + }, ], }, ] } fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { + match field { + "closed" => { + pline.is_closed = if value == "toggle" { + !pline.is_closed + } else { + value == "true" + }; + return; + } + "plinegen" => { + pline.plinegen = if value == "toggle" { + !pline.plinegen + } else { + value == "true" + }; + return; + } + _ => {} + } let Some(v) = parse_f64(value) else { return; }; diff --git a/src/entities/mline.rs b/src/entities/mline.rs index 48b20bb66..6f1b3bb63 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -211,7 +211,11 @@ impl PropertyEditable for MLine { PropSection { title: "Misc".into(), props: vec![ - ro("Style", "ml_style", self.style_name.clone()), + Property { + label: "Style".into(), + field: "ml_style", + value: PropValue::EditText(self.style_name.clone()), + }, Property { label: "Style justification".into(), field: "ml_justification", @@ -249,6 +253,10 @@ impl PropertyEditable for MLine { }; return; } + "ml_style" => { + self.style_name = value.to_string(); + return; + } _ => {} } let Ok(v) = value.trim().parse::() else { diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 881f620ef..0377b0008 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -543,10 +543,10 @@ fn properties(ml: &MultiLeader) -> Vec { &["None", "MText", "Block", "Tolerance"], ), edit("Overall scale", "scale_factor", ml.scale_factor), - ro( + bool_toggle( "Annotative", "enable_annotation_scale", - if ml.enable_annotation_scale { "Yes" } else { "No" }, + ml.enable_annotation_scale, ), // No stored annotative-scale name on the entity. ro("Annotative scale", "annotative_scale", String::new()), @@ -588,11 +588,7 @@ fn properties(ml: &MultiLeader) -> Vec { 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()), - ro( - "Landing", - "enable_landing", - if ml.enable_landing { "Yes" } else { "No" }, - ), + bool_toggle("Landing", "enable_landing", ml.enable_landing), edit( "Landing distance", "landing_distance", @@ -652,14 +648,10 @@ fn properties(ml: &MultiLeader) -> Vec { format!("{:?}", ml.text_attachment_direction), ), edit("Landing gap", "landing_gap", ctx.landing_gap), - ro( + bool_toggle( "Background mask", "background_fill_enabled", - if ctx.background_fill_enabled { - "Yes" - } else { - "No" - }, + ctx.background_fill_enabled, ), ], }; @@ -771,6 +763,13 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { value == "true" } } + "background_fill_enabled" => { + ml.context.background_fill_enabled = if value == "toggle" { + !ml.context.background_fill_enabled + } else { + value == "true" + } + } "dogleg_length" => { if let Some(v) = f64(value) { ml.dogleg_length = v; diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 0f1ea8d8e..6f526890f 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -400,20 +400,20 @@ impl PropertyEditable for Polyline2D { area = (area * 0.5).abs(); let v0 = self.vertices.first(); - let vertex_x = v0.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(); - let vertex_y = v0.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(); - let seg_start_w = v0.map(|v| format!("{:.4}", v.start_width)).unwrap_or_default(); - let seg_end_w = v0.map(|v| format!("{:.4}", v.end_width)).unwrap_or_default(); + let vertex_x = v0.map(|v| v.location.x).unwrap_or_default(); + let vertex_y = v0.map(|v| v.location.y).unwrap_or_default(); + let seg_start_w = v0.map(|v| v.start_width).unwrap_or_default(); + let seg_end_w = v0.map(|v| v.end_width).unwrap_or_default(); vec![ PropSection { title: "Geometry".into(), props: vec![ ro("Current Vertex", "pl2_current_vertex", if n > 0 { "1" } else { "" }), - ro("Vertex X", "pl2_vertex_x", vertex_x), - ro("Vertex Y", "pl2_vertex_y", vertex_y), - ro("Start segment width", "pl2_seg_start_w", seg_start_w), - ro("End segment width", "pl2_seg_end_w", seg_end_w), + edit("Vertex X", "pl2_vertex_x", vertex_x), + edit("Vertex Y", "pl2_vertex_y", vertex_y), + edit("Start segment width", "pl2_seg_start_w", seg_start_w), + edit("End segment width", "pl2_seg_end_w", seg_end_w), edit("Global width", "pl2_start_w", self.start_width), edit("Elevation", "pl2_elevation", self.elevation), ro("Area", "pl2_area", format!("{area:.4}")), @@ -431,11 +431,14 @@ impl PropertyEditable for Polyline2D { value: self.is_closed(), }, }, - ro( - "Linetype generation", - "pl2_ltype_gen", - if self.flags.bits() & 128 != 0 { "Enabled" } else { "Disabled" }, - ), + Property { + label: "Linetype generation".into(), + field: "pl2_ltype_gen", + value: PropValue::BoolToggle { + field: "pl2_ltype_gen", + value: self.flags.bits() & 128 != 0, + }, + }, ], }, ] @@ -468,6 +471,43 @@ impl PropertyEditable for Polyline2D { } } } + "pl2_vertex_x" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + vert.location.x = v; + } + } + "pl2_vertex_y" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + vert.location.y = v; + } + } + "pl2_seg_start_w" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + if v >= 0.0 { + vert.start_width = v; + } + } + } + "pl2_seg_end_w" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + if v >= 0.0 { + vert.end_width = v; + } + } + } + "pl2_ltype_gen" => { + let on = if value == "toggle" { + self.flags.bits() & 128 == 0 + } else { + value == "true" + }; + let bits = if on { + self.flags.bits() | 128 + } else { + self.flags.bits() & !128 + }; + self.flags = acadrust::entities::polyline::PolylineFlags::from_bits(bits); + } _ => {} } } @@ -616,9 +656,9 @@ impl PropertyEditable for Polyline3D { use acadrust::entities::polyline3d::SmoothSurfaceType as SST; let n = self.vertices.len(); let v0 = self.vertices.first(); - let vertex_x = v0.map(|v| format!("{:.4}", v.position.x)).unwrap_or_default(); - let vertex_y = v0.map(|v| format!("{:.4}", v.position.y)).unwrap_or_default(); - let vertex_z = v0.map(|v| format!("{:.4}", v.position.z)).unwrap_or_default(); + let vertex_x = v0.map(|v| v.position.x).unwrap_or_default(); + let vertex_y = v0.map(|v| v.position.y).unwrap_or_default(); + let vertex_z = v0.map(|v| v.position.z).unwrap_or_default(); let fit_smooth = match self.smooth_type { SST::None => "None", SST::QuadraticBSpline => "Quadratic", @@ -631,9 +671,9 @@ impl PropertyEditable for Polyline3D { title: "Geometry".into(), props: vec![ ro("Vertex", "pl3_vertex", if n > 0 { "1" } else { "" }), - ro("Vertex X", "pl3_vertex_x", vertex_x), - ro("Vertex Y", "pl3_vertex_y", vertex_y), - ro("Vertex Z", "pl3_vertex_z", vertex_z), + edit("Vertex X", "pl3_vertex_x", vertex_x), + edit("Vertex Y", "pl3_vertex_y", vertex_y), + edit("Vertex Z", "pl3_vertex_z", vertex_z), ], }, PropSection { @@ -654,17 +694,35 @@ impl PropertyEditable for Polyline3D { } fn apply_geom_prop(&mut self, field: &str, value: &str) { - if field == "pl3_closed" { - let closed = if value == "toggle" { - !self.is_closed() - } else { - value == "true" - }; - if closed { - self.close(); - } else { - self.open(); + match field { + "pl3_closed" => { + let closed = if value == "toggle" { + !self.is_closed() + } else { + value == "true" + }; + if closed { + self.close(); + } else { + self.open(); + } } + "pl3_vertex_x" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + vert.position.x = v; + } + } + "pl3_vertex_y" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + vert.position.y = v; + } + } + "pl3_vertex_z" => { + if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + vert.position.z = v; + } + } + _ => {} } } } diff --git a/src/entities/shape.rs b/src/entities/shape.rs index 4d59d179c..12e419418 100644 --- a/src/entities/shape.rs +++ b/src/entities/shape.rs @@ -11,7 +11,7 @@ use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue}; use crate::scene::view::transform; use crate::scene::model::wire_model::SnapHint; @@ -118,7 +118,7 @@ impl Grippable for Shape { // ── PropertyEditable ────────────────────────────────────────────────────────── impl PropertyEditable for Shape { - fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + fn geometry_properties(&self, text_style_names: &[String]) -> Vec { let style_handle_display = match self.style_handle { Some(h) if !h.is_null() => format!("{:X}", h.value()), _ => "(none)".to_string(), @@ -128,7 +128,14 @@ impl PropertyEditable for Shape { props: vec![ ro("Name", "shp_name", self.shape_name.clone()), ro("Number", "shp_number", self.shape_number.to_string()), - ro("Style", "shp_style", self.style_name.clone()), + Property { + label: "Style".into(), + field: "shp_style", + value: PropValue::Choice { + selected: self.style_name.clone(), + options: text_style_names.to_vec(), + }, + }, ro("Style Handle", "shp_style_handle", style_handle_display), edit("Insert X", "shp_ix", self.insertion_point.x), edit("Insert Y", "shp_iy", self.insertion_point.y), @@ -146,6 +153,10 @@ impl PropertyEditable for Shape { } fn apply_geom_prop(&mut self, field: &str, value: &str) { + if field == "shp_style" { + self.style_name = value.to_string(); + return; + } let Ok(v) = value.trim().parse::() else { return; }; diff --git a/src/entities/spline.rs b/src/entities/spline.rs index a6d608a8c..1b1cc9633 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -162,29 +162,29 @@ fn properties(spline: &Spline) -> Vec { title: "Data Points".into(), props: vec![ ro("Show", "show", show), - edit("Degree", "degree", spline.degree as f64), + ro("Degree", "degree", spline.degree.to_string()), ro( "Control Point Count", "ctrl_pt_count", spline.control_points.len().to_string(), ), ro("Control Point", "ctrl_pt_index", "0"), - ro( + edit( "Control Point X", "ctrl_pt_x", - format!("{:.4}", cp0.map(|p| p.x).unwrap_or(0.0)), + cp0.map(|p| p.x).unwrap_or(0.0), ), - ro( + edit( "Control Point Y", "ctrl_pt_y", - format!("{:.4}", cp0.map(|p| p.y).unwrap_or(0.0)), + cp0.map(|p| p.y).unwrap_or(0.0), ), - ro( + edit( "Control Point Z", "ctrl_pt_z", - format!("{:.4}", cp0.map(|p| p.z).unwrap_or(0.0)), + cp0.map(|p| p.z).unwrap_or(0.0), ), - ro("Weight", "weight", format!("{:.4}", w0.unwrap_or(1.0))), + edit("Weight", "weight", w0.unwrap_or(1.0)), ro("Knot Parameterization", "knot_param", knot_param), ro( "Fit Point Count", @@ -192,28 +192,28 @@ fn properties(spline: &Spline) -> Vec { spline.fit_points.len().to_string(), ), ro("Fit Point", "fit_pt_index", "0"), - ro( + edit( "Fit Point X", "fit_pt_x", - format!("{:.4}", fp0.map(|p| p.x).unwrap_or(0.0)), + fp0.map(|p| p.x).unwrap_or(0.0), ), - ro( + edit( "Fit Point Y", "fit_pt_y", - format!("{:.4}", fp0.map(|p| p.y).unwrap_or(0.0)), + fp0.map(|p| p.y).unwrap_or(0.0), ), - ro( + edit( "Fit Point Z", "fit_pt_z", - format!("{:.4}", fp0.map(|p| p.z).unwrap_or(0.0)), + fp0.map(|p| p.z).unwrap_or(0.0), ), - ro("Fit Tolerance", "fit_tolerance", format!("{:.6}", spline.fit_tolerance)), - ro("Start Tangent X", "start_tan_x", format!("{:.4}", spline.begin_tangent.x)), - ro("Start Tangent Y", "start_tan_y", format!("{:.4}", spline.begin_tangent.y)), - ro("Start Tangent Z", "start_tan_z", format!("{:.4}", spline.begin_tangent.z)), - ro("End Tangent X", "end_tan_x", format!("{:.4}", spline.end_tangent.x)), - ro("End Tangent Y", "end_tan_y", format!("{:.4}", spline.end_tangent.y)), - ro("End Tangent Z", "end_tan_z", format!("{:.4}", spline.end_tangent.z)), + edit("Fit Tolerance", "fit_tolerance", spline.fit_tolerance), + edit("Start Tangent X", "start_tan_x", spline.begin_tangent.x), + edit("Start Tangent Y", "start_tan_y", spline.begin_tangent.y), + edit("Start Tangent Z", "start_tan_z", spline.begin_tangent.z), + edit("End Tangent X", "end_tan_x", spline.end_tangent.x), + edit("End Tangent Y", "end_tan_y", spline.end_tangent.y), + edit("End Tangent Z", "end_tan_z", spline.end_tangent.z), ], }, PropSection { @@ -229,10 +229,51 @@ fn properties(spline: &Spline) -> Vec { } fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { - if field == "degree" { - if let Some(v) = parse_f64(value) { - spline.degree = v.round() as i32; + let Some(v) = parse_f64(value) else { return }; + match field { + "ctrl_pt_x" => { + if let Some(cp) = spline.control_points.first_mut() { + cp.x = v; + } } + "ctrl_pt_y" => { + if let Some(cp) = spline.control_points.first_mut() { + cp.y = v; + } + } + "ctrl_pt_z" => { + if let Some(cp) = spline.control_points.first_mut() { + cp.z = v; + } + } + "weight" => { + if let Some(w) = spline.weights.first_mut() { + *w = v; + } + } + "fit_pt_x" => { + if let Some(fp) = spline.fit_points.first_mut() { + fp.x = v; + } + } + "fit_pt_y" => { + if let Some(fp) = spline.fit_points.first_mut() { + fp.y = v; + } + } + "fit_pt_z" => { + if let Some(fp) = spline.fit_points.first_mut() { + fp.z = v; + } + } + "fit_tolerance" => spline.fit_tolerance = v, + "start_tan_x" => spline.begin_tangent.x = v, + "start_tan_y" => spline.begin_tangent.y = v, + "start_tan_z" => spline.begin_tangent.z = v, + "end_tan_x" => spline.end_tangent.x = v, + "end_tan_y" => spline.end_tangent.y = v, + "end_tan_z" => spline.end_tangent.z = v, + _ => {} } } diff --git a/src/entities/table.rs b/src/entities/table.rs index 5f0a1fa6d..a6b9f01d4 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -8,7 +8,7 @@ use crate::entities::text_support::{ }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue}; use crate::scene::view::transform; use crate::scene::model::wire_model::SnapHint; @@ -828,8 +828,12 @@ impl PropertyEditable for Table { _ => "(none)".to_string(), } }; - let yes_no = |b: bool| -> String { - if b { "Yes".into() } else { "No".into() } + let toggle = |label: &'static str, field: &'static str, b: bool| -> Property { + Property { + label: label.into(), + field, + value: PropValue::BoolToggle { field, value: b }, + } }; // Direction = angle of the horizontal direction vector in the XY plane. let direction_deg = @@ -862,47 +866,39 @@ impl PropertyEditable for Table { PropSection { title: "Table Breaks".into(), props: vec![ - ro( + toggle( "Enabled", "tbl_break_enabled", - yes_no(self.break_options.contains(BreakOptionFlags::ENABLE_BREAKS)), + self.break_options.contains(BreakOptionFlags::ENABLE_BREAKS), ), ro( "Direction", "tbl_break_direction", format!("{:?}", self.break_flow_direction), ), - ro( + toggle( "Repeat top labels", "tbl_break_repeat_top", - yes_no( - self.break_options - .contains(BreakOptionFlags::REPEAT_TOP_LABELS), - ), + self.break_options + .contains(BreakOptionFlags::REPEAT_TOP_LABELS), ), - ro( + toggle( "Repeat bottom labels", "tbl_break_repeat_bottom", - yes_no( - self.break_options - .contains(BreakOptionFlags::REPEAT_BOTTOM_LABELS), - ), + self.break_options + .contains(BreakOptionFlags::REPEAT_BOTTOM_LABELS), ), - ro( + toggle( "Manual positions", "tbl_break_manual_positions", - yes_no( - self.break_options - .contains(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), - ), + self.break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), ), - ro( + toggle( "Manual heights", "tbl_break_manual_heights", - yes_no( - self.break_options - .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), - ), + self.break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), ), ro("Break height", "tbl_break_height", String::new()), edit("Spacing", "tbl_break_spacing", self.break_spacing), @@ -913,6 +909,24 @@ impl PropertyEditable for Table { fn apply_geom_prop(&mut self, field: &str, value: &str) { use crate::entities::common::parse_f64; + use acadrust::entities::table::BreakOptionFlags; + let flag = match field { + "tbl_break_enabled" => Some(BreakOptionFlags::ENABLE_BREAKS), + "tbl_break_repeat_top" => Some(BreakOptionFlags::REPEAT_TOP_LABELS), + "tbl_break_repeat_bottom" => Some(BreakOptionFlags::REPEAT_BOTTOM_LABELS), + "tbl_break_manual_positions" => Some(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), + "tbl_break_manual_heights" => Some(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), + _ => None, + }; + if let Some(flag) = flag { + let on = if value == "toggle" { + !self.break_options.contains(flag) + } else { + value == "true" + }; + self.break_options.set(flag, on); + return; + } if field == "tbl_break_spacing" { if let Some(v) = parse_f64(value) { self.break_spacing = v; diff --git a/src/entities/text.rs b/src/entities/text.rs index c7f4a5af3..3b5e5ba8c 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -222,9 +222,9 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec { edit("Rotation", "rotation", t.rotation.to_degrees()), edit("Width factor", "width_factor", t.width_factor), edit("Obliquing", "oblique_angle", t.oblique_angle.to_degrees()), - ro("Text alignment X", "align_x", format!("{:.4}", ap.x)), - ro("Text alignment Y", "align_y", format!("{:.4}", ap.y)), - ro("Text alignment Z", "align_z", format!("{:.4}", ap.z)), + edit("Text alignment X", "align_x", ap.x), + edit("Text alignment Y", "align_y", ap.y), + edit("Text alignment Z", "align_z", ap.z), ], }, PropSection { @@ -238,24 +238,22 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec { PropSection { title: "Misc".into(), props: vec![ - ro( - "Upside down", - "upside_down", - if t.generation_flags & 0x4 != 0 { - "Yes" - } else { - "No" + Property { + label: "Upside down".into(), + field: "upside_down", + value: PropValue::BoolToggle { + field: "upside_down", + value: t.generation_flags & 0x4 != 0, }, - ), - ro( - "Backward", - "backward", - if t.generation_flags & 0x2 != 0 { - "Yes" - } else { - "No" + }, + Property { + label: "Backward".into(), + field: "backward", + value: PropValue::BoolToggle { + field: "backward", + value: t.generation_flags & 0x2 != 0, }, - ), + }, ], }, ] @@ -295,6 +293,32 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) { sync_text_alignment_point(t); return; } + "upside_down" => { + let set = if value == "toggle" { + t.generation_flags & 0x4 == 0 + } else { + value == "true" + }; + if set { + t.generation_flags |= 0x4; + } else { + t.generation_flags &= !0x4; + } + return; + } + "backward" => { + let set = if value == "toggle" { + t.generation_flags & 0x2 == 0 + } else { + value == "true" + }; + if set { + t.generation_flags |= 0x2; + } else { + t.generation_flags &= !0x2; + } + return; + } _ => {} } let Some(v) = parse_f64(value) else { @@ -304,6 +328,15 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) { "ins_x" => t.insertion_point.x = v, "ins_y" => t.insertion_point.y = v, "ins_z" => t.insertion_point.z = v, + "align_x" | "align_y" | "align_z" => { + let ins = t.insertion_point; + let ap = t.alignment_point.get_or_insert(ins); + match field { + "align_x" => ap.x = v, + "align_y" => ap.y = v, + _ => ap.z = v, + } + } "height" if v > 0.0 => t.height = v, "rotation" => t.rotation = v.to_radians(), "width_factor" if v > 0.0 => t.width_factor = v, diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 49bf80d3b..6c3f6eff3 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -10,7 +10,7 @@ use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -147,8 +147,6 @@ impl Grippable for Underlay { impl PropertyEditable for Underlay { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let yes_no = |b: bool| if b { "Yes" } else { "No" }; - // Width / Height derived from the world-space clip boundary bounds // (the only in-entity source of the placed footprint size). let (width, height) = if self.clip_boundary_vertices.is_empty() { @@ -193,12 +191,22 @@ impl PropertyEditable for Underlay { props: vec![ edit("Contrast", "ul_contrast", self.contrast as f64), edit("Fade", "ul_fade", self.fade as f64), - ro("Monochrome", "ul_mono", yes_no(monochrome)), - ro( - "Adjust Colors for Background", - "ul_adjust_bg", - yes_no(adjust_bg), - ), + Property { + label: "Monochrome".into(), + field: "ul_mono", + value: PropValue::BoolToggle { + field: "ul_mono", + value: monochrome, + }, + }, + Property { + label: "Adjust Colors for Background".into(), + field: "ul_adjust_bg", + value: PropValue::BoolToggle { + field: "ul_adjust_bg", + value: adjust_bg, + }, + }, ], }, PropSection { @@ -208,9 +216,30 @@ impl PropertyEditable for Underlay { // UnderlayDefinition object, not reachable from the entity. ro("Underlay name", "ul_name", String::new()), ro("Underlay path", "ul_path", String::new()), - ro("Show underlay", "ul_on", yes_no(show)), - ro("Clipping", "ul_clip", yes_no(clipping)), - ro("Show clipped", "ul_clip_inverted", yes_no(self.clip_inverted)), + Property { + label: "Show underlay".into(), + field: "ul_on", + value: PropValue::BoolToggle { + field: "ul_on", + value: show, + }, + }, + Property { + label: "Clipping".into(), + field: "ul_clip", + value: PropValue::BoolToggle { + field: "ul_clip", + value: clipping, + }, + }, + Property { + label: "Show clipped".into(), + field: "ul_clip_inverted", + value: PropValue::BoolToggle { + field: "ul_clip_inverted", + value: self.clip_inverted, + }, + }, ro("Underlay layers", "ul_layers", String::new()), ], }, @@ -218,6 +247,62 @@ impl PropertyEditable for Underlay { } fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "ul_mono" => { + let on = if value == "toggle" { + !self.flags.contains(UnderlayDisplayFlags::MONOCHROME) + } else { + value == "true" + }; + self.set_monochrome(on); + return; + } + "ul_adjust_bg" => { + let on = if value == "toggle" { + !self.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND) + } else { + value == "true" + }; + if on { + self.flags |= UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND; + } else { + self.flags -= UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND; + } + return; + } + "ul_on" => { + let on = if value == "toggle" { + !self.flags.contains(UnderlayDisplayFlags::ON) + } else { + value == "true" + }; + self.set_on(on); + return; + } + "ul_clip" => { + let on = if value == "toggle" { + !self.flags.contains(UnderlayDisplayFlags::CLIPPING) + } else { + value == "true" + }; + if on { + self.flags |= UnderlayDisplayFlags::CLIPPING; + } else { + self.flags -= UnderlayDisplayFlags::CLIPPING; + } + return; + } + "ul_clip_inverted" => { + let on = if value == "toggle" { + !self.clip_inverted + } else { + value == "true" + }; + self.clip_inverted = on; + return; + } + _ => {} + } if let Ok(v) = value.trim().parse::() { match field { "ul_ix" => self.insertion_point.x = v,