From b9cebab951ac2d17e716208b9a89ccecd1dfd071 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Jul 2026 23:03:18 +0300 Subject: [PATCH] feat(properties): conform entity property groups to PROPERTIES.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite each entity's property builder to the per-entity group/row spec: the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders, Block, …) with the spec's rows and real computed values — Diameter, Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio, etc. — plus apply routing for the editable rows. Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines, Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid, Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport, Attribute, OLE). The 3D Visualization/Material group and the full Dimension spec are deferred (they need document access for material/dimstyle name resolution). Rows whose data is not in the pinned acadrust revision are shown as empty placeholders, to be filled when the dependency is bumped. Co-Authored-By: Claude Opus 4.8 --- src/entities/arc.rs | 54 ++++- src/entities/attribute.rs | 320 +++++++++++++++-------------- src/entities/circle.rs | 21 +- src/entities/ellipse.rs | 101 +++++++--- src/entities/hatch.rs | 210 ++++++++++++++----- src/entities/insert.rs | 67 ++++-- src/entities/leader.rs | 180 +++++++---------- src/entities/line.rs | 8 + src/entities/lwpolyline.rs | 75 +++++-- src/entities/mesh.rs | 205 +++++++++++-------- src/entities/mline.rs | 91 +++++---- src/entities/mtext.rs | 164 +++++++++------ src/entities/multileader.rs | 381 +++++++++++++++++------------------ src/entities/ole2frame.rs | 61 +++--- src/entities/point.rs | 6 +- src/entities/polyline.rs | 164 +++++++-------- src/entities/raster_image.rs | 245 ++++++++++++++-------- src/entities/ray.rs | 18 +- src/entities/solid.rs | 30 +-- src/entities/solid3d.rs | 173 ++++------------ src/entities/spline.rs | 117 +++++++++-- src/entities/table.rs | 188 +++++++++-------- src/entities/text.rs | 140 ++++++++----- src/entities/tolerance.rs | 80 ++++---- src/entities/underlay.rs | 118 ++++++----- src/entities/viewport.rs | 258 +++++++----------------- 26 files changed, 1917 insertions(+), 1558 deletions(-) diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 0719806e9..a1aa18c88 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -2,7 +2,9 @@ use acadrust::entities::Arc; use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, square_grip}; +use crate::entities::common::{ + center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, +}; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; @@ -122,6 +124,31 @@ fn grips(arc: &Arc) -> Vec { } fn properties(arc: &Arc) -> Vec { + let r = arc.radius; + let sa = arc.start_angle; + let ea = arc.end_angle; + let sweep = (ea - sa).rem_euclid(TAU); + let total_angle = sweep.to_degrees(); + let arc_length = r * sweep; + let area = 0.5 * r * r * sweep; + + let normal = (arc.normal.x, arc.normal.y, arc.normal.z); + let (ax, ay) = crate::scene::view::transform::ocs_axes(normal); + let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs( + (arc.center.x, arc.center.y, arc.center.z), + normal, + ); + let arc_pt = |a: f64| { + let (c, s) = (a.cos(), a.sin()); + ( + cwx + r * c * ax.0 + r * s * ay.0, + cwy + r * c * ax.1 + r * s * ay.1, + cwz + r * c * ax.2 + r * s * ay.2, + ) + }; + let (sx, sy, sz) = arc_pt(sa); + let (ex, ey, ez) = arc_pt(ea); + vec![PropSection { title: "Geometry".into(), props: vec![ @@ -129,12 +156,21 @@ fn properties(arc: &Arc) -> Vec { edit("Center Y", "center_y", arc.center.y), edit("Center Z", "center_z", arc.center.z), edit("Radius", "radius", arc.radius), - edit( - "Start Angle (deg)", - "start_angle", - arc.start_angle.to_degrees(), - ), - edit("End Angle (deg)", "end_angle", arc.end_angle.to_degrees()), + edit("Start angle", "start_angle", sa.to_degrees()), + edit("End angle", "end_angle", ea.to_degrees()), + ro("Total angle", "total_angle", format!("{total_angle:.2}")), + ro("Arc length", "arc_length", format!("{arc_length:.4}")), + ro("Area", "area", format!("{area:.4}")), + ro("Start X", "start_x", format!("{sx:.4}")), + ro("Start Y", "start_y", format!("{sy:.4}")), + ro("Start Z", "start_z", format!("{sz:.4}")), + ro("End X", "end_x", format!("{ex:.4}")), + ro("End Y", "end_y", format!("{ey:.4}")), + ro("End Z", "end_z", format!("{ez:.4}")), + edit("Normal X", "normal_x", arc.normal.x), + edit("Normal Y", "normal_y", arc.normal.y), + edit("Normal Z", "normal_z", arc.normal.z), + edit("Thickness", "thickness", arc.thickness), ], }] } @@ -150,6 +186,10 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) { "radius" if v > 0.0 => arc.radius = v, "start_angle" => arc.start_angle = v.to_radians(), "end_angle" => arc.end_angle = v.to_radians(), + "normal_x" => arc.normal.x = v, + "normal_y" => arc.normal.y = v, + "normal_z" => arc.normal.z = v, + "thickness" => arc.thickness = v, _ => {} } } diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index be207379b..5c8b6220c 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -385,26 +385,33 @@ impl Grippable for AttributeDefinition { impl PropertyEditable for AttributeDefinition { fn geometry_properties(&self, text_style_names: &[String]) -> Vec { - let mut props = vec![ + let mut text_props = vec![ ro("Tag", "att_tag", self.tag.clone()), ro("Prompt", "att_prompt", self.prompt.clone()), Property { - label: "Default".into(), + label: "Value".into(), field: "att_default", value: PropValue::EditText(self.default_value.clone()), }, - edit("Insert X", "att_ix", self.insertion_point.x), - edit("Insert Y", "att_iy", self.insertion_point.y), - edit("Insert Z", "att_iz", self.insertion_point.z), - edit("Align X", "att_ax", self.alignment_point.x), - edit("Align Y", "att_ay", self.alignment_point.y), - edit("Align Z", "att_az", self.alignment_point.z), - edit("Height", "att_h", self.height), - edit("Rotation", "att_rot", self.rotation.to_degrees()), - edit("Width Factor", "att_wf", self.width_factor), - edit("Oblique Angle", "att_ob", self.oblique_angle.to_degrees()), Property { - label: "H-Align".into(), + label: "Style".into(), + field: "att_style", + value: PropValue::Choice { + selected: if self.text_style.trim().is_empty() { + "Standard".into() + } else { + self.text_style.clone() + }, + options: text_style_names.to_vec(), + }, + }, + ro( + "Annotative", + "att_annotative", + bool_yn(self.flags.annotative), + ), + Property { + label: "Justify".into(), field: "att_halign", value: PropValue::Choice { selected: halign_str(self.horizontal_alignment).to_string(), @@ -425,63 +432,71 @@ impl PropertyEditable for AttributeDefinition { .collect(), }, }, - Property { - label: "Style".into(), - field: "att_style", - value: PropValue::Choice { - selected: if self.text_style.trim().is_empty() { - "Standard".into() - } else { - self.text_style.clone() - }, - options: text_style_names.to_vec(), - }, - }, + edit("Height", "att_h", self.height), + edit("Rotation", "att_rot", self.rotation.to_degrees()), + edit("Width factor", "att_wf", self.width_factor), + edit("Obliquing", "att_ob", self.oblique_angle.to_degrees()), + edit("Text alignment X", "att_ax", self.alignment_point.x), + edit("Text alignment Y", "att_ay", self.alignment_point.y), + edit("Text alignment Z", "att_az", self.alignment_point.z), ro( - "Generation", - "att_gen_flags", - format!("{:#06b}", self.text_generation_flags & 0xff), - ), - ro( - "Field Length", + "Boundary width", "att_field_len", self.field_length.to_string(), ), - Property { - label: "MText Mode".into(), - field: "att_mtext_flag", - value: PropValue::Choice { - selected: mtext_flag_str(self.mtext_flag).to_string(), - options: ["SingleLine", "MultiLine", "ConstantMultiLine"] - .into_iter() - .map(str::to_string) - .collect(), - }, - }, - ro("Multiline", "att_is_multiline", bool_yn(self.is_multiline)), - ro("Line Count", "att_line_count", self.line_count.to_string()), - ro("Lock Position", "att_lock_pos", bool_yn(self.lock_position)), - ro("Invisible", "att_invisible", bool_yn(self.flags.invisible)), - ro("Constant", "att_constant", bool_yn(self.flags.constant)), - ro("Verify", "att_verify", bool_yn(self.flags.verify)), - ro("Preset", "att_preset", bool_yn(self.flags.preset)), ro( - "Annotative", - "att_annotative", - bool_yn(self.flags.annotative), + "Upside down", + "att_upside_down", + bool_yn(self.text_generation_flags & 0x4 != 0), + ), + ro( + "Backward", + "att_backward", + bool_yn(self.text_generation_flags & 0x2 != 0), ), ]; // Constant attributes can't be edited at insert time — surface that - // by marking the Default field read-only. + // by marking the Value field read-only. if self.flags.constant { - if let Some(p) = props.iter_mut().find(|p| p.field == "att_default") { + if let Some(p) = text_props.iter_mut().find(|p| p.field == "att_default") { p.value = PropValue::ReadOnly(self.default_value.clone()); } } - vec![PropSection { - title: "Geometry".into(), - props, - }] + vec![ + PropSection { + title: "Text".into(), + props: text_props, + }, + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "att_ix", self.insertion_point.x), + edit("Position Y", "att_iy", self.insertion_point.y), + edit("Position Z", "att_iz", self.insertion_point.z), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Invisible", "att_invisible", bool_yn(self.flags.invisible)), + ro("Constant", "att_constant", bool_yn(self.flags.constant)), + ro("Verify", "att_verify", bool_yn(self.flags.verify)), + ro("Preset", "att_preset", bool_yn(self.flags.preset)), + ro("Lock position", "att_lock_pos", bool_yn(self.lock_position)), + Property { + label: "Multiple lines".into(), + field: "att_mtext_flag", + value: PropValue::Choice { + selected: mtext_flag_str(self.mtext_flag).to_string(), + options: ["SingleLine", "MultiLine", "ConstantMultiLine"] + .into_iter() + .map(str::to_string) + .collect(), + }, + }, + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { @@ -626,101 +641,108 @@ impl Grippable for AttributeEntity { impl PropertyEditable for AttributeEntity { fn geometry_properties(&self, text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Tag", "atte_tag", self.tag.clone()), - Property { - label: "Value".into(), - field: "atte_val", - value: PropValue::EditText(self.value.clone()), - }, - edit("Insert X", "atte_ix", self.insertion_point.x), - edit("Insert Y", "atte_iy", self.insertion_point.y), - edit("Insert Z", "atte_iz", self.insertion_point.z), - edit("Align X", "atte_ax", self.alignment_point.x), - edit("Align Y", "atte_ay", self.alignment_point.y), - edit("Align Z", "atte_az", self.alignment_point.z), - edit("Height", "atte_h", self.height), - edit("Rotation", "atte_rot", self.rotation.to_degrees()), - edit("Width Factor", "atte_wf", self.width_factor), - edit("Oblique Angle", "atte_ob", self.oblique_angle.to_degrees()), - Property { - label: "H-Align".into(), - field: "atte_halign", - value: PropValue::Choice { - selected: halign_str(self.horizontal_alignment).to_string(), - options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"] - .into_iter() - .map(str::to_string) - .collect(), + vec![ + PropSection { + title: "Text".into(), + props: vec![ + ro("Tag", "atte_tag", self.tag.clone()), + ro("Prompt", "atte_prompt", String::new()), + Property { + label: "Value".into(), + field: "atte_val", + value: PropValue::EditText(self.value.clone()), }, - }, - Property { - label: "V-Align".into(), - field: "atte_valign", - value: PropValue::Choice { - selected: valign_str(self.vertical_alignment).to_string(), - options: ["Baseline", "Bottom", "Middle", "Top"] - .into_iter() - .map(str::to_string) - .collect(), - }, - }, - Property { - label: "Style".into(), - field: "atte_style", - value: PropValue::Choice { - selected: if self.text_style.trim().is_empty() { - "Standard".into() - } else { - self.text_style.clone() + Property { + label: "Style".into(), + field: "atte_style", + value: PropValue::Choice { + selected: if self.text_style.trim().is_empty() { + "Standard".into() + } else { + self.text_style.clone() + }, + options: text_style_names.to_vec(), }, - options: text_style_names.to_vec(), }, - }, - ro( - "Generation", - "atte_gen_flags", - format!("{:#06b}", self.text_generation_flags & 0xff), - ), - ro( - "Field Length", - "atte_field_len", - self.field_length.to_string(), - ), - ro( - "MText Mode", - "atte_mtext_flag", - mtext_flag_str(self.mtext_flag), - ), - ro("Multiline", "atte_is_multiline", bool_yn(self.is_multiline)), - ro("Line Count", "atte_line_count", self.line_count.to_string()), - ro( - "Lock Position", - "atte_lock_pos", - bool_yn(self.lock_position), - ), - ro( - "Definition", - "atte_attdef", - if self.attdef_handle.is_null() { - "(none)".into() - } else { - format!("{:X}", self.attdef_handle.value()) + Property { + label: "Justify".into(), + field: "atte_halign", + value: PropValue::Choice { + selected: halign_str(self.horizontal_alignment).to_string(), + options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"] + .into_iter() + .map(str::to_string) + .collect(), + }, }, - ), - ro("Invisible", "atte_invisible", bool_yn(self.flags.invisible)), - ro("Constant", "atte_constant", bool_yn(self.flags.constant)), - ro("Verify", "atte_verify", bool_yn(self.flags.verify)), - ro("Preset", "atte_preset", bool_yn(self.flags.preset)), - ro( - "Annotative", - "atte_annotative", - bool_yn(self.flags.annotative), - ), - ], - }] + Property { + label: "V-Align".into(), + field: "atte_valign", + value: PropValue::Choice { + selected: valign_str(self.vertical_alignment).to_string(), + options: ["Baseline", "Bottom", "Middle", "Top"] + .into_iter() + .map(str::to_string) + .collect(), + }, + }, + ro( + "Annotative", + "atte_annotative", + bool_yn(self.flags.annotative), + ), + edit("Height", "atte_h", self.height), + edit("Rotation", "atte_rot", self.rotation.to_degrees()), + edit("Width factor", "atte_wf", self.width_factor), + edit("Obliquing", "atte_ob", self.oblique_angle.to_degrees()), + edit("Text alignment X", "atte_ax", self.alignment_point.x), + edit("Text alignment Y", "atte_ay", self.alignment_point.y), + edit("Text alignment Z", "atte_az", self.alignment_point.z), + ro( + "Boundary width", + "atte_field_len", + self.field_length.to_string(), + ), + ], + }, + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "atte_ix", self.insertion_point.x), + edit("Position Y", "atte_iy", self.insertion_point.y), + edit("Position Z", "atte_iz", self.insertion_point.z), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro( + "Upside down", + "atte_upside_down", + bool_yn(self.text_generation_flags & 0x4 != 0), + ), + ro( + "Backward", + "atte_backward", + bool_yn(self.text_generation_flags & 0x2 != 0), + ), + ro("Invisible", "atte_invisible", bool_yn(self.flags.invisible)), + ro( + "Multiple lines", + "atte_mtext_flag", + mtext_flag_str(self.mtext_flag), + ), + ro("Constant", "atte_constant", bool_yn(self.flags.constant)), + ro("Verify", "atte_verify", bool_yn(self.flags.verify)), + ro("Preset", "atte_preset", bool_yn(self.flags.preset)), + ro( + "Lock position", + "atte_lock_pos", + bool_yn(self.lock_position), + ), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 119232b5b..6eafc05d7 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -119,23 +119,26 @@ fn grips(circle: &Circle) -> Vec { } fn properties(circle: &Circle) -> Vec { + use std::f64::consts::PI; + let r = circle.radius; vec![PropSection { title: "Geometry".into(), props: vec![ edit("Center X", "center_x", circle.center.x), edit("Center Y", "center_y", circle.center.y), edit("Center Z", "center_z", circle.center.z), - edit("Radius", "radius", circle.radius), - ro( - "Diameter", - "diameter", - format!("{:.4}", circle.radius * 2.0), - ), + edit("Radius", "radius", r), + ro("Diameter", "diameter", format!("{:.4}", r * 2.0)), ro( "Circumference", "circumference", - format!("{:.4}", circle.radius * 2.0 * std::f64::consts::PI), + format!("{:.4}", 2.0 * PI * r), ), + ro("Area", "area", format!("{:.4}", PI * r * r)), + edit("Normal X", "normal_x", circle.normal.x), + edit("Normal Y", "normal_y", circle.normal.y), + edit("Normal Z", "normal_z", circle.normal.z), + edit("Thickness", "thickness", circle.thickness), ], }] } @@ -149,6 +152,10 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) { "center_y" => circle.center.y = v, "center_z" => circle.center.z = v, "radius" if v > 0.0 => circle.radius = v, + "normal_x" => circle.normal.x = v, + "normal_y" => circle.normal.y = v, + "normal_z" => circle.normal.z = v, + "thickness" => circle.thickness = v, _ => {} } } diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 1ee3170ee..8a0bb3175 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -2,7 +2,9 @@ use acadrust::entities::Ellipse; use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire}; 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, parse_f64, ro_prop as ro, square_grip, +}; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; @@ -159,32 +161,81 @@ fn grips(ell: &Ellipse) -> Vec { } fn properties(ell: &Ellipse) -> Vec { - let r_major = (ell.major_axis.x * ell.major_axis.x - + ell.major_axis.y * ell.major_axis.y - + ell.major_axis.z * ell.major_axis.z) - .sqrt(); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Center X", "center_x", ell.center.x), - edit("Center Y", "center_y", ell.center.y), - edit("Center Z", "center_z", ell.center.z), - ro("Major Radius", "major_r", format!("{r_major:.4}")), - ro( - "Minor Radius", - "minor_r", - format!("{:.4}", r_major * ell.minor_axis_ratio), - ), - ro( - "Minor/Major", - "ratio", - format!("{:.4}", ell.minor_axis_ratio), - ), - ], - }] + use crate::entities::traits::MassPropsCalc; + + let r_major = ell.major_axis_length(); + let r_minor = ell.minor_axis_length(); + + // Axis frame in WCS: major-axis unit vector `u`, minor direction `v = normal × u`. + let u = glam::DVec3::new(ell.major_axis.x, ell.major_axis.y, ell.major_axis.z); + let u = if r_major > 1e-12 { + u / r_major + } else { + glam::DVec3::X + }; + let n = glam::DVec3::new(ell.normal.x, ell.normal.y, ell.normal.z); + let v = n.cross(u); + + // Parametric end point in WCS: center + r_major·cos(t1)·u + r_minor·sin(t1)·v. + let t1 = ell.end_parameter; + let end = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z) + + u * (r_major * t1.cos()) + + v * (r_minor * t1.sin()); + + let start_angle = ell.start_parameter.to_degrees().rem_euclid(360.0); + let end_angle = ell.end_parameter.to_degrees().rem_euclid(360.0); + + let props = ell.mass_props(); + + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Center X", "center_x", ell.center.x), + edit("Center Y", "center_y", ell.center.y), + edit("Center Z", "center_z", ell.center.z), + 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}")), + 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}")), + ro("End angle", "end_angle", format!("{end_angle:.2}")), + edit("Start parameter", "start_param", ell.start_parameter), + edit("End parameter", "end_param", ell.end_parameter), + ro("Area", "area", format!("{:.4}", props.area)), + ro("Length", "length", format!("{:.4}", props.perimeter)), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + edit("Normal X", "normal_x", ell.normal.x), + edit("Normal Y", "normal_y", ell.normal.y), + edit("Normal Z", "normal_z", ell.normal.z), + ], + }, + ] } -fn apply_geom_prop(_ell: &mut Ellipse, _field: &str, _value: &str) {} +fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) { + let Some(v) = parse_f64(value) else { + return; + }; + match field { + "center_x" => ell.center.x = v, + "center_y" => ell.center.y = v, + "center_z" => ell.center.z = v, + "ratio" => ell.minor_axis_ratio = v, + "start_param" => ell.start_parameter = v, + "end_param" => ell.end_parameter = v, + "normal_x" => ell.normal.x = v, + "normal_y" => ell.normal.y = v, + "normal_z" => ell.normal.z = v, + _ => {} + } +} fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) { match (grip_id, apply) { diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 685aec79c..310065b7b 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -8,6 +8,59 @@ use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Pr use crate::scene::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry}; use crate::scene::model::wire_model::SnapHint; +/// Signed area of every boundary path, summed via the shoelace formula over +/// the OCS boundary vertices (bulge/curved edges approximated by their end +/// points). Returns the absolute total area. +fn boundary_area(h: &Hatch) -> f64 { + let mut area = 0.0; + for path in &h.paths { + let mut ring: Vec<[f64; 2]> = Vec::new(); + for edge in &path.edges { + match edge { + BoundaryEdge::Polyline(poly) => { + for v in &poly.vertices { + ring.push([v.x, v.y]); + } + } + BoundaryEdge::Line(l) => { + ring.push([l.start.x, l.start.y]); + ring.push([l.end.x, l.end.y]); + } + BoundaryEdge::CircularArc(a) => ring.push([a.center.x, a.center.y]), + BoundaryEdge::EllipticArc(e) => ring.push([e.center.x, e.center.y]), + BoundaryEdge::Spline(s) => { + let src = if !s.fit_points.is_empty() { + s.fit_points.iter().map(|p| [p.x, p.y]).collect::>() + } else { + s.control_points + .iter() + .map(|p| [p.x, p.y]) + .collect::>() + }; + ring.extend(src); + } + } + } + let n = ring.len(); + if n < 3 { + continue; + } + let mut acc = 0.0; + for i in 0..n { + let a = ring[i]; + let b = ring[(i + 1) % n]; + acc += a[0] * b[1] - b[0] * a[1]; + } + area += acc.abs() * 0.5; + } + area +} + +/// Format a color for a read-only row (`ByLayer` / `ByBlock` / index / RGB). +fn color_str(c: acadrust::types::Color) -> String { + c.to_string() +} + fn properties(h: &Hatch) -> Vec { let pattern_type = match h.pattern_type { acadrust::entities::HatchPatternType::Predefined => "Predefined", @@ -19,61 +72,109 @@ fn properties(h: &Hatch) -> Vec { acadrust::entities::HatchStyleType::Outer => "Outer", acadrust::entities::HatchStyleType::Ignore => "Ignore", }; - let fill_type = if h.gradient_color.enabled { - format!("Gradient ({})", h.gradient_color.name) - } else if h.is_solid { - "Solid".into() - } else { - format!("Pattern ({})", h.pattern.name) - }; - let boundary_count: usize = h - .paths - .iter() - .map(|p| { - p.edges - .iter() - .map(|e| match e { - BoundaryEdge::Polyline(poly) => poly.vertices.len(), - _ => 1, - }) - .sum::() - }) - .sum(); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Fill Type", "fill_type", fill_type), - Property { - label: "Pattern Name".into(), - field: "pattern_name", - value: PropValue::HatchPatternChoice(h.pattern.name.clone()), + let area = boundary_area(h); + let g = &h.gradient_color; + let grad_c1 = g.colors.first().map(|e| color_str(e.color)).unwrap_or_default(); + let grad_c2 = g.colors.get(1).map(|e| color_str(e.color)).unwrap_or_default(); + + if g.enabled { + // ── Gradient fill ────────────────────────────────────────────────── + let grad_type = if g.is_single_color { "One color" } else { "Two color" }; + let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" }; + return vec![ + PropSection { + title: "Pattern".into(), + props: vec![ + ro("Type", "fill_type", grad_type), + ro("Gradient name", "gradient_name", g.name.clone()), + ro("Color 1", "gradient_color_1", grad_c1), + ro("Color 2", "gradient_color_2", grad_c2), + edit("Angle", "pattern_angle", g.angle.to_degrees()), + ro("Centered", "gradient_centered", centered), + ], }, - ro("Pattern Type", "pattern_type", pattern_type), - edit( - "Pattern Angle", - "pattern_angle", - h.pattern_angle.to_degrees(), - ), - edit("Pattern Scale", "pattern_scale", h.pattern_scale), - ro("Style", "style", style), - ro("Boundary Paths", "path_count", h.paths.len().to_string()), - ro("Boundary Verts", "vert_count", boundary_count.to_string()), - ro("Double", "double", if h.is_double { "Yes" } else { "No" }), - ro( - "Associative", - "associative", - if h.is_associative { "Yes" } else { "No" }, - ), - edit("Elevation", "elevation", h.elevation), - ro("Seed Points", "seed_count", h.seed_points.len().to_string()), - ro("Pixel Size", "pixel_size", format!("{:.6}", h.pixel_size)), - ro( - "Normal", - "normal", - format!("{:.3}, {:.3}, {:.3}", h.normal.x, h.normal.y, h.normal.z), - ), - ], - }] + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Elevation", "elevation", h.elevation), + ro("Origin X", "origin_x", String::new()), + ro("Origin Y", "origin_y", String::new()), + ro("Area", "area", format!("{:.4}", area)), + ro("Cumulative area", "cumulative_area", format!("{:.4}", area)), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro( + "Associative", + "associative", + if h.is_associative { "Yes" } else { "No" }, + ), + ro("Annotative", "annotative", String::new()), + ro("Island detection style", "style", style), + ro("Background color", "background_color", String::new()), + ], + }, + ]; + } + + // ── Hatch (pattern / solid) ──────────────────────────────────────────── + let hatch_type = if h.is_solid { "Solid" } else { "Pattern" }; + vec![ + PropSection { + title: "Pattern".into(), + props: vec![ + ro("Type", "pattern_type_label", hatch_type), + Property { + label: "Pattern name".into(), + field: "pattern_name", + value: PropValue::HatchPatternChoice(h.pattern.name.clone()), + }, + ro("Color", "color", color_str(h.common.color)), + ro("Background color", "background_color", String::new()), + ro( + "Transparency", + "transparency", + format!("{}%", (h.common.transparency.as_percent() * 100.0).round() as i32), + ), + edit("Angle", "pattern_angle", h.pattern_angle.to_degrees()), + edit("Scale", "pattern_scale", h.pattern_scale), + ro("Origin X", "origin_x", String::new()), + ro("Origin Y", "origin_y", String::new()), + ro("Annotative", "annotative", String::new()), + ro( + "Associative", + "associative", + if h.is_associative { "Yes" } else { "No" }, + ), + ro("Layer override", "layer_override", String::new()), + ro("Double", "double", if h.is_double { "Yes" } else { "No" }), + ro("Spacing", "spacing", String::new()), + ro("ISO pen width", "iso_pen_width", String::new()), + ro("Gradient colors", "gradient_colors", g.colors.len().to_string()), + ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)), + ro("Gradient color 1", "gradient_color_1", grad_c1), + ro("Gradient color 2", "gradient_color_2", grad_c2), + ro("Pattern type", "pattern_type", pattern_type), + ], + }, + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Elevation", "elevation", h.elevation), + ro("Area", "area", format!("{:.4}", area)), + ro("Cumulative area", "cumulative_area", format!("{:.4}", area)), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Boundary retention", "boundary_retention", String::new()), + ro("Island detection style", "style", style), + ], + }, + ] } fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { @@ -81,6 +182,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { return; }; match field { + "pattern_angle" if h.gradient_color.enabled => h.gradient_color.angle = v.to_radians(), "pattern_angle" => h.pattern_angle = v.to_radians(), "pattern_scale" if v > 0.0 => h.pattern_scale = v, "elevation" => h.elevation = v, diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 0f4120bd9..2e222f401 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -6,7 +6,7 @@ use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::WireModel; use crate::scene::cache::block_cache; use crate::scene::convert::tessellate; @@ -20,19 +20,58 @@ fn grips(ins: &Insert) -> Vec { } fn properties(ins: &Insert) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Insert X", "ins_x", ins.insert_point.x), - edit("Insert Y", "ins_y", ins.insert_point.y), - edit("Insert Z", "ins_z", ins.insert_point.z), - edit("Scale X", "x_scale", ins.x_scale()), - edit("Scale Y", "y_scale", ins.y_scale()), - edit("Scale Z", "z_scale", ins.z_scale()), - edit("Rotation", "rotation", ins.rotation.to_degrees()), - ro("Block", "block", ins.block_name.clone()), - ], - }] + let annotative = ins + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some(); + let attrs: Vec = ins + .attributes + .iter() + .map(|a| Property { + label: a.tag.clone(), + field: "attr", + value: PropValue::AttrText { + tag: a.tag.clone(), + value: a.value.clone(), + }, + }) + .collect(); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "ins_x", ins.insert_point.x), + edit("Position Y", "ins_y", ins.insert_point.y), + edit("Position Z", "ins_z", ins.insert_point.z), + edit("Scale X", "x_scale", ins.x_scale()), + edit("Scale Y", "y_scale", ins.y_scale()), + edit("Scale Z", "z_scale", ins.z_scale()), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Name", "block", ins.block_name.clone()), + edit("Rotation", "rotation", ins.rotation.to_degrees()), + ro("Block Unit", "block_unit", String::new()), + ro("Unit factor", "unit_factor", String::new()), + ro( + "Annotative", + "annotative", + if annotative { "Yes" } else { "No" }, + ), + ], + }, + PropSection { + title: "Attributes".into(), + props: attrs, + }, + PropSection { + title: "Custom".into(), + props: Vec::new(), + }, + ] } fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) { diff --git a/src/entities/leader.rs b/src/entities/leader.rs index e4fa7fb3d..15bc6adc6 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -191,102 +191,94 @@ fn hookline_dir_str(hd: &HooklineDirection) -> &'static str { } } +fn color_str(c: &acadrust::types::Color) -> String { + match c.rgb() { + Some((r, g, b)) => format!("RGB({},{},{})", r, g, b), + None => format!("{:?}", c), + } +} + fn properties(leader: &Leader) -> Vec { let n = leader.vertices.len(); - let mut props = vec![ - // Style - Property { - label: "Dim Style".into(), - field: "dimension_style", - value: PropValue::EditText(leader.dimension_style.clone()), - }, - // Path type + + let arrow_size = leader.text_height.max(1.0); + + let misc = vec![ choice_prop( - "Path Type", + "Type", "path_type", path_type_str(&leader.path_type), &["Straight", "Spline"], ), - // Creation type choice_prop( - "Creation Type", + "Annotation type", "creation_type", creation_type_str(&leader.creation_type), &["With Text", "With Tolerance", "With Block", "No Annotation"], ), - // Arrow - bool_toggle("Arrow", "arrow_enabled", leader.arrow_enabled), - // Hookline - bool_toggle("Hookline", "hookline_enabled", leader.hookline_enabled), - choice_prop( - "Hookline Dir", - "hookline_direction", - hookline_dir_str(&leader.hookline_direction), - &["Opposite", "Same"], - ), - // Text dims - edit("Text Height", "text_height", leader.text_height), - edit("Text Width", "text_width", leader.text_width), - // Normal - edit("Normal X", "normal_x", leader.normal.x), - edit("Normal Y", "normal_y", leader.normal.y), - edit("Normal Z", "normal_z", leader.normal.z), - // Horizontal direction - edit("H Dir X", "h_dir_x", leader.horizontal_direction.x), - edit("H Dir Y", "h_dir_y", leader.horizontal_direction.y), - edit("H Dir Z", "h_dir_z", leader.horizontal_direction.z), - // Block offset - edit("Block Offset X", "block_offset_x", leader.block_offset.x), - edit("Block Offset Y", "block_offset_y", leader.block_offset.y), - edit("Block Offset Z", "block_offset_z", leader.block_offset.z), - // Annotation offset - edit("Ann Offset X", "ann_offset_x", leader.annotation_offset.x), - edit("Ann Offset Y", "ann_offset_y", leader.annotation_offset.y), - edit("Ann Offset Z", "ann_offset_z", leader.annotation_offset.z), - // Stats - ro("Vertices", "vertex_count", n.to_string()), - ro("Length", "length", format!("{:.4}", leader.length())), - // Annotation reference + dim-style colour override (read-only — - // they are written by the file and survive a round-trip). - ro( - "Annotation", - "annotation_handle", - if leader.annotation_handle.is_null() { - "(none)".to_string() - } else { - format!("{:X}", leader.annotation_handle.value()) - }, - ), - ro( - "Override Color", - "override_color", - match leader.override_color.rgb() { - Some((r, g, b)) => format!("RGB({},{},{})", r, g, b), - None => format!("{:?}", leader.override_color), - }, - ), + bool_toggle("Arrowhead", "arrow_enabled", leader.arrow_enabled), + Property { + label: "Dim style".into(), + field: "dimension_style", + value: PropValue::EditText(leader.dimension_style.clone()), + }, ]; - // Arrow point (vertex[0]) + 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", String::new()), + 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() { - props.push(edit("Arrow X", "arrow_x", a.x)); - props.push(edit("Arrow Y", "arrow_y", a.y)); - props.push(edit("Arrow Z", "arrow_z", a.z)); + 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)); + } 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())); } - // End point (last vertex) - if n >= 2 { - if let Some(e) = leader.end_point() { - props.push(edit("End X", "end_x", e.x)); - props.push(edit("End Y", "end_y", e.y)); - props.push(edit("End Z", "end_z", e.z)); - } - } - - vec![PropSection { - title: "Geometry".into(), - props, - }] + vec![ + PropSection { + title: "Misc".into(), + props: misc, + }, + PropSection { + title: "Lines & Arrows".into(), + props: lines_arrows, + }, + PropSection { + title: "Text".into(), + props: text, + }, + PropSection { + title: "Fit".into(), + props: fit, + }, + PropSection { + title: "Geometry".into(), + props: geometry, + }, + ] } fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) { @@ -398,45 +390,21 @@ fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) { leader.annotation_offset.z = v; } } - "arrow_x" => { + "vertex_x" => { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { vert.x = v; } } - "arrow_y" => { + "vertex_y" => { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { vert.y = v; } } - "arrow_z" => { + "vertex_z" => { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { vert.z = v; } } - "end_x" => { - let last = leader.vertices.len().saturating_sub(1); - if last > 0 { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) { - vert.x = v; - } - } - } - "end_y" => { - let last = leader.vertices.len().saturating_sub(1); - if last > 0 { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) { - vert.y = v; - } - } - } - "end_z" => { - let last = leader.vertices.len().saturating_sub(1); - if last > 0 { - if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) { - vert.z = v; - } - } - } _ => {} } } diff --git a/src/entities/line.rs b/src/entities/line.rs index 7ae3dbd42..5a6c7b1f7 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -73,6 +73,10 @@ fn grips(line: &Line) -> Vec { } fn properties(line: &Line) -> Vec { + let dx = line.end.x - line.start.x; + let dy = line.end.y - line.start.y; + let dz = line.end.z - line.start.z; + let angle = dy.atan2(dx).to_degrees().rem_euclid(360.0); vec![PropSection { title: "Geometry".into(), props: vec![ @@ -82,7 +86,11 @@ fn properties(line: &Line) -> Vec { edit("End X", "end_x", line.end.x), edit("End Y", "end_y", line.end.y), edit("End Z", "end_z", line.end.z), + ro("Delta X", "delta_x", format!("{dx:.4}")), + ro("Delta Y", "delta_y", format!("{dy:.4}")), + ro("Delta Z", "delta_z", format!("{dz:.4}")), ro("Length", "length", format!("{:.4}", line.length())), + ro("Angle", "angle", format!("{angle:.2}")), ], }] } diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 83bea3c70..e39b63f18 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -323,21 +323,40 @@ fn grips(pline: &LwPolyline) -> Vec { } fn properties(pline: &LwPolyline) -> Vec { - let (length, area) = length_and_area(pline); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Vertices", "vertices", pline.vertices.len().to_string()), - ro( - "Closed", - "closed", - if pline.is_closed { "Yes" } else { "No" }, - ), - edit("Elevation", "elevation", pline.elevation), - ro("Length", "length", format!("{length:.4}")), - ro("Area", "area", format!("{area:.4}")), - ], - }] + let v0 = pline.vertices.first(); + let vx = v0.map_or(0.0, |v| v.location.x); + let vy = v0.map_or(0.0, |v| v.location.y); + let start_w = v0.map_or(0.0, |v| v.start_width); + let end_w = v0.map_or(0.0, |v| v.end_width); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Current Vertex", "current_vertex", String::new()), + edit("Vertex X", "vertex_x", vx), + edit("Vertex Y", "vertex_y", vy), + edit("Start segment width", "start_width", start_w), + edit("End segment width", "end_width", end_w), + edit("Global width", "global_width", pline.constant_width), + edit("Elevation", "elevation", pline.elevation), + ], + }, + 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" }, + ), + ], + }, + ] } /// Path length and enclosed area of a polyline, accounting for arc @@ -382,8 +401,30 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) { let Some(v) = parse_f64(value) else { return; }; - if field == "elevation" { - pline.elevation = v; + match field { + "elevation" => pline.elevation = v, + "global_width" => pline.constant_width = v, + "vertex_x" => { + if let Some(vtx) = pline.vertices.first_mut() { + vtx.location.x = v; + } + } + "vertex_y" => { + if let Some(vtx) = pline.vertices.first_mut() { + vtx.location.y = v; + } + } + "start_width" => { + if let Some(vtx) = pline.vertices.first_mut() { + vtx.start_width = v; + } + } + "end_width" => { + if let Some(vtx) = pline.vertices.first_mut() { + vtx.end_width = v; + } + } + _ => {} } } diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 2083a0837..f2916ccee 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -117,21 +117,28 @@ impl Grippable for Face3D { impl PropertyEditable for Face3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { use crate::entities::common::edit_prop as edit; + let inv = self.invisible_edges; + let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" }; vec![PropSection { title: "Geometry".into(), props: vec![ - edit("P1 X", "f3_p1x", self.first_corner.x), - edit("P1 Y", "f3_p1y", self.first_corner.y), - edit("P1 Z", "f3_p1z", self.first_corner.z), - edit("P2 X", "f3_p2x", self.second_corner.x), - edit("P2 Y", "f3_p2y", self.second_corner.y), - edit("P2 Z", "f3_p2z", self.second_corner.z), - edit("P3 X", "f3_p3x", self.third_corner.x), - edit("P3 Y", "f3_p3y", self.third_corner.y), - edit("P3 Z", "f3_p3z", self.third_corner.z), - edit("P4 X", "f3_p4x", self.fourth_corner.x), - edit("P4 Y", "f3_p4y", self.fourth_corner.y), - edit("P4 Z", "f3_p4z", self.fourth_corner.z), + ro("Current vertex", "f3_current", String::new()), + edit("Vertex 1 X", "f3_p1x", self.first_corner.x), + edit("Vertex 1 Y", "f3_p1y", self.first_corner.y), + edit("Vertex 1 Z", "f3_p1z", self.first_corner.z), + edit("Vertex 2 X", "f3_p2x", self.second_corner.x), + edit("Vertex 2 Y", "f3_p2y", self.second_corner.y), + edit("Vertex 2 Z", "f3_p2z", self.second_corner.z), + edit("Vertex 3 X", "f3_p3x", self.third_corner.x), + edit("Vertex 3 Y", "f3_p3y", self.third_corner.y), + edit("Vertex 3 Z", "f3_p3z", self.third_corner.z), + edit("Vertex 4 X", "f3_p4x", self.fourth_corner.x), + edit("Vertex 4 Y", "f3_p4y", self.fourth_corner.y), + edit("Vertex 4 Z", "f3_p4z", self.fourth_corner.z), + ro("Edge 1", "f3_edge1", edge(inv.is_first_invisible())), + ro("Edge 2", "f3_edge2", edge(inv.is_second_invisible())), + ro("Edge 3", "f3_edge3", edge(inv.is_third_invisible())), + ro("Edge 4", "f3_edge4", edge(inv.is_fourth_invisible())), ], }] } @@ -283,38 +290,49 @@ impl PropertyEditable for PolygonMesh { acadrust::entities::polygon_mesh::SurfaceSmoothType::Cubic => "Cubic", acadrust::entities::polygon_mesh::SurfaceSmoothType::Bezier => "Bezier", }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("M count", "pm_m", self.m_vertex_count.to_string()), - ro("N count", "pm_n", self.n_vertex_count.to_string()), - ro("Vertices", "pm_v", self.vertices.len().to_string()), - ro("Smooth Type", "pm_smooth", smooth), - ro( - "Smooth M Density", - "pm_smooth_m", - self.m_smooth_density.to_string(), - ), - ro( - "Smooth N Density", - "pm_smooth_n", - self.n_smooth_density.to_string(), - ), - ro( - "Elevation", - "pm_elevation", - format!("{:.4}", self.elevation), - ), - ro( - "Normal", - "pm_normal", - format!( - "{:.3}, {:.3}, {:.3}", - self.normal.x, self.normal.y, self.normal.z + let yesno = |b: bool| if b { "Yes" } else { "No" }; + let first = self.vertices.first(); + // Grid faces: one quad per cell; closed direction adds a wrap row/column. + let m = self.m_vertex_count.max(0) as i64; + let n = self.n_vertex_count.max(0) as i64; + let cells_m = if self.is_closed_m() { m } else { (m - 1).max(0) }; + let cells_n = if self.is_closed_n() { n } else { (n - 1).max(0) }; + let face_count = cells_m * cells_n; + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Vertex", "pm_vertex", String::new()), + ro( + "Vertex X", + "pm_vx", + first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(), ), - ), - ], - }] + ro( + "Vertex Y", + "pm_vy", + first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(), + ), + ro( + "Vertex Z", + "pm_vz", + first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(), + ), + ro("M vertex count", "pm_m", self.m_vertex_count.to_string()), + ro("N vertex count", "pm_n", self.n_vertex_count.to_string()), + ro("M closed", "pm_closed_m", yesno(self.is_closed_m())), + ro("N closed", "pm_closed_n", yesno(self.is_closed_n())), + ro("M density", "pm_smooth_m", self.m_smooth_density.to_string()), + ro("N density", "pm_smooth_n", self.n_smooth_density.to_string()), + ro("Vertex count", "pm_v", self.vertices.len().to_string()), + ro("Face count", "pm_faces", face_count.to_string()), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ro("Fit/smooth", "pm_smooth", smooth)], + }, + ] } fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} @@ -431,40 +449,44 @@ impl PropertyEditable for PolyfaceMesh { acadrust::entities::PolyfaceSmoothType::Cubic => "Cubic", acadrust::entities::PolyfaceSmoothType::Bezier => "Bezier", }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Vertices", "pfm_v", self.vertices.len().to_string()), - ro("Faces", "pfm_f", self.faces.len().to_string()), - ro("Smooth Surface", "pfm_smooth", smooth), - ro( - "Seqend", - "pfm_seqend_handle", - match self.seqend_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ro( - "Elevation", - "pfm_elevation", - format!("{:.4}", self.elevation), - ), - ro( - "Normal", - "pfm_normal", - format!( - "{:.3}, {:.3}, {:.3}", - self.normal.x, self.normal.y, self.normal.z + let first = self.vertices.first(); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Vertex", "pfm_vertex", String::new()), + ro( + "Vertex X", + "pfm_vx", + first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(), ), - ), - ro( - "Thickness", - "pfm_thickness", - format!("{:.4}", self.thickness), - ), - ], - }] + ro( + "Vertex Y", + "pfm_vy", + first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(), + ), + ro( + "Vertex Z", + "pfm_vz", + first.map(|v| format!("{:.4}", v.location.z)).unwrap_or_default(), + ), + // Polyface meshes store an explicit vertex/face list rather + // than an M×N grid, so the grid-only rows are not applicable. + ro("M vertex count", "pfm_m", String::new()), + ro("N vertex count", "pfm_n", String::new()), + ro("M closed", "pfm_closed_m", String::new()), + ro("N closed", "pfm_closed_n", String::new()), + ro("M density", "pfm_density_m", String::new()), + ro("N density", "pfm_density_n", String::new()), + ro("Vertex count", "pfm_v", self.vertices.len().to_string()), + ro("Face count", "pfm_f", self.faces.len().to_string()), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ro("Fit/smooth", "pfm_smooth", smooth)], + }, + ] } fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} @@ -598,24 +620,35 @@ impl Grippable for Mesh { impl PropertyEditable for Mesh { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let creased_edges = self.edges.iter().filter(|e| e.has_crease()).count(); + // Watertight when every face edge is shared by exactly two faces + // (closed manifold). Empty meshes are not watertight. + let mut edge_use: std::collections::HashMap<(usize, usize), u32> = + std::collections::HashMap::new(); + for face in &self.faces { + let vs = &face.vertices; + for i in 0..vs.len() { + let a = vs[i]; + let b = vs[(i + 1) % vs.len()]; + let key = if a < b { (a, b) } else { (b, a) }; + *edge_use.entry(key).or_insert(0) += 1; + } + } + let watertight = + !self.faces.is_empty() && edge_use.values().all(|&c| c == 2); vec![PropSection { title: "Geometry".into(), props: vec![ - ro("Vertices", "msh_v", self.vertices.len().to_string()), - ro("Faces", "msh_f", self.faces.len().to_string()), - ro("Edges", "msh_e", self.edges.len().to_string()), - ro("Creased Edges", "msh_creased", creased_edges.to_string()), - ro("Version", "msh_version", self.version.to_string()), ro( - "Subdivision Level", + "Level of Smoothness", "msh_subdiv", self.subdivision_level.to_string(), ), + ro("Number of Faces", "msh_f", self.faces.len().to_string()), + ro("Number of Grips", "msh_grips", self.vertices.len().to_string()), ro( - "Blend Crease", - "msh_blend_crease", - if self.blend_crease { "Yes" } else { "No" }, + "Watertight", + "msh_watertight", + if watertight { "Yes" } else { "No" }, ), ], }] diff --git a/src/entities/mline.rs b/src/entities/mline.rs index b7b726bad..48b20bb66 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -194,46 +194,39 @@ impl PropertyEditable for MLine { acadrust::entities::MLineJustification::Zero => "Zero", acadrust::entities::MLineJustification::Bottom => "Bottom", }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Style", "ml_style", self.style_name.clone()), - ro( - "Style Handle", - "ml_style_handle", - match self.style_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), + let cur = self.vertices.first(); + let cur_x = cur.map(|v| v.position.x).unwrap_or(0.0); + let cur_y = cur.map(|v| v.position.y).unwrap_or(0.0); + let cur_z = cur.map(|v| v.position.z).unwrap_or(0.0); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Vertex", "ml_vertex", if self.vertices.is_empty() { String::new() } else { "1".to_string() }), + edit("Vertex X", "ml_vertex_x", cur_x), + edit("Vertex Y", "ml_vertex_y", cur_y), + edit("Vertex Z", "ml_vertex_z", cur_z), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Style", "ml_style", self.style_name.clone()), + Property { + label: "Style justification".into(), + field: "ml_justification", + value: PropValue::Choice { + selected: just_str.to_string(), + options: ["Top", "Zero", "Bottom"] + .into_iter() + .map(str::to_string) + .collect(), + }, }, - ), - ro("Vertices", "ml_verts", self.vertices.len().to_string()), - ro( - "Style Elements", - "ml_style_elem_count", - self.style_element_count.to_string(), - ), - edit("Scale", "ml_scale", self.scale_factor), - Property { - label: "Justification".into(), - field: "ml_justification", - value: PropValue::Choice { - selected: just_str.to_string(), - options: ["Top", "Zero", "Bottom"] - .into_iter() - .map(str::to_string) - .collect(), - }, - }, - Property { - label: "Closed".into(), - field: "ml_closed", - value: PropValue::BoolToggle { - field: "ml_closed", - value: self.flags.contains(acadrust::entities::MLineFlags::CLOSED), - }, - }, - ], - }] + edit("Style scale", "ml_scale", self.scale_factor), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { @@ -261,8 +254,24 @@ impl PropertyEditable for MLine { let Ok(v) = value.trim().parse::() else { return; }; - if field == "ml_scale" && v != 0.0 { - self.scale_factor = v; + match field { + "ml_scale" if v != 0.0 => self.scale_factor = v, + "ml_vertex_x" => { + if let Some(vx) = self.vertices.first_mut() { + vx.position.x = v; + } + } + "ml_vertex_y" => { + if let Some(vx) = self.vertices.first_mut() { + vx.position.y = v; + } + } + "ml_vertex_z" => { + if let Some(vx) = self.vertices.first_mut() { + vx.position.z = v; + } + } + _ => {} } } } diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 76cd4b012..f3caadf2e 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -192,69 +192,115 @@ fn grips(t: &MText) -> Vec { vec![square_grip(0, p), triangle_grip(1, width_grip)] } +fn columns_str(c: &acadrust::entities::MTextColumnData) -> &'static str { + match c.column_type { + 1 => "Static", + 2 => "Dynamic", + _ => "None", + } +} + fn properties(t: &MText, text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Insert X", "ins_x", t.insertion_point.x), - edit("Insert Y", "ins_y", t.insertion_point.y), - edit("Insert Z", "ins_z", t.insertion_point.z), - edit("Height", "height", t.height), - edit("Width", "rect_w", t.rectangle_width), - edit("Rect Height", "rect_h", t.rectangle_height.unwrap_or(0.0)), - edit("Rotation", "rotation", t.rotation.to_degrees()), - edit("Line Spacing", "line_spacing", t.line_spacing_factor), - Property { - label: "H-Align".into(), - field: "h_align", - value: PropValue::Choice { - selected: mtext_halign_str(&t.attachment_point).to_string(), - options: ["Left", "Center", "Right"] - .into_iter() - .map(str::to_string) - .collect(), + // Absolute line-space distance for single spacing (~1.66 * height) scaled + // by the line-spacing factor. + let line_space_distance = t.height * 1.666_666_666_666_667 * t.line_spacing_factor; + let bg_mask_on = (t.background_fill_flags & 0x03) != 0; + let text_frame_on = (t.background_fill_flags & 0x10) != 0; + vec![ + PropSection { + title: "Text".into(), + props: vec![ + Property { + label: "Contents".into(), + field: "content", + value: PropValue::EditText(t.value.clone()), }, - }, - Property { - label: "V-Align".into(), - field: "v_align", - value: PropValue::Choice { - selected: mtext_valign_str(&t.attachment_point).to_string(), - options: ["Top", "Middle", "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(), - ), - Property { - label: "Content".into(), - field: "content", - value: PropValue::EditText(t.value.clone()), - }, - Property { - label: "Style".into(), - field: "style", - value: PropValue::Choice { - selected: if t.style.trim().is_empty() { - "Standard".into() - } else { - t.style.clone() + Property { + label: "Style".into(), + field: "style", + value: PropValue::Choice { + selected: if t.style.trim().is_empty() { + "Standard".into() + } else { + t.style.clone() + }, + options: text_style_names.to_vec(), }, - options: text_style_names.to_vec(), }, - }, - ], - }] + ro( + "Annotative", + "annotative", + if t.is_annotative { "Yes" } else { "No" }, + ), + Property { + label: "Justify".into(), + field: "h_align", + value: PropValue::Choice { + selected: mtext_halign_str(&t.attachment_point).to_string(), + options: ["Left", "Center", "Right"] + .into_iter() + .map(str::to_string) + .collect(), + }, + }, + Property { + label: "V-Align".into(), + field: "v_align", + value: PropValue::Choice { + selected: mtext_valign_str(&t.attachment_point).to_string(), + options: ["Top", "Middle", "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", String::new()), + ro( + "Background mask", + "background_mask", + if bg_mask_on { "On" } else { "Off" }, + ), + edit("Defined width", "rect_w", t.rectangle_width), + edit("Defined height", "rect_h", t.rectangle_height.unwrap_or(0.0)), + ro( + "Text frame", + "text_frame", + if text_frame_on { "On" } else { "Off" }, + ), + ro( + "Columns", + "columns", + columns_str(&t.column_data).to_string(), + ), + ], + }, + 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), + ], + }, + ] } fn apply_geom_prop(t: &mut MText, field: &str, value: &str) { diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index ac6339614..881f620ef 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -520,204 +520,186 @@ fn choice(label: &str, field: &'static str, selected: &str, opts: &[&str]) -> Pr } } +fn hexh(h: Option) -> String { + match h { + Some(h) if !h.is_null() => format!("{:X}", h.value()), + _ => "(none)".to_string(), + } +} + fn properties(ml: &MultiLeader) -> Vec { let ctx = &ml.context; - let total_pts: usize = ctx - .leader_roots - .iter() - .flat_map(|r| r.lines.iter()) - .map(|l| l.points.len()) - .sum(); + let first_root = ctx.leader_roots.first(); - let mut props = vec![ - // Content - choice( - "Content Type", - "content_type", - content_type_str(&ml.content_type), - &["None", "MText", "Block", "Tolerance"], - ), - Property { - label: "Text".into(), - field: "text_string", - value: PropValue::EditText(ctx.text_string.clone()), - }, - edit("Text Height", "text_height", ml.text_height), - edit("Text X", "text_x", ctx.text_location.x), - edit("Text Y", "text_y", ctx.text_location.y), - edit("Text Z", "text_z", ctx.text_location.z), - bool_toggle("Text Frame", "text_frame", ml.text_frame), - // Leader line - choice( - "Path Type", - "path_type", - path_type_str(&ml.path_type), - &["Straight", "Spline", "Invisible"], - ), - bool_toggle("Landing", "enable_landing", ml.enable_landing), - bool_toggle("Dogleg", "enable_dogleg", ml.enable_dogleg), - edit("Dogleg Length", "dogleg_length", ml.dogleg_length), - edit("Arrow Size", "arrowhead_size", ml.arrowhead_size), - edit("Scale", "scale_factor", ml.scale_factor), - bool_toggle( - "Annotation Scale", - "enable_annotation_scale", - ml.enable_annotation_scale, - ), - // Text attachment - choice( - "Left Attach", - "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", - ], - ), - choice( - "Right Attach", - "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", - ], - ), - // Top / Bottom attach (used when text_attachment_direction = Vertical) - choice( - "Top Attach", - "text_top_attachment", - attachment_str(&ml.text_top_attachment), - &[ - "Top of Top", - "Mid of Top", - "Mid of Text", - "Mid of Bot", - "Bot of Bot", - "Bottom Line", - ], - ), - choice( - "Bottom Attach", - "text_bottom_attachment", - attachment_str(&ml.text_bottom_attachment), - &[ - "Top of Top", - "Mid of Top", - "Mid of Text", - "Mid of Bot", - "Bot of Bot", - "Bottom Line", - ], - ), - // Stats - ro("Leader Pts", "total_pts", total_pts.to_string()), - ro("Roots", "root_count", ctx.leader_roots.len().to_string()), - // Style references / handles (read-only — the multileader's own - // copies of the style values are the authoritative render inputs). - ro( - "Style Handle", - "style_handle", - match ml.style_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ro( - "Text Style Handle", - "text_style_handle", - match ml.text_style_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ro( - "Arrow Handle", - "arrowhead_handle", - match ml.arrowhead_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ro( - "Line Type Handle", - "line_type_handle", - match ml.line_type_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ro( - "Block Content Handle", - "block_content_handle", - match ml.block_content_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - // Less common toggles surfaced read-only. - ro( - "Extend Leader", - "extend_leader_to_text", - if ml.extend_leader_to_text { - "Yes" - } else { - "No" - }, - ), - ro( - "Text Direction Negative", - "text_direction_negative", - if ml.text_direction_negative { - "Yes" - } else { - "No" - }, - ), - ro( - "Text Align In IPE", - "text_align_in_ipe", - ml.text_align_in_ipe.to_string(), - ), - ro( - "Property Override Flags", - "property_override_flags", - format!("{:#018b}", ml.property_override_flags.bits()), - ), - ro( - "Block Scale", - "block_scale", - format!( - "{:.3} × {:.3} × {:.3}", - ml.block_scale.x, ml.block_scale.y, ml.block_scale.z + // ── 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"], ), - ), - ro( - "Block Rotation", - "block_rotation", - format!("{:.3}°", ml.block_rotation.to_degrees()), - ), - ]; + edit("Overall scale", "scale_factor", ml.scale_factor), + ro( + "Annotative", + "enable_annotation_scale", + if ml.enable_annotation_scale { "Yes" } else { "No" }, + ), + // No stored annotative-scale name on the entity. + ro("Annotative scale", "annotative_scale", String::new()), + ], + }; - // Connection point for first root (most common case) - if let Some(root) = ctx.leader_roots.first() { - props.push(edit("Root Conn X", "conn_x", root.connection_point.x)); - props.push(edit("Root Conn Y", "conn_y", root.connection_point.y)); - props.push(edit("Root Conn Z", "conn_z", root.connection_point.z)); - } + // ── Leaders ────────────────────────────────────────────────────────── + 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), + ), + 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)), + edit("Arrowhead size", "arrowhead_size", ml.arrowhead_size), + ], + }; - vec![PropSection { - title: "Geometry".into(), - props, - }] + // ── 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()), + ro( + "Landing", + "enable_landing", + if ml.enable_landing { "Yes" } else { "No" }, + ), + edit( + "Landing distance", + "landing_distance", + first_root.map(|r| r.landing_distance).unwrap_or(0.0), + ), + edit("Dogleg length", "dogleg_length", ml.dogleg_length), + ], + }; + + // ── Text ───────────────────────────────────────────────────────────── + 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", + "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", + ], + ), + 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", + ], + ), + ro( + "Text align type", + "text_attachment_direction", + format!("{:?}", ml.text_attachment_direction), + ), + edit("Landing gap", "landing_gap", ctx.landing_gap), + ro( + "Background mask", + "background_fill_enabled", + if ctx.background_fill_enabled { + "Yes" + } else { + "No" + }, + ), + ], + }; + + // ── Block ──────────────────────────────────────────────────────────── + let block = PropSection { + title: "Block".into(), + props: vec![ + ro( + "Source block", + "block_content_handle", + hexh(ml.block_content_handle), + ), + ro( + "Block attachment", + "block_connection_type", + format!("{:?}", ml.block_connection_type), + ), + ro( + "Block color", + "block_content_color", + format!("{:?}", ml.block_content_color), + ), + ro( + "Block scale", + "block_scale", + format!( + "{:.3} × {:.3} × {:.3}", + ml.block_scale.x, ml.block_scale.y, ml.block_scale.z + ), + ), + edit( + "Block rotation", + "block_rotation", + ml.block_rotation.to_degrees(), + ), + ], + }; + + vec![misc, leaders, structure, text, block] } fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { @@ -804,6 +786,21 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) { ml.scale_factor = v; } } + "landing_distance" => { + if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) { + root.landing_distance = v; + } + } + "landing_gap" => { + if let Some(v) = f64(value) { + ml.context.landing_gap = v; + } + } + "block_rotation" => { + if let Some(v) = f64(value) { + ml.block_rotation = v.to_radians(); + } + } "conn_x" => { if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) { root.connection_point.x = v; diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index f4a3958ee..37907b883 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -79,42 +79,30 @@ fn properties(ole: &Ole2Frame) -> Vec { OleObjectType::Embedded => "Embedded", OleObjectType::Static => "Static", }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Type", "ole_type", type_str), - edit("Upper Left X", "ole_ulx", ole.upper_left_corner.x), - edit("Upper Left Y", "ole_uly", ole.upper_left_corner.y), - edit("Lower Right X", "ole_lrx", ole.lower_right_corner.x), - edit("Lower Right Y", "ole_lry", ole.lower_right_corner.y), - ro("Version", "ole_version", ole.version.to_string()), - ro( - "Source App", - "ole_source_app", - if ole.source_application.is_empty() { - "(unknown)".to_string() - } else { - ole.source_application.clone() - }, - ), - ro( - "Data Size", - "ole_data_size", - format!("{} bytes", ole.binary_data.len()), - ), - ro("DWG Mode", "ole_dwg_mode", ole.dwg_mode.to_string()), - ro( - "DWG Trailing Byte", - "ole_dwg_trailing", - format!("{:#04x}", ole.dwg_trailing_byte), - ), - ro( - "Paper Space", - "ole_paper_space", - if ole.is_paper_space { "Yes" } else { "No" }, - ), - ], - }] + let width = (ole.lower_right_corner.x - ole.upper_left_corner.x).abs(); + let height = (ole.upper_left_corner.y - ole.lower_right_corner.y).abs(); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "ole_ulx", ole.upper_left_corner.x), + edit("Position Y", "ole_uly", ole.upper_left_corner.y), + edit("Position Z", "ole_ulz", ole.upper_left_corner.z), + ro("Width", "ole_width", format!("{:.4}", width)), + ro("Height", "ole_height", format!("{:.4}", height)), + ro("Scale width", "ole_scale_width", String::new()), + ro("Scale height", "ole_scale_height", String::new()), + ro("Lock aspect", "ole_lock_aspect", String::new()), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Type", "ole_type", type_str), + ro("Plot quality", "ole_plot_quality", String::new()), + ], + }, + ] } fn apply_geom_prop(ole: &mut Ole2Frame, field: &str, value: &str) { @@ -124,6 +112,7 @@ fn apply_geom_prop(ole: &mut Ole2Frame, field: &str, value: &str) { match field { "ole_ulx" => ole.upper_left_corner.x = v, "ole_uly" => ole.upper_left_corner.y = v, + "ole_ulz" => ole.upper_left_corner.z = v, "ole_lrx" => ole.lower_right_corner.x = v, "ole_lry" => ole.lower_right_corner.y = v, _ => {} diff --git a/src/entities/point.rs b/src/entities/point.rs index 21c62a39a..eb718f5df 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -182,9 +182,9 @@ fn properties(pt: &Point) -> Vec { vec![PropSection { title: "Geometry".into(), props: vec![ - edit("X", "loc_x", pt.location.x), - edit("Y", "loc_y", pt.location.y), - edit("Z", "loc_z", pt.location.z), + edit("Position X", "loc_x", pt.location.x), + edit("Position Y", "loc_y", pt.location.y), + edit("Position Z", "loc_z", pt.location.z), ], }] } diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 35e727a2f..0f1ea8d8e 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -2,7 +2,7 @@ use acadrust::entities::{Polyline, Polyline2D, Polyline3D}; use truck_modeling::{builder, Edge, Point3, Wire}; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{edit_prop as edit, parse_f64, 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, PropValue, Property}; @@ -387,39 +387,58 @@ impl Grippable for Polyline2D { impl PropertyEditable for Polyline2D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let smooth = match self.smooth_surface { - acadrust::entities::SmoothSurfaceType::None => "None", - acadrust::entities::SmoothSurfaceType::QuadraticBSpline => "Quadratic", - acadrust::entities::SmoothSurfaceType::CubicBSpline => "Cubic", - acadrust::entities::SmoothSurfaceType::Bezier => "Bezier", - }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Vertices", "vertices", self.vertices.len().to_string()), - edit("Elevation", "pl2_elevation", self.elevation), - edit("Default Start W", "pl2_start_w", self.start_width), - edit("Default End W", "pl2_end_w", self.end_width), - edit("Thickness", "pl2_thickness", self.thickness), - ro("Smooth Surface", "pl2_smooth", smooth), - ro( - "Normal", - "pl2_normal", - format!( - "{:.3}, {:.3}, {:.3}", - self.normal.x, self.normal.y, self.normal.z - ), - ), - Property { - label: "Closed".into(), - field: "pl2_closed", - value: PropValue::BoolToggle { + let n = self.vertices.len(); + let mut area = 0.0; + let mut length = 0.0; + let seg_count = if self.is_closed() { n } else { n.saturating_sub(1) }; + for i in 0..seg_count { + let a = &self.vertices[i].location; + let b = &self.vertices[(i + 1) % n].location; + area += a.x * b.y - b.x * a.y; + length += ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt(); + } + 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(); + + 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("Global width", "pl2_start_w", self.start_width), + edit("Elevation", "pl2_elevation", self.elevation), + ro("Area", "pl2_area", format!("{area:.4}")), + ro("Length", "pl2_length", format!("{length:.4}")), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + Property { + label: "Closed".into(), field: "pl2_closed", - value: self.is_closed(), + value: PropValue::BoolToggle { + field: "pl2_closed", + value: self.is_closed(), + }, }, - }, - ], - }] + ro( + "Linetype generation", + "pl2_ltype_gen", + if self.flags.bits() & 128 != 0 { "Enabled" } else { "Disabled" }, + ), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { @@ -437,29 +456,18 @@ impl PropertyEditable for Polyline2D { } } "pl2_elevation" => { - if let Ok(v) = value.trim().parse::() { + if let Some(v) = parse_f64(value) { self.elevation = v; } } "pl2_start_w" => { - if let Ok(v) = value.trim().parse::() { + if let Some(v) = parse_f64(value) { if v >= 0.0 { self.start_width = v; - } - } - } - "pl2_end_w" => { - if let Ok(v) = value.trim().parse::() { - if v >= 0.0 { self.end_width = v; } } } - "pl2_thickness" => { - if let Ok(v) = value.trim().parse::() { - self.thickness = v; - } - } _ => {} } } @@ -606,41 +614,43 @@ impl Grippable for Polyline3D { impl PropertyEditable for Polyline3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { use acadrust::entities::polyline3d::SmoothSurfaceType as SST; - let smooth = match self.smooth_type { + 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 fit_smooth = match self.smooth_type { SST::None => "None", SST::QuadraticBSpline => "Quadratic", SST::CubicBSpline => "Cubic", SST::Bezier => "Bezier", }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Vertices", "vertices", self.vertices.len().to_string()), - edit("Default Start W", "pl3_start_w", self.default_start_width), - edit("Default End W", "pl3_end_w", self.default_end_width), - ro("Smooth", "pl3_smooth", smooth), - ro("Mesh M", "pl3_mesh_m", self.mesh_m_count.to_string()), - ro("Mesh N", "pl3_mesh_n", self.mesh_n_count.to_string()), - ro( - "Smooth M Density", - "pl3_smooth_m", - self.smooth_m_density.to_string(), - ), - ro( - "Smooth N Density", - "pl3_smooth_n", - self.smooth_n_density.to_string(), - ), - Property { - label: "Closed".into(), - field: "pl3_closed", - value: PropValue::BoolToggle { + + vec![ + PropSection { + 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), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + Property { + label: "Closed".into(), field: "pl3_closed", - value: self.is_closed(), + value: PropValue::BoolToggle { + field: "pl3_closed", + value: self.is_closed(), + }, }, - }, - ], - }] + ro("Fit/Smooth", "pl3_smooth", fit_smooth), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { @@ -655,14 +665,6 @@ impl PropertyEditable for Polyline3D { } else { self.open(); } - return; - } - if let Ok(v) = value.trim().parse::() { - match field { - "pl3_start_w" if v >= 0.0 => self.default_start_width = v, - "pl3_end_w" if v >= 0.0 => self.default_end_width = v, - _ => {} - } } } } diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index 0bf9eb378..f2f3c0948 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -163,57 +163,101 @@ impl Grippable for RasterImage { impl PropertyEditable for RasterImage { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("File", "ri_file", self.file_path.clone()), - edit("Insert X", "ri_ox", self.insertion_point.x), - edit("Insert Y", "ri_oy", self.insertion_point.y), - edit("Insert Z", "ri_oz", self.insertion_point.z), - edit("Brightness", "ri_bright", self.brightness as f64), - edit("Contrast", "ri_contrast", self.contrast as f64), - edit("Fade", "ri_fade", self.fade as f64), - Property { - label: "Clipping".into(), - field: "ri_clip", - value: PropValue::BoolToggle { - field: "ri_clip", - value: self.clipping_enabled, + let rotation_deg = self.u_vector.y.atan2(self.u_vector.x).to_degrees(); + let scale = self.u_vector.length(); + let show_image = self.flags.contains(acadrust::entities::ImageDisplayFlags::SHOW_IMAGE); + let show_clipped = self + .flags + .contains(acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY); + let clip_inverted = self.clip_boundary.clip_mode == acadrust::entities::ClipMode::Inside; + let transparency = format!("{:.0}%", self.common.transparency.as_percent() * 100.0); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "ri_ox", self.insertion_point.x), + edit("Position Y", "ri_oy", self.insertion_point.y), + edit("Position Z", "ri_oz", self.insertion_point.z), + ro("Rotation", "ri_rotation", format!("{:.4}", rotation_deg)), + ro("Width", "ri_width", format!("{:.4}", self.width())), + ro("Height", "ri_height", format!("{:.4}", self.height())), + ro("Scale", "ri_scale", format!("{:.4}", scale)), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Name", "ri_name", self.file_name().to_string()), + edit("Brightness", "ri_bright", self.brightness as f64), + edit("Contrast", "ri_contrast", self.contrast as f64), + edit("Fade", "ri_fade", self.fade as f64), + ro("Transparency", "ri_transparency", transparency), + Property { + label: "Show image".into(), + field: "ri_show_image", + value: PropValue::BoolToggle { + field: "ri_show_image", + value: show_image, + }, }, - }, - ro( - "Class Version", - "ri_class_version", - self.class_version.to_string(), - ), - ro( - "Definition", - "ri_def_handle", - match self.definition_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), + Property { + label: "Show clipped".into(), + field: "ri_show_clipped", + value: PropValue::BoolToggle { + field: "ri_show_clipped", + value: show_clipped, + }, }, - ), - ro( - "Def Reactor", - "ri_def_reactor_handle", - match self.definition_reactor_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), + Property { + label: "Clip inverted".into(), + field: "ri_clip_inverted", + value: PropValue::BoolToggle { + field: "ri_clip_inverted", + value: clip_inverted, + }, }, - ), - ], - }] + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { match field { - "ri_clip" => { - self.clipping_enabled = if value == "toggle" { - !self.clipping_enabled + "ri_show_image" => { + let on = if value == "toggle" { + !self.flags.contains(acadrust::entities::ImageDisplayFlags::SHOW_IMAGE) } else { value == "true" }; + self.set_visible(on); + return; + } + "ri_show_clipped" => { + let on = if value == "toggle" { + !self + .flags + .contains(acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY) + } else { + value == "true" + }; + if on { + self.flags |= acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY; + } else { + self.flags &= !acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY; + } + return; + } + "ri_clip_inverted" => { + let on = if value == "toggle" { + self.clip_boundary.clip_mode != acadrust::entities::ClipMode::Inside + } else { + value == "true" + }; + self.clip_boundary.clip_mode = if on { + acadrust::entities::ClipMode::Inside + } else { + acadrust::entities::ClipMode::Outside + }; return; } _ => {} @@ -424,57 +468,91 @@ impl Grippable for Wipeout { impl PropertyEditable for Wipeout { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Insert X", "wo_ox", self.insertion_point.x), - edit("Insert Y", "wo_oy", self.insertion_point.y), - edit("Insert Z", "wo_oz", self.insertion_point.z), - edit("Brightness", "wo_bright", self.brightness as f64), - edit("Contrast", "wo_contrast", self.contrast as f64), - edit("Fade", "wo_fade", self.fade as f64), - Property { - label: "Clipping".into(), - field: "wo_clip", - value: PropValue::BoolToggle { - field: "wo_clip", - value: self.clipping_enabled, + let show_image = self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE); + let show_clipped = self + .flags + .contains(acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY); + let bg_transparency = self + .flags + .contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON); + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "wo_ox", self.insertion_point.x), + edit("Position Y", "wo_oy", self.insertion_point.y), + edit("Position Z", "wo_oz", self.insertion_point.z), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + Property { + label: "Show image".into(), + field: "wo_show_image", + value: PropValue::BoolToggle { + field: "wo_show_image", + value: show_image, + }, }, - }, - ro( - "Class Version", - "wo_class_version", - self.class_version.to_string(), - ), - ro("Clip Mode", "wo_clip_mode", format!("{:?}", self.clip_mode)), - ro( - "Definition", - "wo_def_handle", - match self.definition_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), + Property { + label: "Show clipped".into(), + field: "wo_show_clipped", + value: PropValue::BoolToggle { + field: "wo_show_clipped", + value: show_clipped, + }, }, - ), - ro( - "Def Reactor", - "wo_def_reactor_handle", - match self.definition_reactor_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), + Property { + label: "Background transparency".into(), + field: "wo_bg_transparency", + value: PropValue::BoolToggle { + field: "wo_bg_transparency", + value: bg_transparency, + }, }, - ), - ], - }] + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { match field { - "wo_clip" => { - self.clipping_enabled = if value == "toggle" { - !self.clipping_enabled + "wo_show_image" => { + let on = if value == "toggle" { + !self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE) } else { value == "true" }; + self.set_frame_visible(on); + return; + } + "wo_show_clipped" => { + let on = if value == "toggle" { + !self + .flags + .contains(acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY) + } else { + value == "true" + }; + if on { + self.flags |= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY; + } else { + self.flags -= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY; + } + return; + } + "wo_bg_transparency" => { + let on = if value == "toggle" { + !self.flags.contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON) + } else { + value == "true" + }; + if on { + self.flags |= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON; + } else { + self.flags -= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON; + } return; } _ => {} @@ -486,9 +564,6 @@ impl PropertyEditable for Wipeout { "wo_ox" => self.insertion_point.x = v, "wo_oy" => self.insertion_point.y = v, "wo_oz" => self.insertion_point.z = v, - "wo_bright" => self.brightness = v.clamp(0.0, 100.0) as u8, - "wo_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8, - "wo_fade" => self.fade = v.clamp(0.0, 100.0) as u8, _ => {} } } diff --git a/src/entities/ray.rs b/src/entities/ray.rs index d96276479..00a3ab253 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -92,12 +92,12 @@ impl PropertyEditable for Ray { vec![PropSection { title: "Geometry".into(), props: vec![ - edit("Base X", "ray_bx", self.base_point.x), - edit("Base Y", "ray_by", self.base_point.y), - edit("Base Z", "ray_bz", self.base_point.z), - edit("Dir X", "ray_dx", self.direction.x), - edit("Dir Y", "ray_dy", self.direction.y), - edit("Dir Z", "ray_dz", self.direction.z), + edit("Start X", "ray_bx", self.base_point.x), + edit("Start Y", "ray_by", self.base_point.y), + edit("Start Z", "ray_bz", self.base_point.z), + edit("Direction vector X", "ray_dx", self.direction.x), + edit("Direction vector Y", "ray_dy", self.direction.y), + edit("Direction vector Z", "ray_dz", self.direction.z), ], }] } @@ -232,9 +232,9 @@ impl PropertyEditable for XLine { edit("Base X", "xl_bx", self.base_point.x), edit("Base Y", "xl_by", self.base_point.y), edit("Base Z", "xl_bz", self.base_point.z), - edit("Dir X", "xl_dx", self.direction.x), - edit("Dir Y", "xl_dy", self.direction.y), - edit("Dir Z", "xl_dz", self.direction.z), + edit("Direction vector X", "xl_dx", self.direction.x), + edit("Direction vector Y", "xl_dy", self.direction.y), + edit("Direction vector Z", "xl_dz", self.direction.z), ], }] } diff --git a/src/entities/solid.rs b/src/entities/solid.rs index cac5f1012..74c33af7c 100644 --- a/src/entities/solid.rs +++ b/src/entities/solid.rs @@ -7,7 +7,7 @@ use acadrust::entities::Solid; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, square_grip}; +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}; @@ -98,21 +98,25 @@ impl Grippable for Solid { impl PropertyEditable for Solid { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + // Elevation is the OCS Z shared by the planar corners (no dedicated + // field on the entity); reported from the first corner's Z. + let elevation = self.first_corner.z; vec![PropSection { title: "Geometry".into(), props: vec![ - edit("P1 X", "sl_p1x", self.first_corner.x), - edit("P1 Y", "sl_p1y", self.first_corner.y), - edit("P1 Z", "sl_p1z", self.first_corner.z), - edit("P2 X", "sl_p2x", self.second_corner.x), - edit("P2 Y", "sl_p2y", self.second_corner.y), - edit("P2 Z", "sl_p2z", self.second_corner.z), - edit("P3 X", "sl_p3x", self.third_corner.x), - edit("P3 Y", "sl_p3y", self.third_corner.y), - edit("P3 Z", "sl_p3z", self.third_corner.z), - edit("P4 X", "sl_p4x", self.fourth_corner.x), - edit("P4 Y", "sl_p4y", self.fourth_corner.y), - edit("P4 Z", "sl_p4z", self.fourth_corner.z), + edit("Point 1 X", "sl_p1x", self.first_corner.x), + edit("Point 1 Y", "sl_p1y", self.first_corner.y), + edit("Point 1 Z", "sl_p1z", self.first_corner.z), + edit("Point 2 X", "sl_p2x", self.second_corner.x), + edit("Point 2 Y", "sl_p2y", self.second_corner.y), + edit("Point 2 Z", "sl_p2z", self.second_corner.z), + edit("Point 3 X", "sl_p3x", self.third_corner.x), + edit("Point 3 Y", "sl_p3y", self.third_corner.y), + edit("Point 3 Z", "sl_p3z", self.third_corner.z), + edit("Point 4 X", "sl_p4x", self.fourth_corner.x), + edit("Point 4 Y", "sl_p4y", self.fourth_corner.y), + edit("Point 4 Z", "sl_p4z", self.fourth_corner.z), + ro("Elevation", "sl_elev", format!("{:.4}", elevation)), edit("Thickness", "sl_thick", self.thickness), ], }] diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index bf6731a18..2aca5e513 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -10,7 +10,7 @@ use acadrust::entities::{Body, Region, Solid3D, Surface}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, ro_prop as ro}; +use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; @@ -52,17 +52,6 @@ fn translate_wires(wires: &mut Vec, d: Vec3) { } } -fn acis_size_str(has_data: bool, sat_len: usize, sab_len: usize, is_binary: bool) -> String { - if !has_data { - return "none".to_string(); - } - if is_binary { - format!("{} bytes (SAB)", sab_len) - } else { - format!("{} bytes (SAT)", sat_len) - } -} - // ── Solid3D ─────────────────────────────────────────────────────────────────── impl Grippable for Solid3D { @@ -85,58 +74,40 @@ impl Grippable for Solid3D { impl PropertyEditable for Solid3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let size = acis_size_str( - self.acis_data.has_data(), - self.acis_data.sat_data.len(), - self.acis_data.sab_data.len(), - self.acis_data.is_binary, - ); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro( - "Ref Pt X", - "s3d_px", - format!("{:.4}", self.point_of_reference.x), - ), - ro( - "Ref Pt Y", - "s3d_py", - format!("{:.4}", self.point_of_reference.y), - ), - ro( - "Ref Pt Z", - "s3d_pz", - format!("{:.4}", self.point_of_reference.z), - ), - ro("ACIS Data", "s3d_acis", size), - ro( - "UID", - "s3d_uid", - if self.uid.is_empty() { - "(none)".to_string() - } else { - self.uid.clone() - }, - ), - ro( - "Silhouettes", - "s3d_silhouettes", - self.silhouettes.len().to_string(), - ), - ro( - "History", - "s3d_history", - match self.history_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - ], - }] + let history = match self.history_handle { + Some(h) if !h.is_null() => format!("{:X}", h.value()), + _ => "None".to_string(), + }; + vec![ + PropSection { + title: "Solid History".into(), + props: vec![ + ro("History", "s3d_history", history), + ro("Show History", "s3d_show_history", String::new()), + ], + }, + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "s3d_px", self.point_of_reference.x), + edit("Position Y", "s3d_py", self.point_of_reference.y), + edit("Position Z", "s3d_pz", self.point_of_reference.z), + ], + }, + ] } - fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Some(v) = parse_f64(value) else { + return; + }; + match field { + "s3d_px" => self.point_of_reference.x = v, + "s3d_py" => self.point_of_reference.y = v, + "s3d_pz" => self.point_of_reference.z = v, + _ => {} + } + } } // ── Region ──────────────────────────────────────────────────────────────────── @@ -161,45 +132,11 @@ impl Grippable for Region { impl PropertyEditable for Region { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let size = acis_size_str( - self.acis_data.has_data(), - self.acis_data.sat_data.len(), - self.acis_data.sab_data.len(), - self.acis_data.is_binary, - ); vec![PropSection { title: "Geometry".into(), props: vec![ - ro( - "Ref Pt X", - "rgn_px", - format!("{:.4}", self.point_of_reference.x), - ), - ro( - "Ref Pt Y", - "rgn_py", - format!("{:.4}", self.point_of_reference.y), - ), - ro( - "Ref Pt Z", - "rgn_pz", - format!("{:.4}", self.point_of_reference.z), - ), - ro("ACIS Data", "rgn_acis", size), - ro( - "UID", - "rgn_uid", - if self.uid.is_empty() { - "(none)".to_string() - } else { - self.uid.clone() - }, - ), - ro( - "Silhouettes", - "rgn_silhouettes", - self.silhouettes.len().to_string(), - ), + ro("Area", "rgn_area", String::new()), + ro("Perimeter", "rgn_perimeter", String::new()), ], }] } @@ -229,45 +166,11 @@ impl Grippable for Body { impl PropertyEditable for Body { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let size = acis_size_str( - self.acis_data.has_data(), - self.acis_data.sat_data.len(), - self.acis_data.sab_data.len(), - self.acis_data.is_binary, - ); vec![PropSection { - title: "Geometry".into(), + title: "Solid History".into(), props: vec![ - ro( - "Ref Pt X", - "bdy_px", - format!("{:.4}", self.point_of_reference.x), - ), - ro( - "Ref Pt Y", - "bdy_py", - format!("{:.4}", self.point_of_reference.y), - ), - ro( - "Ref Pt Z", - "bdy_pz", - format!("{:.4}", self.point_of_reference.z), - ), - ro("ACIS Data", "bdy_acis", size), - ro( - "UID", - "bdy_uid", - if self.uid.is_empty() { - "(none)".to_string() - } else { - self.uid.clone() - }, - ), - ro( - "Silhouettes", - "bdy_silhouettes", - self.silhouettes.len().to_string(), - ), + ro("History", "bdy_history", String::new()), + ro("Show History", "bdy_show_history", String::new()), ], }] } diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 6bd77f67f..1e2d8c62f 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -5,7 +5,7 @@ use truck_modeling::{ }; use crate::command::EntityTransform; -use crate::entities::common::{ro_prop as ro, square_grip}; +use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; @@ -107,21 +107,110 @@ fn grips(spline: &Spline) -> Vec { } fn properties(spline: &Spline) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Degree", "degree", spline.degree.to_string()), - ro( - "Control Pts", - "ctrl_pts", - spline.control_points.len().to_string(), - ), - ro("Fit Pts", "fit_pts", spline.fit_points.len().to_string()), - ], - }] + let show = if spline.fit_points.is_empty() { + "Control Vertices" + } else { + "Fit Points" + }; + let cp0 = spline.control_points.first(); + let fp0 = spline.fit_points.first(); + let w0 = spline.weights.first().copied(); + + // Polyline-approximation length over the effective defining points. + let pts = if spline.fit_points.is_empty() { + &spline.control_points + } else { + &spline.fit_points + }; + let mut length = 0.0; + for w in pts.windows(2) { + let dx = w[1].x - w[0].x; + let dy = w[1].y - w[0].y; + let dz = w[1].z - w[0].z; + length += (dx * dx + dy * dy + dz * dz).sqrt(); + } + + let closed = spline.flags.closed || spline.flags.periodic; + let yes_no = |b: bool| if b { "Yes" } else { "No" }; + + vec![ + PropSection { + title: "Data Points".into(), + props: vec![ + ro("Show", "show", show), + edit("Degree", "degree", spline.degree as f64), + ro( + "Control Point Count", + "ctrl_pt_count", + spline.control_points.len().to_string(), + ), + ro("Control Point", "ctrl_pt_index", "0"), + ro( + "Control Point X", + "ctrl_pt_x", + format!("{:.4}", cp0.map(|p| p.x).unwrap_or(0.0)), + ), + ro( + "Control Point Y", + "ctrl_pt_y", + format!("{:.4}", cp0.map(|p| p.y).unwrap_or(0.0)), + ), + ro( + "Control Point Z", + "ctrl_pt_z", + format!("{:.4}", cp0.map(|p| p.z).unwrap_or(0.0)), + ), + ro("Weight", "weight", format!("{:.4}", w0.unwrap_or(1.0))), + ro("Knot Parameterization", "knot_param", String::new()), + ro( + "Fit Point Count", + "fit_pt_count", + spline.fit_points.len().to_string(), + ), + ro("Fit Point", "fit_pt_index", "0"), + ro( + "Fit Point X", + "fit_pt_x", + format!("{:.4}", fp0.map(|p| p.x).unwrap_or(0.0)), + ), + ro( + "Fit Point Y", + "fit_pt_y", + format!("{:.4}", fp0.map(|p| p.y).unwrap_or(0.0)), + ), + ro( + "Fit Point Z", + "fit_pt_z", + format!("{:.4}", fp0.map(|p| p.z).unwrap_or(0.0)), + ), + ro("Fit Tolerance", "fit_tolerance", String::new()), + ro("Start Tangent X", "start_tan_x", String::new()), + ro("Start Tangent Y", "start_tan_y", String::new()), + ro("Start Tangent Z", "start_tan_z", String::new()), + ro("End Tangent X", "end_tan_x", String::new()), + ro("End Tangent Y", "end_tan_y", String::new()), + ro("End Tangent Z", "end_tan_z", String::new()), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro("Closed", "closed", yes_no(closed)), + ro("Planar", "planar", yes_no(spline.flags.planar)), + ro("Length", "length", format!("{length:.4}")), + ro("Area", "area", String::new()), + ], + }, + ] } -fn apply_geom_prop(_spline: &mut Spline, _field: &str, _value: &str) {} +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; + } + } +} fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) { if let Some(cp) = spline.control_points.get_mut(grip_id) { diff --git a/src/entities/table.rs b/src/entities/table.rs index 22d7aca28..5f0a1fa6d 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -819,112 +819,106 @@ impl Grippable for Table { impl PropertyEditable for Table { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + use crate::entities::common::edit_prop as edit; + use acadrust::entities::table::BreakOptionFlags; + let fmt_h = |oh: &Option| -> String { match oh { Some(h) if !h.is_null() => format!("{:X}", h.value()), _ => "(none)".to_string(), } }; - vec![PropSection { - title: "Table".into(), - props: vec![ - ro("Rows", "tbl_rows", self.rows.len().to_string()), - ro("Columns", "tbl_cols", self.columns.len().to_string()), - ro( - "Insert X", - "tbl_ix", - format!("{:.4}", self.insertion_point.x), - ), - ro( - "Insert Y", - "tbl_iy", - format!("{:.4}", self.insertion_point.y), - ), - ro( - "Insert Z", - "tbl_iz", - format!("{:.4}", self.insertion_point.z), - ), - ro( - "Table Style", - "tbl_style_handle", - fmt_h(&self.table_style_handle), - ), - ro( - "Block Record", - "tbl_block_rec_handle", - fmt_h(&self.block_record_handle), - ), - ro( - "Data Version", - "tbl_data_version", - self.data_version.to_string(), - ), - ro( - "Value Flags", - "tbl_value_flags", - format!("{:#010x}", self.value_flags), - ), - ro( - "Override Flag", - "tbl_override_flag", - if self.override_flag { "Yes" } else { "No" }, - ), - ro( - "Override Border Color", - "tbl_override_border_color", - if self.override_border_color { - "Yes" - } else { - "No" - }, - ), - ro( - "Override Border LW", - "tbl_override_border_lw", - if self.override_border_line_weight { - "Yes" - } else { - "No" - }, - ), - ro( - "Override Border Vis", - "tbl_override_border_vis", - if self.override_border_visibility { - "Yes" - } else { - "No" - }, - ), - ro( - "Break Spacing", - "tbl_break_spacing", - format!("{:.4}", self.break_spacing), - ), - ro( - "Break Flow", - "tbl_break_flow", - format!("{:?}", self.break_flow_direction), - ), - ro( - "Break Options", - "tbl_break_options", - format!("{:#018b}", self.break_options.bits()), - ), - ro( - "Normal", - "tbl_normal", - format!( - "{:.3}, {:.3}, {:.3}", - self.normal.x, self.normal.y, self.normal.z + let yes_no = |b: bool| -> String { + if b { "Yes".into() } else { "No".into() } + }; + // Direction = angle of the horizontal direction vector in the XY plane. + let direction_deg = + (self.horizontal_direction.y.atan2(self.horizontal_direction.x)).to_degrees(); + + vec![ + PropSection { + title: "Table".into(), + props: vec![ + ro( + "Table style", + "tbl_style_handle", + fmt_h(&self.table_style_handle), ), - ), - ], - }] + ro("Rows", "tbl_rows", self.rows.len().to_string()), + ro("Columns", "tbl_cols", self.columns.len().to_string()), + ro("Direction", "tbl_direction", format!("{:.4}", direction_deg)), + ro( + "Table width", + "tbl_width", + format!("{:.4}", self.total_width()), + ), + ro( + "Table height", + "tbl_height", + format!("{:.4}", self.total_height()), + ), + ], + }, + PropSection { + title: "Table Breaks".into(), + props: vec![ + ro( + "Enabled", + "tbl_break_enabled", + yes_no(self.break_options.contains(BreakOptionFlags::ENABLE_BREAKS)), + ), + ro( + "Direction", + "tbl_break_direction", + format!("{:?}", self.break_flow_direction), + ), + ro( + "Repeat top labels", + "tbl_break_repeat_top", + yes_no( + self.break_options + .contains(BreakOptionFlags::REPEAT_TOP_LABELS), + ), + ), + ro( + "Repeat bottom labels", + "tbl_break_repeat_bottom", + yes_no( + self.break_options + .contains(BreakOptionFlags::REPEAT_BOTTOM_LABELS), + ), + ), + ro( + "Manual positions", + "tbl_break_manual_positions", + yes_no( + self.break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_POSITIONS), + ), + ), + ro( + "Manual heights", + "tbl_break_manual_heights", + yes_no( + self.break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), + ), + ), + ro("Break height", "tbl_break_height", String::new()), + edit("Spacing", "tbl_break_spacing", self.break_spacing), + ], + }, + ] } - fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} + fn apply_geom_prop(&mut self, field: &str, value: &str) { + use crate::entities::common::parse_f64; + if field == "tbl_break_spacing" { + if let Some(v) = parse_f64(value) { + self.break_spacing = v; + } + } + } } impl Transformable for Table { diff --git a/src/entities/text.rs b/src/entities/text.rs index fe4b20595..c7f4a5af3 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -1,7 +1,7 @@ use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA}; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, parse_f64, square_grip}; +use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; use crate::entities::text_support::{ resolve_dxf_special_chars, resolve_text_style, text_local_bounds, }; @@ -170,61 +170,95 @@ fn grips(t: &Text) -> Vec { } fn properties(t: &Text, text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Insert X", "ins_x", t.insertion_point.x), - edit("Insert Y", "ins_y", t.insertion_point.y), - edit("Insert Z", "ins_z", t.insertion_point.z), - edit("Height", "height", t.height), - edit("Rotation", "rotation", t.rotation.to_degrees()), - edit("Width Factor", "width_factor", t.width_factor), - edit( - "Oblique Angle", - "oblique_angle", - t.oblique_angle.to_degrees(), - ), - Property { - label: "H-Align".into(), - field: "h_align", - 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(), + // Text alignment point (second alignment / justify point). Falls back to + // the insertion point for Left/Baseline text where no second point exists. + let ap = t.alignment_point.unwrap_or(t.insertion_point); + vec![ + PropSection { + title: "Text".into(), + props: vec![ + Property { + label: "Contents".into(), + field: "content", + value: PropValue::EditText(t.value.clone()), }, - }, - 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(), - }, - }, - Property { - label: "Content".into(), - field: "content", - value: PropValue::EditText(t.value.clone()), - }, - Property { - label: "Style".into(), - field: "style", - value: PropValue::Choice { - selected: if t.style.trim().is_empty() { - "Standard".into() - } else { - t.style.clone() + Property { + label: "Style".into(), + field: "style", + value: PropValue::Choice { + selected: if t.style.trim().is_empty() { + "Standard".into() + } else { + t.style.clone() + }, + options: text_style_names.to_vec(), }, - options: text_style_names.to_vec(), }, - }, - ], - }] + ro("Annotative", "annotative", String::new()), + ro("Annotative scale", "annotative_scale", String::new()), + Property { + label: "Justify".into(), + field: "h_align", + 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(), + }, + }, + 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()), + 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)), + ], + }, + 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), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro( + "Upside down", + "upside_down", + if t.generation_flags & 0x4 != 0 { + "Yes" + } else { + "No" + }, + ), + ro( + "Backward", + "backward", + if t.generation_flags & 0x2 != 0 { + "Yes" + } else { + "No" + }, + ), + ], + }, + ] } fn apply_geom_prop(t: &mut Text, field: &str, value: &str) { diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index e1a76ae47..56968f914 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -294,50 +294,40 @@ impl Grippable for Tolerance { impl PropertyEditable for Tolerance { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Text", "tol_text", self.text.clone()), - edit("Insert X", "tol_ix", self.insertion_point.x), - edit("Insert Y", "tol_iy", self.insertion_point.y), - edit("Insert Z", "tol_iz", self.insertion_point.z), - ro( - "Dim Style", - "tol_dim_style", - if self.dimension_style_name.is_empty() { - "(default)".to_string() - } else { - self.dimension_style_name.clone() - }, - ), - ro( - "Dim Style Handle", - "tol_dim_style_handle", - match self.dimension_style_handle { - Some(h) if !h.is_null() => format!("{:X}", h.value()), - _ => "(none)".to_string(), - }, - ), - edit("Text Height", "tol_text_height", self.text_height), - edit("Dim Gap", "tol_dim_gap", self.dimension_gap), - ro( - "Direction", - "tol_direction", - format!( - "{:.3}, {:.3}, {:.3}", - self.direction.x, self.direction.y, self.direction.z + vec![ + PropSection { + title: "Text".into(), + props: vec![ + ro("Text style", "tol_text_style", String::new()), + edit("Text height", "tol_text_height", self.text_height), + ], + }, + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "tol_ix", self.insertion_point.x), + edit("Position Y", "tol_iy", self.insertion_point.y), + edit("Position Z", "tol_iz", self.insertion_point.z), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + ro( + "Dimension style", + "tol_dim_style", + if self.dimension_style_name.is_empty() { + "(default)".to_string() + } else { + self.dimension_style_name.clone() + }, ), - ), - ro( - "Normal", - "tol_normal", - format!( - "{:.3}, {:.3}, {:.3}", - self.normal.x, self.normal.y, self.normal.z - ), - ), - ], - }] + edit("Direction X", "tol_dir_x", self.direction.x), + edit("Direction Y", "tol_dir_y", self.direction.y), + edit("Direction Z", "tol_dir_z", self.direction.z), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { @@ -349,7 +339,9 @@ impl PropertyEditable for Tolerance { "tol_iy" => self.insertion_point.y = v, "tol_iz" => self.insertion_point.z = v, "tol_text_height" if v > 0.0 => self.text_height = v, - "tol_dim_gap" => self.dimension_gap = v, + "tol_dir_x" => self.direction.x = v, + "tol_dir_y" => self.direction.y = v, + "tol_dir_z" => self.direction.z = v, _ => {} } } diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 7529e9651..49bf80d3b 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -147,58 +147,74 @@ impl Grippable for Underlay { impl PropertyEditable for Underlay { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let type_str = match self.underlay_type { - acadrust::entities::UnderlayType::Pdf => "PDF", - acadrust::entities::UnderlayType::Dwf => "DWF", - acadrust::entities::UnderlayType::Dgn => "DGN", + 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() { + (0.0_f64, 0.0_f64) + } else { + let verts = self.world_clip_boundary(); + let mut min_x = f64::INFINITY; + let mut min_y = f64::INFINITY; + let mut max_x = f64::NEG_INFINITY; + let mut max_y = f64::NEG_INFINITY; + for v in &verts { + min_x = min_x.min(v.x); + min_y = min_y.min(v.y); + max_x = max_x.max(v.x); + max_y = max_y.max(v.y); + } + (max_x - min_x, max_y - min_y) }; - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Type", "ul_type", type_str), - edit("Ins X", "ul_ix", self.insertion_point.x), - edit("Ins Y", "ul_iy", self.insertion_point.y), - edit("Ins Z", "ul_iz", self.insertion_point.z), - edit("X Scale", "ul_sx", self.x_scale), - edit("Y Scale", "ul_sy", self.y_scale), - edit("Z Scale", "ul_sz", self.z_scale), - edit("Rotation", "ul_rot", self.rotation.to_degrees()), - edit("Contrast", "ul_contrast", self.contrast as f64), - edit("Fade", "ul_fade", self.fade as f64), - ro( - "On", - "ul_on", - if self.flags.contains(UnderlayDisplayFlags::ON) { - "Yes" - } else { - "No" - }, - ), - ro( - "Clipping", - "ul_clip", - if self.flags.contains(UnderlayDisplayFlags::CLIPPING) { - "Yes" - } else { - "No" - }, - ), - ro( - "Monochrome", - "ul_mono", - if self.flags.contains(UnderlayDisplayFlags::MONOCHROME) { - "Yes" - } else { - "No" - }, - ), - ro( - "Clip Inverted", - "ul_clip_inverted", - if self.clip_inverted { "Yes" } else { "No" }, - ), - ], - }] + + let show = self.flags.contains(UnderlayDisplayFlags::ON); + let clipping = self.flags.contains(UnderlayDisplayFlags::CLIPPING); + let monochrome = self.flags.contains(UnderlayDisplayFlags::MONOCHROME); + let adjust_bg = self.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND); + + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", "ul_ix", self.insertion_point.x), + edit("Position Y", "ul_iy", self.insertion_point.y), + edit("Position Z", "ul_iz", self.insertion_point.z), + edit("Scale X", "ul_sx", self.x_scale), + edit("Scale Y", "ul_sy", self.y_scale), + edit("Scale Z", "ul_sz", self.z_scale), + ro("Width", "ul_width", format!("{:.4}", width)), + ro("Height", "ul_height", format!("{:.4}", height)), + edit("Rotation", "ul_rot", self.rotation.to_degrees()), + ], + }, + PropSection { + title: "Underlay Adjust".into(), + 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), + ), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + // Underlay name/path/layers live on the separate + // 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)), + ro("Underlay layers", "ul_layers", String::new()), + ], + }, + ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { diff --git a/src/entities/viewport.rs b/src/entities/viewport.rs index edeff1120..523387213 100644 --- a/src/entities/viewport.rs +++ b/src/entities/viewport.rs @@ -122,196 +122,94 @@ fn properties(vp: &Viewport) -> Vec { crate::scene::vp_effective_scale(vp.custom_scale, vp.view_height, vp.height); let current_scale_label = scale_label(effective_scale); - let view_opts: Vec = STD_VIEWS.iter().map(|s| s.to_string()).collect(); - let current_view = viewport_view_label(vp); - - let render_opts: Vec = RENDER_MODES.iter().map(|(s, _)| s.to_string()).collect(); - let current_render = render_mode_label(&vp.render_mode).to_string(); - let shade_opts: Vec = SHADE_PLOT_LABELS.iter().map(|s| s.to_string()).collect(); let current_shade = shade_plot_label(vp.shade_plot_mode).to_string(); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - edit("Center X", "center_x", vp.center.x), - edit("Center Y", "center_y", vp.center.y), - edit("Center Z", "center_z", vp.center.z), - edit("Width", "vp_w", vp.width), - edit("Height", "vp_h", vp.height), - // Numeric scale entry (derived from view_height; writes both). - edit("Scale (num)", "vscale", effective_scale), - // Standard scale picker. - Property { - label: "Scale".into(), - field: "vscale_std", - value: PropValue::Choice { - selected: current_scale_label, - options: scale_opts, - }, - }, - // Standard view picker. - Property { - label: "View".into(), - field: "vp_view", - value: PropValue::Choice { - selected: current_view, - options: view_opts, - }, - }, - // Render mode picker. - Property { - label: "Render Mode".into(), - field: "vp_render", - value: PropValue::Choice { - selected: current_render, - options: render_opts, - }, - }, - // Shade plot mode picker. - Property { - label: "Shade Plot".into(), - field: "vp_shade_plot", - value: PropValue::Choice { - selected: current_shade, - options: shade_opts, - }, - }, - // Display state toggles. - Property { - label: "Locked".into(), - field: "vp_locked", - value: PropValue::BoolToggle { - field: "vp_locked", - value: vp.status.locked, - }, - }, - Property { - label: "On".into(), - field: "vp_on", - value: PropValue::BoolToggle { + let annotation_scale = if effective_scale.abs() > 1e-9 { + format!("1:{}", scale_label(effective_scale)) + } else { + String::new() + }; + + vec![ + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Center X", "center_x", vp.center.x), + edit("Center Y", "center_y", vp.center.y), + edit("Center Z", "center_z", vp.center.z), + edit("Height", "vp_h", vp.height), + edit("Width", "vp_w", vp.width), + ], + }, + PropSection { + title: "Misc".into(), + props: vec![ + Property { + label: "On".into(), field: "vp_on", - value: vp.status.is_on, + value: PropValue::BoolToggle { + field: "vp_on", + value: vp.status.is_on, + }, }, - }, - Property { - label: "Perspective".into(), - field: "vp_perspective", - value: PropValue::BoolToggle { - field: "vp_perspective", - value: vp.status.perspective, + ro("Clipped", "vp_clipped", String::new()), + Property { + label: "Display locked".into(), + field: "vp_locked", + value: PropValue::BoolToggle { + field: "vp_locked", + value: vp.status.locked, + }, }, - }, - Property { - label: "Hide Plot".into(), - field: "vp_hide_plot", - value: PropValue::BoolToggle { - field: "vp_hide_plot", - value: vp.status.hide_plot, + ro("Annotation scale", "vp_anno_scale", annotation_scale), + Property { + label: "Standard scale".into(), + field: "vscale_std", + value: PropValue::Choice { + selected: current_scale_label, + options: scale_opts, + }, }, - }, - Property { - label: "UCS Icon".into(), - field: "vp_ucs_icon", - value: PropValue::BoolToggle { - field: "vp_ucs_icon", - value: vp.ucs_icon_visible, - }, - }, - edit("Target X", "vtgt_x", vp.view_target.x), - edit("Target Z", "vtgt_z", vp.view_target.z), - // Perspective lens length. - edit("Lens Length (mm)", "vp_lens", vp.lens_length), - // Per-Viewport UCS. - Property { - label: "UCS Per Viewport".into(), - field: "vp_ucs_per_vp", - value: PropValue::BoolToggle { + edit("Custom scale", "vscale", effective_scale), + Property { + label: "UCS per viewport".into(), field: "vp_ucs_per_vp", - value: vp.ucs_per_viewport, + value: PropValue::BoolToggle { + field: "vp_ucs_per_vp", + value: vp.ucs_per_viewport, + }, }, - }, - edit("UCS Origin X", "vp_ucs_ox", vp.ucs_origin.x), - edit("UCS Origin Y", "vp_ucs_oy", vp.ucs_origin.y), - edit("UCS Origin Z", "vp_ucs_oz", vp.ucs_origin.z), - edit("UCS X-Axis X", "vp_ucs_xx", vp.ucs_x_axis.x), - edit("UCS X-Axis Y", "vp_ucs_xy", vp.ucs_x_axis.y), - edit("UCS X-Axis Z", "vp_ucs_xz", vp.ucs_x_axis.z), - edit("UCS Y-Axis X", "vp_ucs_yx", vp.ucs_y_axis.x), - edit("UCS Y-Axis Y", "vp_ucs_yy", vp.ucs_y_axis.y), - edit("UCS Y-Axis Z", "vp_ucs_yz", vp.ucs_y_axis.z), - // Snap / grid metadata (drives the snap-aware UI; not rendered - // into the viewport contents directly). - edit("Snap Base X", "vp_snap_bx", vp.snap_base.x), - edit("Snap Base Y", "vp_snap_by", vp.snap_base.y), - edit("Snap Spacing X", "vp_snap_sx", vp.snap_spacing.x), - edit("Snap Spacing Y", "vp_snap_sy", vp.snap_spacing.y), - edit("Snap Angle", "vp_snap_ang", vp.snap_angle.to_degrees()), - edit("Twist Angle", "vp_twist", vp.twist_angle.to_degrees()), - edit("Front Clip Z", "vp_front_clip", vp.front_clip_z), - edit("Back Clip Z", "vp_back_clip", vp.back_clip_z), - edit("Circle Sides", "vp_circle_sides", vp.circle_sides as f64), - ro( - "Grid Flags", - "vp_grid_flags", - format!("{:#06b}", vp.grid_flags.to_bits()), - ), - ro("Grid Major", "vp_grid_major", vp.grid_major.to_string()), - ro( - "Base UCS", - "vp_base_ucs_handle", - if vp.base_ucs_handle.is_null() { - "(none)".to_string() - } else { - format!("{:X}", vp.base_ucs_handle.value()) + ro( + "Layer property overrides", + "vp_layer_overrides", + if vp.frozen_layers.is_empty() { + "No".to_string() + } else { + format!("Yes ({})", vp.frozen_layers.len()) + }, + ), + ro( + "Visual style", + "vp_visual_style_handle", + if vp.visual_style_handle.is_null() { + "(none)".to_string() + } else { + format!("{:X}", vp.visual_style_handle.value()) + }, + ), + Property { + label: "Shade plot".into(), + field: "vp_shade_plot", + value: PropValue::Choice { + selected: current_shade, + options: shade_opts, + }, }, - ), - ro("UCS Ortho", "vp_ucs_ortho", vp.ucs_ortho_type.to_string()), - ro( - "Background", - "vp_background_handle", - if vp.background_handle.is_null() { - "(none)".to_string() - } else { - format!("{:X}", vp.background_handle.value()) - }, - ), - ro( - "Shade Plot", - "vp_shade_plot_handle", - if vp.shade_plot_handle.is_null() { - "(none)".to_string() - } else { - format!("{:X}", vp.shade_plot_handle.value()) - }, - ), - ro( - "Visual Style", - "vp_visual_style_handle", - if vp.visual_style_handle.is_null() { - "(none)".to_string() - } else { - format!("{:X}", vp.visual_style_handle.value()) - }, - ), - // 3D lighting (not yet applied to the viewport mesh path). - ro( - "Default Lighting", - "vp_default_lighting", - if vp.default_lighting { "Yes" } else { "No" }, - ), - ro( - "Lighting Type", - "vp_lighting_type", - vp.default_lighting_type.to_string(), - ), - ro( - "Ambient Color", - "vp_ambient_color", - format!("{:#010x}", vp.ambient_color as u32), - ), - ], - }] + ro("Linked to Sheet View", "vp_sheet_view", String::new()), + ], + }, + ] } /// Identify which standard view matches the viewport's view direction.