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]]
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -150,7 +150,15 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||
),
|
||||
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)),
|
||||
|
|
|
|||
|
|
@ -231,7 +231,11 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
|||
|
||||
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()),
|
||||
|
|
|
|||
|
|
@ -272,7 +272,14 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
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<PropSection> {
|
||||
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}")),
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,30 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
|||
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<PropSection> {
|
|||
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<PropSection> {
|
|||
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),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue