feat(properties): fill placeholder rows from acadrust fields + doc lookups
Bump acadrust to a46cb8b (spline knot parameterization + mtext line-space style now stored) and populate rows the builders left blank: Entity-local: spline Knot Parameterization + closed-spline Area (shoelace), mtext Line space style, hatch Spacing (pattern-line offset), leader Text offset (annotation offset), region Area + Perimeter (wire-loop approx). Doc-dependent (resolved in app/properties.rs where the document is reachable): insert Block Unit + Unit factor (block record units vs INSUNITS), underlay Name + Path (definition object), leader Text style / vertical placement / overall scale and tolerance Text style (dimension style), multileader Max points + segment angles (mleader style). Rows needing an annotation-scale / sheet-set subsystem or an internal PDF/DWF layer parser are intentionally left blank. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0a9c27e39d
commit
f50ad0dde4
7 changed files with 258 additions and 8 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
if !group_names.is_empty() {
|
||||||
let label = group_names.join(", ");
|
let label = group_names.join(", ");
|
||||||
if let Some(general) = sections.first_mut() {
|
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.
|
/// Convert INSUNITS (DXF group 70) to millimetres.
|
||||||
/// 0 = unitless / unknown: returns 1.0 so the caller treats it as "do not scale".
|
/// 0 = unitless / unknown: returns 1.0 so the caller treats it as "do not scale".
|
||||||
fn insunits_to_mm(code: i16) -> f64 {
|
fn insunits_to_mm(code: i16) -> f64 {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,15 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
||||||
),
|
),
|
||||||
ro("Layer override", "layer_override", String::new()),
|
ro("Layer override", "layer_override", String::new()),
|
||||||
ro("Double", "double", if h.is_double { "Yes" } else { "No" }),
|
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("ISO pen width", "iso_pen_width", String::new()),
|
||||||
ro("Gradient colors", "gradient_colors", g.colors.len().to_string()),
|
ro("Gradient colors", "gradient_colors", g.colors.len().to_string()),
|
||||||
ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)),
|
ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)),
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,11 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
||||||
|
|
||||||
let text = vec![
|
let text = vec![
|
||||||
edit("Text height", "text_height", leader.text_height),
|
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 style", "text_style", String::new()),
|
||||||
ro("Text color", "text_color", color_str(&leader.override_color)),
|
ro("Text color", "text_color", color_str(&leader.override_color)),
|
||||||
ro("Text position vert", "text_pos_vert", String::new()),
|
ro("Text position vert", "text_pos_vert", String::new()),
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,14 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
"line_space_distance",
|
"line_space_distance",
|
||||||
format!("{line_space_distance:.4}"),
|
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(
|
ro(
|
||||||
"Background mask",
|
"Background mask",
|
||||||
"background_mask",
|
"background_mask",
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,39 @@ fn translate_wires(wires: &mut Vec<acadrust::entities::Wire>, 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 ───────────────────────────────────────────────────────────────────
|
// ── Solid3D ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
impl Grippable for Solid3D {
|
impl Grippable for Solid3D {
|
||||||
|
|
@ -132,11 +165,12 @@ impl Grippable for Region {
|
||||||
|
|
||||||
impl PropertyEditable for Region {
|
impl PropertyEditable for Region {
|
||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
|
let (area, perimeter) = region_area_perimeter(&self.wires);
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: "Geometry".into(),
|
title: "Geometry".into(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro("Area", "rgn_area", String::new()),
|
ro("Area", "rgn_area", format!("{area:.4}")),
|
||||||
ro("Perimeter", "rgn_perimeter", String::new()),
|
ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}")),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,30 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
||||||
let closed = spline.flags.closed || spline.flags.periodic;
|
let closed = spline.flags.closed || spline.flags.periodic;
|
||||||
let yes_no = |b: bool| if b { "Yes" } else { "No" };
|
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![
|
vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: "Data Points".into(),
|
title: "Data Points".into(),
|
||||||
|
|
@ -161,7 +185,7 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
||||||
format!("{:.4}", cp0.map(|p| p.z).unwrap_or(0.0)),
|
format!("{:.4}", cp0.map(|p| p.z).unwrap_or(0.0)),
|
||||||
),
|
),
|
||||||
ro("Weight", "weight", format!("{:.4}", w0.unwrap_or(1.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(
|
ro(
|
||||||
"Fit Point Count",
|
"Fit Point Count",
|
||||||
"fit_pt_count",
|
"fit_pt_count",
|
||||||
|
|
@ -198,7 +222,7 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
||||||
ro("Closed", "closed", yes_no(closed)),
|
ro("Closed", "closed", yes_no(closed)),
|
||||||
ro("Planar", "planar", yes_no(spline.flags.planar)),
|
ro("Planar", "planar", yes_no(spline.flags.planar)),
|
||||||
ro("Length", "length", format!("{length:.4}")),
|
ro("Length", "length", format!("{length:.4}")),
|
||||||
ro("Area", "area", String::new()),
|
ro("Area", "area", area),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue