diff --git a/Cargo.lock b/Cargo.lock index 955d66b6..246bf0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#486c0a9447774e3997d4ff3c7645263b8bad72b7" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#a46cb8b3a2e5523bbd28e5afa0f8efb7ae291c50" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/app/properties.rs b/src/app/properties.rs index d6a21941..1fb4e285 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -280,6 +280,112 @@ impl OpenCADStudio { } } + // ── Doc-dependent property rows ────────────────────────── + // Rows whose value lives on another object (a block record, + // an underlay definition, a dimension / multileader style) + // are left empty by the entity builders and resolved here, + // where the document is reachable. + let doc = &self.tabs[i].scene.document; + match entity { + // Block reference: the referenced block's units and the + // unit-scale factor against the drawing's INSUNITS. + acadrust::EntityType::Insert(ins) => { + let host = doc.header.insertion_units; + let src = doc + .block_records + .get(&ins.block_name) + .map(|br| br.units) + .unwrap_or(0); + set_row(&mut sections, "block_unit", insunits_name(src).to_string()); + let host_mm = if host == 0 { 1.0 } else { insunits_to_mm(host) }; + let src_mm = if src == 0 { 1.0 } else { insunits_to_mm(src) }; + let factor = if host_mm.abs() > 1e-12 { src_mm / host_mm } else { 1.0 }; + set_row(&mut sections, "unit_factor", format!("{factor:.4}")); + } + // Underlay: name + path from the referenced definition. + acadrust::EntityType::Underlay(ul) => { + if let Some((name, path)) = + doc.objects.iter().find_map(|(h, o)| match o { + acadrust::objects::ObjectType::UnderlayDefinition(def) + if *h == ul.definition_handle => + { + let nm = if !def.name.is_empty() { + def.name.clone() + } else { + def.page_name.clone() + }; + Some((nm, def.file_path.clone())) + } + _ => None, + }) + { + set_row(&mut sections, "ul_name", name); + set_row(&mut sections, "ul_path", path); + } + } + // Leader: text style / vertical text placement / overall + // scale come from its dimension style. + acadrust::EntityType::Leader(ld) => { + if let Some(ds) = find_dim_style(doc, &ld.dimension_style) { + if !ds.dimtxsty.is_empty() { + set_row(&mut sections, "text_style", ds.dimtxsty.clone()); + } + set_row( + &mut sections, + "text_pos_vert", + dimtad_label(ds.dimtad).to_string(), + ); + set_row( + &mut sections, + "dim_scale_overall", + format!("{:.4}", ds.dimscale), + ); + } + } + // Feature-control frame: FCF text style is the dimension + // style's DIMTXSTY. + acadrust::EntityType::Tolerance(tol) => { + if let Some(ds) = find_dim_style(doc, &tol.dimension_style_name) { + if !ds.dimtxsty.is_empty() { + set_row(&mut sections, "tol_text_style", ds.dimtxsty.clone()); + } + } + } + // MultiLeader: max points + segment-angle constraints + // are MLeaderStyle settings, not stored on the entity. + acadrust::EntityType::MultiLeader(ml) => { + if let Some(sh) = ml.style_handle { + if let Some((mx, a1, a2)) = + doc.objects.iter().find_map(|(h, o)| match o { + acadrust::objects::ObjectType::MultiLeaderStyle(s) + if *h == sh => + { + Some(( + s.max_leader_points, + s.first_segment_angle, + s.second_segment_angle, + )) + } + _ => None, + }) + { + set_row(&mut sections, "max_leader_points", mx.to_string()); + set_row( + &mut sections, + "first_segment_angle", + format!("{a1:.4}"), + ); + set_row( + &mut sections, + "second_segment_angle", + format!("{a2:.4}"), + ); + } + } + } + _ => {} + } + if !group_names.is_empty() { let label = group_names.join(", "); if let Some(general) = sections.first_mut() { @@ -926,6 +1032,73 @@ fn merge_prop_value( } } +/// Set the first property row matching `field` (across all sections) to a +/// read-only `value`. No-op when the field is absent. Used to fill the +/// doc-dependent placeholder rows the entity builders leave empty. +fn set_row( + sections: &mut [crate::scene::model::object::PropSection], + field: &str, + value: String, +) { + for section in sections.iter_mut() { + if let Some(row) = section.props.iter_mut().find(|p| p.field == field) { + row.value = crate::scene::model::object::PropValue::ReadOnly(value); + return; + } + } +} + +/// Resolve a dimension style by name (case-insensitive), falling back to +/// "Standard" when the name is blank. +fn find_dim_style<'a>( + doc: &'a acadrust::CadDocument, + name: &str, +) -> Option<&'a acadrust::tables::DimStyle> { + doc.dim_styles.iter().find(|s| { + s.name.eq_ignore_ascii_case(name) + || (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")) + }) +} + +/// Vertical text placement (DIMTAD) label. +fn dimtad_label(dimtad: i16) -> &'static str { + match dimtad { + 1 => "Above", + 2 => "Outside", + 3 => "JIS", + 4 => "Below", + _ => "Centered", + } +} + +/// Human-readable INSUNITS name (DXF group 70 unit codes). +fn insunits_name(code: i16) -> &'static str { + match code { + 1 => "Inches", + 2 => "Feet", + 3 => "Miles", + 4 => "Millimeters", + 5 => "Centimeters", + 6 => "Meters", + 7 => "Kilometers", + 8 => "Microinches", + 9 => "Mils", + 10 => "Yards", + 11 => "Angstroms", + 12 => "Nanometers", + 13 => "Microns", + 14 => "Decimeters", + 15 => "Decameters", + 16 => "Hectometers", + 17 => "Gigameters", + 18 => "Astronomical Units", + 19 => "Light Years", + 20 => "Parsecs", + 21 => "US Survey Feet", + _ => "Unitless", + } +} + /// Convert INSUNITS (DXF group 70) to millimetres. /// 0 = unitless / unknown: returns 1.0 so the caller treats it as "do not scale". fn insunits_to_mm(code: i16) -> f64 { diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 310065b7..c06f80db 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -150,7 +150,15 @@ fn properties(h: &Hatch) -> Vec { ), ro("Layer override", "layer_override", String::new()), ro("Double", "double", if h.is_double { "Yes" } else { "No" }), - ro("Spacing", "spacing", String::new()), + ro( + "Spacing", + "spacing", + h.pattern + .lines + .first() + .map(|l| format!("{:.4}", l.offset.length())) + .unwrap_or_default(), + ), 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)), diff --git a/src/entities/leader.rs b/src/entities/leader.rs index be5e4b8b..d6f9d032 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -231,7 +231,11 @@ fn properties(leader: &Leader) -> Vec { let text = vec![ edit("Text height", "text_height", leader.text_height), - ro("Text offset", "text_offset", String::new()), + ro( + "Text offset", + "text_offset", + format!("{:.4}", leader.annotation_offset.length()), + ), ro("Text style", "text_style", String::new()), ro("Text color", "text_color", color_str(&leader.override_color)), ro("Text position vert", "text_pos_vert", String::new()), diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index f3caadf2..e44f1e38 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -272,7 +272,14 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { "line_space_distance", format!("{line_space_distance:.4}"), ), - ro("Line space style", "line_space_style", String::new()), + ro( + "Line space style", + "line_space_style", + match t.line_spacing_style { + acadrust::entities::LineSpacingStyle::Exactly => "Exactly", + _ => "At least", + }, + ), ro( "Background mask", "background_mask", diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index 2aca5e51..d696f314 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -52,6 +52,39 @@ fn translate_wires(wires: &mut Vec, d: Vec3) { } } +/// Approximate a region's enclosed area and boundary perimeter from its +/// wireframe loops. Perimeter is the total edge length across every wire. +/// Area accumulates the Newell area vector of each loop (opposite-wound +/// holes subtract) and halves its magnitude — exact for a single planar +/// loop, approximate for multi-loop or curved regions. Returns zeros when +/// there is nothing to measure. +fn region_area_perimeter(wires: &[acadrust::entities::Wire]) -> (f64, f64) { + let mut perimeter = 0.0; + let (mut nx, mut ny, mut nz) = (0.0, 0.0, 0.0); + for wire in wires { + let pts = &wire.points; + if pts.len() < 2 { + continue; + } + for seg in pts.windows(2) { + let dx = seg[1].x - seg[0].x; + let dy = seg[1].y - seg[0].y; + let dz = seg[1].z - seg[0].z; + perimeter += (dx * dx + dy * dy + dz * dz).sqrt(); + } + let n = pts.len(); + for i in 0..n { + let a = &pts[i]; + let b = &pts[(i + 1) % n]; + nx += (a.y - b.y) * (a.z + b.z); + ny += (a.z - b.z) * (a.x + b.x); + nz += (a.x - b.x) * (a.y + b.y); + } + } + let area = 0.5 * (nx * nx + ny * ny + nz * nz).sqrt(); + (area, perimeter) +} + // ── Solid3D ─────────────────────────────────────────────────────────────────── impl Grippable for Solid3D { @@ -132,11 +165,12 @@ impl Grippable for Region { impl PropertyEditable for Region { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + let (area, perimeter) = region_area_perimeter(&self.wires); vec![PropSection { title: "Geometry".into(), props: vec![ - ro("Area", "rgn_area", String::new()), - ro("Perimeter", "rgn_perimeter", String::new()), + ro("Area", "rgn_area", format!("{area:.4}")), + ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}")), ], }] } diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 27d07d6a..a6d608a8 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -133,6 +133,30 @@ fn properties(spline: &Spline) -> Vec { let closed = spline.flags.closed || spline.flags.periodic; let yes_no = |b: bool| if b { "Yes" } else { "No" }; + // Knot parameterization method (R2013+ DWG); older splines report 0. + let knot_param = match spline.knot_parameterization { + 0 => "Chord", + 1 => "Square Root", + 2 => "Uniform", + _ => "Custom", + }; + + // Closed splines enclose an area; approximate it with the shoelace + // formula over the defining points projected to the XY plane, matching + // the polyline approximation already used for Length. + let area = if closed && pts.len() >= 3 { + let mut acc = 0.0; + for w in pts.windows(2) { + acc += w[0].x * w[1].y - w[1].x * w[0].y; + } + if let (Some(first), Some(last)) = (pts.first(), pts.last()) { + acc += last.x * first.y - first.x * last.y; + } + format!("{:.4}", acc.abs() * 0.5) + } else { + String::new() + }; + vec![ PropSection { title: "Data Points".into(), @@ -161,7 +185,7 @@ fn properties(spline: &Spline) -> Vec { 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("Knot Parameterization", "knot_param", knot_param), ro( "Fit Point Count", "fit_pt_count", @@ -198,7 +222,7 @@ fn properties(spline: &Spline) -> 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()), + ro("Area", "area", area), ], }, ]