feat(properties): conform entity property groups to PROPERTIES.md

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-02 23:03:18 +03:00
commit b9cebab951
26 changed files with 1913 additions and 1554 deletions

View file

@ -2,7 +2,9 @@ use acadrust::entities::Arc;
use truck_modeling::{builder, Point3}; use truck_modeling::{builder, Point3};
use crate::command::EntityTransform; 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::entities::traits::TruckConvertible;
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
@ -122,6 +124,31 @@ fn grips(arc: &Arc) -> Vec<GripDef> {
} }
fn properties(arc: &Arc) -> Vec<PropSection> { fn properties(arc: &Arc) -> Vec<PropSection> {
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 { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
@ -129,12 +156,21 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
edit("Center Y", "center_y", arc.center.y), edit("Center Y", "center_y", arc.center.y),
edit("Center Z", "center_z", arc.center.z), edit("Center Z", "center_z", arc.center.z),
edit("Radius", "radius", arc.radius), edit("Radius", "radius", arc.radius),
edit( edit("Start angle", "start_angle", sa.to_degrees()),
"Start Angle (deg)", edit("End angle", "end_angle", ea.to_degrees()),
"start_angle", ro("Total angle", "total_angle", format!("{total_angle:.2}")),
arc.start_angle.to_degrees(), ro("Arc length", "arc_length", format!("{arc_length:.4}")),
), ro("Area", "area", format!("{area:.4}")),
edit("End Angle (deg)", "end_angle", arc.end_angle.to_degrees()), 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, "radius" if v > 0.0 => arc.radius = v,
"start_angle" => arc.start_angle = v.to_radians(), "start_angle" => arc.start_angle = v.to_radians(),
"end_angle" => arc.end_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,
_ => {} _ => {}
} }
} }

View file

@ -385,26 +385,33 @@ impl Grippable for AttributeDefinition {
impl PropertyEditable for AttributeDefinition { impl PropertyEditable for AttributeDefinition {
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> {
let mut props = vec![ let mut text_props = vec![
ro("Tag", "att_tag", self.tag.clone()), ro("Tag", "att_tag", self.tag.clone()),
ro("Prompt", "att_prompt", self.prompt.clone()), ro("Prompt", "att_prompt", self.prompt.clone()),
Property { Property {
label: "Default".into(), label: "Value".into(),
field: "att_default", field: "att_default",
value: PropValue::EditText(self.default_value.clone()), 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 { 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", field: "att_halign",
value: PropValue::Choice { value: PropValue::Choice {
selected: halign_str(self.horizontal_alignment).to_string(), selected: halign_str(self.horizontal_alignment).to_string(),
@ -425,63 +432,71 @@ impl PropertyEditable for AttributeDefinition {
.collect(), .collect(),
}, },
}, },
Property { edit("Height", "att_h", self.height),
label: "Style".into(), edit("Rotation", "att_rot", self.rotation.to_degrees()),
field: "att_style", edit("Width factor", "att_wf", self.width_factor),
value: PropValue::Choice { edit("Obliquing", "att_ob", self.oblique_angle.to_degrees()),
selected: if self.text_style.trim().is_empty() { edit("Text alignment X", "att_ax", self.alignment_point.x),
"Standard".into() edit("Text alignment Y", "att_ay", self.alignment_point.y),
} else { edit("Text alignment Z", "att_az", self.alignment_point.z),
self.text_style.clone()
},
options: text_style_names.to_vec(),
},
},
ro( ro(
"Generation", "Boundary width",
"att_gen_flags",
format!("{:#06b}", self.text_generation_flags & 0xff),
),
ro(
"Field Length",
"att_field_len", "att_field_len",
self.field_length.to_string(), 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( ro(
"Annotative", "Upside down",
"att_annotative", "att_upside_down",
bool_yn(self.flags.annotative), 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 // 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 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()); p.value = PropValue::ReadOnly(self.default_value.clone());
} }
} }
vec![PropSection { vec![
title: "Geometry".into(), PropSection {
props, 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) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
@ -626,101 +641,108 @@ impl Grippable for AttributeEntity {
impl PropertyEditable for AttributeEntity { impl PropertyEditable for AttributeEntity {
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { vec![
title: "Geometry".into(), PropSection {
props: vec![ title: "Text".into(),
ro("Tag", "atte_tag", self.tag.clone()), props: vec![
Property { ro("Tag", "atte_tag", self.tag.clone()),
label: "Value".into(), ro("Prompt", "atte_prompt", String::new()),
field: "atte_val", Property {
value: PropValue::EditText(self.value.clone()), label: "Value".into(),
}, field: "atte_val",
edit("Insert X", "atte_ix", self.insertion_point.x), value: PropValue::EditText(self.value.clone()),
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(),
}, },
}, Property {
Property { label: "Style".into(),
label: "V-Align".into(), field: "atte_style",
field: "atte_valign", value: PropValue::Choice {
value: PropValue::Choice { selected: if self.text_style.trim().is_empty() {
selected: valign_str(self.vertical_alignment).to_string(), "Standard".into()
options: ["Baseline", "Bottom", "Middle", "Top"] } else {
.into_iter() self.text_style.clone()
.map(str::to_string) },
.collect(), options: text_style_names.to_vec(),
},
},
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(),
}, },
}, Property {
ro( label: "Justify".into(),
"Generation", field: "atte_halign",
"atte_gen_flags", value: PropValue::Choice {
format!("{:#06b}", self.text_generation_flags & 0xff), selected: halign_str(self.horizontal_alignment).to_string(),
), options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"]
ro( .into_iter()
"Field Length", .map(str::to_string)
"atte_field_len", .collect(),
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 {
ro("Invisible", "atte_invisible", bool_yn(self.flags.invisible)), label: "V-Align".into(),
ro("Constant", "atte_constant", bool_yn(self.flags.constant)), field: "atte_valign",
ro("Verify", "atte_verify", bool_yn(self.flags.verify)), value: PropValue::Choice {
ro("Preset", "atte_preset", bool_yn(self.flags.preset)), selected: valign_str(self.vertical_alignment).to_string(),
ro( options: ["Baseline", "Bottom", "Middle", "Top"]
"Annotative", .into_iter()
"atte_annotative", .map(str::to_string)
bool_yn(self.flags.annotative), .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) { fn apply_geom_prop(&mut self, field: &str, value: &str) {

View file

@ -119,23 +119,26 @@ fn grips(circle: &Circle) -> Vec<GripDef> {
} }
fn properties(circle: &Circle) -> Vec<PropSection> { fn properties(circle: &Circle) -> Vec<PropSection> {
use std::f64::consts::PI;
let r = circle.radius;
vec![PropSection { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
edit("Center X", "center_x", circle.center.x), edit("Center X", "center_x", circle.center.x),
edit("Center Y", "center_y", circle.center.y), edit("Center Y", "center_y", circle.center.y),
edit("Center Z", "center_z", circle.center.z), edit("Center Z", "center_z", circle.center.z),
edit("Radius", "radius", circle.radius), edit("Radius", "radius", r),
ro( ro("Diameter", "diameter", format!("{:.4}", r * 2.0)),
"Diameter",
"diameter",
format!("{:.4}", circle.radius * 2.0),
),
ro( ro(
"Circumference", "Circumference",
"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_y" => circle.center.y = v,
"center_z" => circle.center.z = v, "center_z" => circle.center.z = v,
"radius" if v > 0.0 => circle.radius = 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,
_ => {} _ => {}
} }
} }

View file

@ -2,7 +2,9 @@ use acadrust::entities::Ellipse;
use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire}; use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire};
use crate::command::EntityTransform; 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::entities::traits::TruckConvertible;
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
@ -159,32 +161,81 @@ fn grips(ell: &Ellipse) -> Vec<GripDef> {
} }
fn properties(ell: &Ellipse) -> Vec<PropSection> { fn properties(ell: &Ellipse) -> Vec<PropSection> {
let r_major = (ell.major_axis.x * ell.major_axis.x use crate::entities::traits::MassPropsCalc;
+ ell.major_axis.y * ell.major_axis.y
+ ell.major_axis.z * ell.major_axis.z) let r_major = ell.major_axis_length();
.sqrt(); let r_minor = ell.minor_axis_length();
vec![PropSection {
title: "Geometry".into(), // Axis frame in WCS: major-axis unit vector `u`, minor direction `v = normal × u`.
props: vec![ let u = glam::DVec3::new(ell.major_axis.x, ell.major_axis.y, ell.major_axis.z);
edit("Center X", "center_x", ell.center.x), let u = if r_major > 1e-12 {
edit("Center Y", "center_y", ell.center.y), u / r_major
edit("Center Z", "center_z", ell.center.z), } else {
ro("Major Radius", "major_r", format!("{r_major:.4}")), glam::DVec3::X
ro( };
"Minor Radius", let n = glam::DVec3::new(ell.normal.x, ell.normal.y, ell.normal.z);
"minor_r", let v = n.cross(u);
format!("{:.4}", r_major * ell.minor_axis_ratio),
), // Parametric end point in WCS: center + r_major·cos(t1)·u + r_minor·sin(t1)·v.
ro( let t1 = ell.end_parameter;
"Minor/Major", let end = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z)
"ratio", + u * (r_major * t1.cos())
format!("{:.4}", ell.minor_axis_ratio), + 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) { fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
match (grip_id, apply) { match (grip_id, apply) {

View file

@ -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::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry};
use crate::scene::model::wire_model::SnapHint; 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::<Vec<_>>()
} else {
s.control_points
.iter()
.map(|p| [p.x, p.y])
.collect::<Vec<_>>()
};
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<PropSection> { fn properties(h: &Hatch) -> Vec<PropSection> {
let pattern_type = match h.pattern_type { let pattern_type = match h.pattern_type {
acadrust::entities::HatchPatternType::Predefined => "Predefined", acadrust::entities::HatchPatternType::Predefined => "Predefined",
@ -19,61 +72,109 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
acadrust::entities::HatchStyleType::Outer => "Outer", acadrust::entities::HatchStyleType::Outer => "Outer",
acadrust::entities::HatchStyleType::Ignore => "Ignore", acadrust::entities::HatchStyleType::Ignore => "Ignore",
}; };
let fill_type = if h.gradient_color.enabled { let area = boundary_area(h);
format!("Gradient ({})", h.gradient_color.name) let g = &h.gradient_color;
} else if h.is_solid { let grad_c1 = g.colors.first().map(|e| color_str(e.color)).unwrap_or_default();
"Solid".into() let grad_c2 = g.colors.get(1).map(|e| color_str(e.color)).unwrap_or_default();
} else {
format!("Pattern ({})", h.pattern.name) if g.enabled {
}; // ── Gradient fill ──────────────────────────────────────────────────
let boundary_count: usize = h let grad_type = if g.is_single_color { "One color" } else { "Two color" };
.paths let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" };
.iter() return vec![
.map(|p| { PropSection {
p.edges title: "Pattern".into(),
.iter() props: vec![
.map(|e| match e { ro("Type", "fill_type", grad_type),
BoundaryEdge::Polyline(poly) => poly.vertices.len(), ro("Gradient name", "gradient_name", g.name.clone()),
_ => 1, ro("Color 1", "gradient_color_1", grad_c1),
}) ro("Color 2", "gradient_color_2", grad_c2),
.sum::<usize>() edit("Angle", "pattern_angle", g.angle.to_degrees()),
}) ro("Centered", "gradient_centered", centered),
.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()),
}, },
ro("Pattern Type", "pattern_type", pattern_type), PropSection {
edit( title: "Geometry".into(),
"Pattern Angle", props: vec![
"pattern_angle", edit("Elevation", "elevation", h.elevation),
h.pattern_angle.to_degrees(), ro("Origin X", "origin_x", String::new()),
), ro("Origin Y", "origin_y", String::new()),
edit("Pattern Scale", "pattern_scale", h.pattern_scale), ro("Area", "area", format!("{:.4}", area)),
ro("Style", "style", style), ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
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" }), PropSection {
ro( title: "Misc".into(),
"Associative", props: vec![
"associative", ro(
if h.is_associative { "Yes" } else { "No" }, "Associative",
), "associative",
edit("Elevation", "elevation", h.elevation), if h.is_associative { "Yes" } else { "No" },
ro("Seed Points", "seed_count", h.seed_points.len().to_string()), ),
ro("Pixel Size", "pixel_size", format!("{:.6}", h.pixel_size)), ro("Annotative", "annotative", String::new()),
ro( ro("Island detection style", "style", style),
"Normal", ro("Background color", "background_color", String::new()),
"normal", ],
format!("{:.3}, {:.3}, {:.3}", h.normal.x, h.normal.y, h.normal.z), },
), ];
], }
}]
// ── 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) { 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; return;
}; };
match field { 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_angle" => h.pattern_angle = v.to_radians(),
"pattern_scale" if v > 0.0 => h.pattern_scale = v, "pattern_scale" if v > 0.0 => h.pattern_scale = v,
"elevation" => h.elevation = v, "elevation" => h.elevation = v,

View file

@ -6,7 +6,7 @@ use glam::Vec3;
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; 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::model::wire_model::WireModel;
use crate::scene::cache::block_cache; use crate::scene::cache::block_cache;
use crate::scene::convert::tessellate; use crate::scene::convert::tessellate;
@ -20,19 +20,58 @@ fn grips(ins: &Insert) -> Vec<GripDef> {
} }
fn properties(ins: &Insert) -> Vec<PropSection> { fn properties(ins: &Insert) -> Vec<PropSection> {
vec![PropSection { let annotative = ins
title: "Geometry".into(), .common
props: vec![ .extended_data
edit("Insert X", "ins_x", ins.insert_point.x), .get_record("AcAnnotativeData")
edit("Insert Y", "ins_y", ins.insert_point.y), .is_some();
edit("Insert Z", "ins_z", ins.insert_point.z), let attrs: Vec<Property> = ins
edit("Scale X", "x_scale", ins.x_scale()), .attributes
edit("Scale Y", "y_scale", ins.y_scale()), .iter()
edit("Scale Z", "z_scale", ins.z_scale()), .map(|a| Property {
edit("Rotation", "rotation", ins.rotation.to_degrees()), label: a.tag.clone(),
ro("Block", "block", ins.block_name.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) { fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) {

View file

@ -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<PropSection> { fn properties(leader: &Leader) -> Vec<PropSection> {
let n = leader.vertices.len(); let n = leader.vertices.len();
let mut props = vec![
// Style let arrow_size = leader.text_height.max(1.0);
Property {
label: "Dim Style".into(), let misc = vec![
field: "dimension_style",
value: PropValue::EditText(leader.dimension_style.clone()),
},
// Path type
choice_prop( choice_prop(
"Path Type", "Type",
"path_type", "path_type",
path_type_str(&leader.path_type), path_type_str(&leader.path_type),
&["Straight", "Spline"], &["Straight", "Spline"],
), ),
// Creation type
choice_prop( choice_prop(
"Creation Type", "Annotation type",
"creation_type", "creation_type",
creation_type_str(&leader.creation_type), creation_type_str(&leader.creation_type),
&["With Text", "With Tolerance", "With Block", "No Annotation"], &["With Text", "With Tolerance", "With Block", "No Annotation"],
), ),
// Arrow bool_toggle("Arrowhead", "arrow_enabled", leader.arrow_enabled),
bool_toggle("Arrow", "arrow_enabled", leader.arrow_enabled), Property {
// Hookline label: "Dim style".into(),
bool_toggle("Hookline", "hookline_enabled", leader.hookline_enabled), field: "dimension_style",
choice_prop( value: PropValue::EditText(leader.dimension_style.clone()),
"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),
},
),
]; ];
// 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() { if let Some(a) = leader.arrow_point() {
props.push(edit("Arrow X", "arrow_x", a.x)); geometry.push(edit("Vertex X", "vertex_x", a.x));
props.push(edit("Arrow Y", "arrow_y", a.y)); geometry.push(edit("Vertex Y", "vertex_y", a.y));
props.push(edit("Arrow Z", "arrow_z", a.z)); 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) vec![
if n >= 2 { PropSection {
if let Some(e) = leader.end_point() { title: "Misc".into(),
props.push(edit("End X", "end_x", e.x)); props: misc,
props.push(edit("End Y", "end_y", e.y)); },
props.push(edit("End Z", "end_z", e.z)); PropSection {
} title: "Lines & Arrows".into(),
} props: lines_arrows,
},
vec![PropSection { PropSection {
title: "Geometry".into(), title: "Text".into(),
props, 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) { 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; leader.annotation_offset.z = v;
} }
} }
"arrow_x" => { "vertex_x" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
vert.x = v; vert.x = v;
} }
} }
"arrow_y" => { "vertex_y" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
vert.y = v; vert.y = v;
} }
} }
"arrow_z" => { "vertex_z" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) { if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
vert.z = v; 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;
}
}
}
_ => {} _ => {}
} }
} }

View file

@ -73,6 +73,10 @@ fn grips(line: &Line) -> Vec<GripDef> {
} }
fn properties(line: &Line) -> Vec<PropSection> { fn properties(line: &Line) -> Vec<PropSection> {
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 { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
@ -82,7 +86,11 @@ fn properties(line: &Line) -> Vec<PropSection> {
edit("End X", "end_x", line.end.x), edit("End X", "end_x", line.end.x),
edit("End Y", "end_y", line.end.y), edit("End Y", "end_y", line.end.y),
edit("End Z", "end_z", line.end.z), 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("Length", "length", format!("{:.4}", line.length())),
ro("Angle", "angle", format!("{angle:.2}")),
], ],
}] }]
} }

View file

@ -323,21 +323,40 @@ fn grips(pline: &LwPolyline) -> Vec<GripDef> {
} }
fn properties(pline: &LwPolyline) -> Vec<PropSection> { fn properties(pline: &LwPolyline) -> Vec<PropSection> {
let (length, area) = length_and_area(pline); let v0 = pline.vertices.first();
vec![PropSection { let vx = v0.map_or(0.0, |v| v.location.x);
title: "Geometry".into(), let vy = v0.map_or(0.0, |v| v.location.y);
props: vec![ let start_w = v0.map_or(0.0, |v| v.start_width);
ro("Vertices", "vertices", pline.vertices.len().to_string()), let end_w = v0.map_or(0.0, |v| v.end_width);
ro( vec![
"Closed", PropSection {
"closed", title: "Geometry".into(),
if pline.is_closed { "Yes" } else { "No" }, props: vec![
), ro("Current Vertex", "current_vertex", String::new()),
edit("Elevation", "elevation", pline.elevation), edit("Vertex X", "vertex_x", vx),
ro("Length", "length", format!("{length:.4}")), edit("Vertex Y", "vertex_y", vy),
ro("Area", "area", format!("{area:.4}")), 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 /// 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 { let Some(v) = parse_f64(value) else {
return; return;
}; };
if field == "elevation" { match field {
pline.elevation = v; "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;
}
}
_ => {}
} }
} }

View file

@ -117,21 +117,28 @@ impl Grippable for Face3D {
impl PropertyEditable for Face3D { impl PropertyEditable for Face3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
use crate::entities::common::edit_prop as edit; use crate::entities::common::edit_prop as edit;
let inv = self.invisible_edges;
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
vec![PropSection { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
edit("P1 X", "f3_p1x", self.first_corner.x), ro("Current vertex", "f3_current", String::new()),
edit("P1 Y", "f3_p1y", self.first_corner.y), edit("Vertex 1 X", "f3_p1x", self.first_corner.x),
edit("P1 Z", "f3_p1z", self.first_corner.z), edit("Vertex 1 Y", "f3_p1y", self.first_corner.y),
edit("P2 X", "f3_p2x", self.second_corner.x), edit("Vertex 1 Z", "f3_p1z", self.first_corner.z),
edit("P2 Y", "f3_p2y", self.second_corner.y), edit("Vertex 2 X", "f3_p2x", self.second_corner.x),
edit("P2 Z", "f3_p2z", self.second_corner.z), edit("Vertex 2 Y", "f3_p2y", self.second_corner.y),
edit("P3 X", "f3_p3x", self.third_corner.x), edit("Vertex 2 Z", "f3_p2z", self.second_corner.z),
edit("P3 Y", "f3_p3y", self.third_corner.y), edit("Vertex 3 X", "f3_p3x", self.third_corner.x),
edit("P3 Z", "f3_p3z", self.third_corner.z), edit("Vertex 3 Y", "f3_p3y", self.third_corner.y),
edit("P4 X", "f3_p4x", self.fourth_corner.x), edit("Vertex 3 Z", "f3_p3z", self.third_corner.z),
edit("P4 Y", "f3_p4y", self.fourth_corner.y), edit("Vertex 4 X", "f3_p4x", self.fourth_corner.x),
edit("P4 Z", "f3_p4z", self.fourth_corner.z), 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::Cubic => "Cubic",
acadrust::entities::polygon_mesh::SurfaceSmoothType::Bezier => "Bezier", acadrust::entities::polygon_mesh::SurfaceSmoothType::Bezier => "Bezier",
}; };
vec![PropSection { let yesno = |b: bool| if b { "Yes" } else { "No" };
title: "Geometry".into(), let first = self.vertices.first();
props: vec![ // Grid faces: one quad per cell; closed direction adds a wrap row/column.
ro("M count", "pm_m", self.m_vertex_count.to_string()), let m = self.m_vertex_count.max(0) as i64;
ro("N count", "pm_n", self.n_vertex_count.to_string()), let n = self.n_vertex_count.max(0) as i64;
ro("Vertices", "pm_v", self.vertices.len().to_string()), let cells_m = if self.is_closed_m() { m } else { (m - 1).max(0) };
ro("Smooth Type", "pm_smooth", smooth), let cells_n = if self.is_closed_n() { n } else { (n - 1).max(0) };
ro( let face_count = cells_m * cells_n;
"Smooth M Density", vec![
"pm_smooth_m", PropSection {
self.m_smooth_density.to_string(), title: "Geometry".into(),
), props: vec![
ro( ro("Vertex", "pm_vertex", String::new()),
"Smooth N Density", ro(
"pm_smooth_n", "Vertex X",
self.n_smooth_density.to_string(), "pm_vx",
), first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
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
), ),
), 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) {} 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::Cubic => "Cubic",
acadrust::entities::PolyfaceSmoothType::Bezier => "Bezier", acadrust::entities::PolyfaceSmoothType::Bezier => "Bezier",
}; };
vec![PropSection { let first = self.vertices.first();
title: "Geometry".into(), vec![
props: vec![ PropSection {
ro("Vertices", "pfm_v", self.vertices.len().to_string()), title: "Geometry".into(),
ro("Faces", "pfm_f", self.faces.len().to_string()), props: vec![
ro("Smooth Surface", "pfm_smooth", smooth), ro("Vertex", "pfm_vertex", String::new()),
ro( ro(
"Seqend", "Vertex X",
"pfm_seqend_handle", "pfm_vx",
match self.seqend_handle { first.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default(),
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
), ),
), ro(
ro( "Vertex Y",
"Thickness", "pfm_vy",
"pfm_thickness", first.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default(),
format!("{:.4}", self.thickness), ),
), 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) {} fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
@ -598,24 +620,35 @@ impl Grippable for Mesh {
impl PropertyEditable for Mesh { impl PropertyEditable for Mesh {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
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 { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ 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( ro(
"Subdivision Level", "Level of Smoothness",
"msh_subdiv", "msh_subdiv",
self.subdivision_level.to_string(), 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( ro(
"Blend Crease", "Watertight",
"msh_blend_crease", "msh_watertight",
if self.blend_crease { "Yes" } else { "No" }, if watertight { "Yes" } else { "No" },
), ),
], ],
}] }]

View file

@ -194,46 +194,39 @@ impl PropertyEditable for MLine {
acadrust::entities::MLineJustification::Zero => "Zero", acadrust::entities::MLineJustification::Zero => "Zero",
acadrust::entities::MLineJustification::Bottom => "Bottom", acadrust::entities::MLineJustification::Bottom => "Bottom",
}; };
vec![PropSection { let cur = self.vertices.first();
title: "Geometry".into(), let cur_x = cur.map(|v| v.position.x).unwrap_or(0.0);
props: vec![ let cur_y = cur.map(|v| v.position.y).unwrap_or(0.0);
ro("Style", "ml_style", self.style_name.clone()), let cur_z = cur.map(|v| v.position.z).unwrap_or(0.0);
ro( vec![
"Style Handle", PropSection {
"ml_style_handle", title: "Geometry".into(),
match self.style_handle { props: vec![
Some(h) if !h.is_null() => format!("{:X}", h.value()), ro("Vertex", "ml_vertex", if self.vertices.is_empty() { String::new() } else { "1".to_string() }),
_ => "(none)".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(),
},
}, },
), edit("Style scale", "ml_scale", self.scale_factor),
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),
},
},
],
}]
} }
fn apply_geom_prop(&mut self, field: &str, value: &str) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
@ -261,8 +254,24 @@ impl PropertyEditable for MLine {
let Ok(v) = value.trim().parse::<f64>() else { let Ok(v) = value.trim().parse::<f64>() else {
return; return;
}; };
if field == "ml_scale" && v != 0.0 { match field {
self.scale_factor = v; "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;
}
}
_ => {}
} }
} }
} }

View file

@ -192,69 +192,115 @@ fn grips(t: &MText) -> Vec<GripDef> {
vec![square_grip(0, p), triangle_grip(1, width_grip)] 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<PropSection> { fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { // Absolute line-space distance for single spacing (~1.66 * height) scaled
title: "Geometry".into(), // by the line-spacing factor.
props: vec![ let line_space_distance = t.height * 1.666_666_666_666_667 * t.line_spacing_factor;
edit("Insert X", "ins_x", t.insertion_point.x), let bg_mask_on = (t.background_fill_flags & 0x03) != 0;
edit("Insert Y", "ins_y", t.insertion_point.y), let text_frame_on = (t.background_fill_flags & 0x10) != 0;
edit("Insert Z", "ins_z", t.insertion_point.z), vec![
edit("Height", "height", t.height), PropSection {
edit("Width", "rect_w", t.rectangle_width), title: "Text".into(),
edit("Rect Height", "rect_h", t.rectangle_height.unwrap_or(0.0)), props: vec![
edit("Rotation", "rotation", t.rotation.to_degrees()), Property {
edit("Line Spacing", "line_spacing", t.line_spacing_factor), label: "Contents".into(),
Property { field: "content",
label: "H-Align".into(), value: PropValue::EditText(t.value.clone()),
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 {
Property { label: "Style".into(),
label: "V-Align".into(), field: "style",
field: "v_align", value: PropValue::Choice {
value: PropValue::Choice { selected: if t.style.trim().is_empty() {
selected: mtext_valign_str(&t.attachment_point).to_string(), "Standard".into()
options: ["Top", "Middle", "Bottom"] } else {
.into_iter() t.style.clone()
.map(str::to_string) },
.collect(), options: text_style_names.to_vec(),
},
},
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()
}, },
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) { fn apply_geom_prop(t: &mut MText, field: &str, value: &str) {

View file

@ -520,204 +520,186 @@ fn choice(label: &str, field: &'static str, selected: &str, opts: &[&str]) -> Pr
} }
} }
fn hexh(h: Option<acadrust::Handle>) -> String {
match h {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
}
}
fn properties(ml: &MultiLeader) -> Vec<PropSection> { fn properties(ml: &MultiLeader) -> Vec<PropSection> {
let ctx = &ml.context; let ctx = &ml.context;
let total_pts: usize = ctx let first_root = ctx.leader_roots.first();
.leader_roots
.iter()
.flat_map(|r| r.lines.iter())
.map(|l| l.points.len())
.sum();
let mut props = vec![ // ── Misc ─────────────────────────────────────────────────────────────
// Content let misc = PropSection {
choice( title: "Misc".into(),
"Content Type", props: vec![
"content_type", ro("Leader type", "path_type", path_type_str(&ml.path_type)),
content_type_str(&ml.content_type), choice(
&["None", "MText", "Block", "Tolerance"], "Content type",
), "content_type",
Property { content_type_str(&ml.content_type),
label: "Text".into(), &["None", "MText", "Block", "Tolerance"],
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
), ),
), edit("Overall scale", "scale_factor", ml.scale_factor),
ro( ro(
"Block Rotation", "Annotative",
"block_rotation", "enable_annotation_scale",
format!("{:.3}°", ml.block_rotation.to_degrees()), 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) // ── Leaders ──────────────────────────────────────────────────────────
if let Some(root) = ctx.leader_roots.first() { let leaders = PropSection {
props.push(edit("Root Conn X", "conn_x", root.connection_point.x)); title: "Leaders".into(),
props.push(edit("Root Conn Y", "conn_y", root.connection_point.y)); props: vec![
props.push(edit("Root Conn Z", "conn_z", root.connection_point.z)); 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 { // ── Leader Structure ─────────────────────────────────────────────────
title: "Geometry".into(), let structure = PropSection {
props, 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) { 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; 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" => { "conn_x" => {
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) { if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
root.connection_point.x = v; root.connection_point.x = v;

View file

@ -79,42 +79,30 @@ fn properties(ole: &Ole2Frame) -> Vec<PropSection> {
OleObjectType::Embedded => "Embedded", OleObjectType::Embedded => "Embedded",
OleObjectType::Static => "Static", OleObjectType::Static => "Static",
}; };
vec![PropSection { let width = (ole.lower_right_corner.x - ole.upper_left_corner.x).abs();
title: "Geometry".into(), let height = (ole.upper_left_corner.y - ole.lower_right_corner.y).abs();
props: vec![ vec![
ro("Type", "ole_type", type_str), PropSection {
edit("Upper Left X", "ole_ulx", ole.upper_left_corner.x), title: "Geometry".into(),
edit("Upper Left Y", "ole_uly", ole.upper_left_corner.y), props: vec![
edit("Lower Right X", "ole_lrx", ole.lower_right_corner.x), edit("Position X", "ole_ulx", ole.upper_left_corner.x),
edit("Lower Right Y", "ole_lry", ole.lower_right_corner.y), edit("Position Y", "ole_uly", ole.upper_left_corner.y),
ro("Version", "ole_version", ole.version.to_string()), edit("Position Z", "ole_ulz", ole.upper_left_corner.z),
ro( ro("Width", "ole_width", format!("{:.4}", width)),
"Source App", ro("Height", "ole_height", format!("{:.4}", height)),
"ole_source_app", ro("Scale width", "ole_scale_width", String::new()),
if ole.source_application.is_empty() { ro("Scale height", "ole_scale_height", String::new()),
"(unknown)".to_string() ro("Lock aspect", "ole_lock_aspect", String::new()),
} else { ],
ole.source_application.clone() },
}, PropSection {
), title: "Misc".into(),
ro( props: vec![
"Data Size", ro("Type", "ole_type", type_str),
"ole_data_size", ro("Plot quality", "ole_plot_quality", String::new()),
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" },
),
],
}]
} }
fn apply_geom_prop(ole: &mut Ole2Frame, field: &str, value: &str) { 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 { match field {
"ole_ulx" => ole.upper_left_corner.x = v, "ole_ulx" => ole.upper_left_corner.x = v,
"ole_uly" => ole.upper_left_corner.y = 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_lrx" => ole.lower_right_corner.x = v,
"ole_lry" => ole.lower_right_corner.y = v, "ole_lry" => ole.lower_right_corner.y = v,
_ => {} _ => {}

View file

@ -182,9 +182,9 @@ fn properties(pt: &Point) -> Vec<PropSection> {
vec![PropSection { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
edit("X", "loc_x", pt.location.x), edit("Position X", "loc_x", pt.location.x),
edit("Y", "loc_y", pt.location.y), edit("Position Y", "loc_y", pt.location.y),
edit("Z", "loc_z", pt.location.z), edit("Position Z", "loc_z", pt.location.z),
], ],
}] }]
} }

View file

@ -2,7 +2,7 @@ use acadrust::entities::{Polyline, Polyline2D, Polyline3D};
use truck_modeling::{builder, Edge, Point3, Wire}; use truck_modeling::{builder, Edge, Point3, Wire};
use crate::command::EntityTransform; 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::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
@ -387,39 +387,58 @@ impl Grippable for Polyline2D {
impl PropertyEditable for Polyline2D { impl PropertyEditable for Polyline2D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let smooth = match self.smooth_surface { let n = self.vertices.len();
acadrust::entities::SmoothSurfaceType::None => "None", let mut area = 0.0;
acadrust::entities::SmoothSurfaceType::QuadraticBSpline => "Quadratic", let mut length = 0.0;
acadrust::entities::SmoothSurfaceType::CubicBSpline => "Cubic", let seg_count = if self.is_closed() { n } else { n.saturating_sub(1) };
acadrust::entities::SmoothSurfaceType::Bezier => "Bezier", for i in 0..seg_count {
}; let a = &self.vertices[i].location;
vec![PropSection { let b = &self.vertices[(i + 1) % n].location;
title: "Geometry".into(), area += a.x * b.y - b.x * a.y;
props: vec![ length += ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
ro("Vertices", "vertices", self.vertices.len().to_string()), }
edit("Elevation", "pl2_elevation", self.elevation), area = (area * 0.5).abs();
edit("Default Start W", "pl2_start_w", self.start_width),
edit("Default End W", "pl2_end_w", self.end_width), let v0 = self.vertices.first();
edit("Thickness", "pl2_thickness", self.thickness), let vertex_x = v0.map(|v| format!("{:.4}", v.location.x)).unwrap_or_default();
ro("Smooth Surface", "pl2_smooth", smooth), let vertex_y = v0.map(|v| format!("{:.4}", v.location.y)).unwrap_or_default();
ro( let seg_start_w = v0.map(|v| format!("{:.4}", v.start_width)).unwrap_or_default();
"Normal", let seg_end_w = v0.map(|v| format!("{:.4}", v.end_width)).unwrap_or_default();
"pl2_normal",
format!( vec![
"{:.3}, {:.3}, {:.3}", PropSection {
self.normal.x, self.normal.y, self.normal.z title: "Geometry".into(),
), props: vec![
), ro("Current Vertex", "pl2_current_vertex", if n > 0 { "1" } else { "" }),
Property { ro("Vertex X", "pl2_vertex_x", vertex_x),
label: "Closed".into(), ro("Vertex Y", "pl2_vertex_y", vertex_y),
field: "pl2_closed", ro("Start segment width", "pl2_seg_start_w", seg_start_w),
value: PropValue::BoolToggle { 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", 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) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
@ -437,29 +456,18 @@ impl PropertyEditable for Polyline2D {
} }
} }
"pl2_elevation" => { "pl2_elevation" => {
if let Ok(v) = value.trim().parse::<f64>() { if let Some(v) = parse_f64(value) {
self.elevation = v; self.elevation = v;
} }
} }
"pl2_start_w" => { "pl2_start_w" => {
if let Ok(v) = value.trim().parse::<f64>() { if let Some(v) = parse_f64(value) {
if v >= 0.0 { if v >= 0.0 {
self.start_width = v; self.start_width = v;
}
}
}
"pl2_end_w" => {
if let Ok(v) = value.trim().parse::<f64>() {
if v >= 0.0 {
self.end_width = v; self.end_width = v;
} }
} }
} }
"pl2_thickness" => {
if let Ok(v) = value.trim().parse::<f64>() {
self.thickness = v;
}
}
_ => {} _ => {}
} }
} }
@ -606,41 +614,43 @@ impl Grippable for Polyline3D {
impl PropertyEditable for Polyline3D { impl PropertyEditable for Polyline3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
use acadrust::entities::polyline3d::SmoothSurfaceType as SST; 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::None => "None",
SST::QuadraticBSpline => "Quadratic", SST::QuadraticBSpline => "Quadratic",
SST::CubicBSpline => "Cubic", SST::CubicBSpline => "Cubic",
SST::Bezier => "Bezier", SST::Bezier => "Bezier",
}; };
vec![PropSection {
title: "Geometry".into(), vec![
props: vec![ PropSection {
ro("Vertices", "vertices", self.vertices.len().to_string()), title: "Geometry".into(),
edit("Default Start W", "pl3_start_w", self.default_start_width), props: vec![
edit("Default End W", "pl3_end_w", self.default_end_width), ro("Vertex", "pl3_vertex", if n > 0 { "1" } else { "" }),
ro("Smooth", "pl3_smooth", smooth), ro("Vertex X", "pl3_vertex_x", vertex_x),
ro("Mesh M", "pl3_mesh_m", self.mesh_m_count.to_string()), ro("Vertex Y", "pl3_vertex_y", vertex_y),
ro("Mesh N", "pl3_mesh_n", self.mesh_n_count.to_string()), ro("Vertex Z", "pl3_vertex_z", vertex_z),
ro( ],
"Smooth M Density", },
"pl3_smooth_m", PropSection {
self.smooth_m_density.to_string(), title: "Misc".into(),
), props: vec![
ro( Property {
"Smooth N Density", label: "Closed".into(),
"pl3_smooth_n",
self.smooth_n_density.to_string(),
),
Property {
label: "Closed".into(),
field: "pl3_closed",
value: PropValue::BoolToggle {
field: "pl3_closed", 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) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
@ -655,14 +665,6 @@ impl PropertyEditable for Polyline3D {
} else { } else {
self.open(); self.open();
} }
return;
}
if let Ok(v) = value.trim().parse::<f64>() {
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,
_ => {}
}
} }
} }
} }

View file

@ -163,57 +163,101 @@ impl Grippable for RasterImage {
impl PropertyEditable for RasterImage { impl PropertyEditable for RasterImage {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { let rotation_deg = self.u_vector.y.atan2(self.u_vector.x).to_degrees();
title: "Geometry".into(), let scale = self.u_vector.length();
props: vec![ let show_image = self.flags.contains(acadrust::entities::ImageDisplayFlags::SHOW_IMAGE);
ro("File", "ri_file", self.file_path.clone()), let show_clipped = self
edit("Insert X", "ri_ox", self.insertion_point.x), .flags
edit("Insert Y", "ri_oy", self.insertion_point.y), .contains(acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY);
edit("Insert Z", "ri_oz", self.insertion_point.z), let clip_inverted = self.clip_boundary.clip_mode == acadrust::entities::ClipMode::Inside;
edit("Brightness", "ri_bright", self.brightness as f64), let transparency = format!("{:.0}%", self.common.transparency.as_percent() * 100.0);
edit("Contrast", "ri_contrast", self.contrast as f64), vec![
edit("Fade", "ri_fade", self.fade as f64), PropSection {
Property { title: "Geometry".into(),
label: "Clipping".into(), props: vec![
field: "ri_clip", edit("Position X", "ri_ox", self.insertion_point.x),
value: PropValue::BoolToggle { edit("Position Y", "ri_oy", self.insertion_point.y),
field: "ri_clip", edit("Position Z", "ri_oz", self.insertion_point.z),
value: self.clipping_enabled, 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,
},
}, },
}, Property {
ro( label: "Show clipped".into(),
"Class Version", field: "ri_show_clipped",
"ri_class_version", value: PropValue::BoolToggle {
self.class_version.to_string(), field: "ri_show_clipped",
), value: show_clipped,
ro( },
"Definition",
"ri_def_handle",
match self.definition_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
}, },
), Property {
ro( label: "Clip inverted".into(),
"Def Reactor", field: "ri_clip_inverted",
"ri_def_reactor_handle", value: PropValue::BoolToggle {
match self.definition_reactor_handle { field: "ri_clip_inverted",
Some(h) if !h.is_null() => format!("{:X}", h.value()), value: clip_inverted,
_ => "(none)".to_string(), },
}, },
), ],
], },
}] ]
} }
fn apply_geom_prop(&mut self, field: &str, value: &str) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
match field { match field {
"ri_clip" => { "ri_show_image" => {
self.clipping_enabled = if value == "toggle" { let on = if value == "toggle" {
!self.clipping_enabled !self.flags.contains(acadrust::entities::ImageDisplayFlags::SHOW_IMAGE)
} else { } else {
value == "true" 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; return;
} }
_ => {} _ => {}
@ -424,57 +468,91 @@ impl Grippable for Wipeout {
impl PropertyEditable for Wipeout { impl PropertyEditable for Wipeout {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { let show_image = self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE);
title: "Geometry".into(), let show_clipped = self
props: vec![ .flags
edit("Insert X", "wo_ox", self.insertion_point.x), .contains(acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY);
edit("Insert Y", "wo_oy", self.insertion_point.y), let bg_transparency = self
edit("Insert Z", "wo_oz", self.insertion_point.z), .flags
edit("Brightness", "wo_bright", self.brightness as f64), .contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON);
edit("Contrast", "wo_contrast", self.contrast as f64), vec![
edit("Fade", "wo_fade", self.fade as f64), PropSection {
Property { title: "Geometry".into(),
label: "Clipping".into(), props: vec![
field: "wo_clip", edit("Position X", "wo_ox", self.insertion_point.x),
value: PropValue::BoolToggle { edit("Position Y", "wo_oy", self.insertion_point.y),
field: "wo_clip", edit("Position Z", "wo_oz", self.insertion_point.z),
value: self.clipping_enabled, ],
},
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,
},
}, },
}, Property {
ro( label: "Show clipped".into(),
"Class Version", field: "wo_show_clipped",
"wo_class_version", value: PropValue::BoolToggle {
self.class_version.to_string(), field: "wo_show_clipped",
), value: show_clipped,
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 {
ro( label: "Background transparency".into(),
"Def Reactor", field: "wo_bg_transparency",
"wo_def_reactor_handle", value: PropValue::BoolToggle {
match self.definition_reactor_handle { field: "wo_bg_transparency",
Some(h) if !h.is_null() => format!("{:X}", h.value()), value: bg_transparency,
_ => "(none)".to_string(), },
}, },
), ],
], },
}] ]
} }
fn apply_geom_prop(&mut self, field: &str, value: &str) { fn apply_geom_prop(&mut self, field: &str, value: &str) {
match field { match field {
"wo_clip" => { "wo_show_image" => {
self.clipping_enabled = if value == "toggle" { let on = if value == "toggle" {
!self.clipping_enabled !self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE)
} else { } else {
value == "true" 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; return;
} }
_ => {} _ => {}
@ -486,9 +564,6 @@ impl PropertyEditable for Wipeout {
"wo_ox" => self.insertion_point.x = v, "wo_ox" => self.insertion_point.x = v,
"wo_oy" => self.insertion_point.y = v, "wo_oy" => self.insertion_point.y = v,
"wo_oz" => self.insertion_point.z = 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,
_ => {} _ => {}
} }
} }

View file

@ -92,12 +92,12 @@ impl PropertyEditable for Ray {
vec![PropSection { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
edit("Base X", "ray_bx", self.base_point.x), edit("Start X", "ray_bx", self.base_point.x),
edit("Base Y", "ray_by", self.base_point.y), edit("Start Y", "ray_by", self.base_point.y),
edit("Base Z", "ray_bz", self.base_point.z), edit("Start Z", "ray_bz", self.base_point.z),
edit("Dir X", "ray_dx", self.direction.x), edit("Direction vector X", "ray_dx", self.direction.x),
edit("Dir Y", "ray_dy", self.direction.y), edit("Direction vector Y", "ray_dy", self.direction.y),
edit("Dir Z", "ray_dz", self.direction.z), 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 X", "xl_bx", self.base_point.x),
edit("Base Y", "xl_by", self.base_point.y), edit("Base Y", "xl_by", self.base_point.y),
edit("Base Z", "xl_bz", self.base_point.z), edit("Base Z", "xl_bz", self.base_point.z),
edit("Dir X", "xl_dx", self.direction.x), edit("Direction vector X", "xl_dx", self.direction.x),
edit("Dir Y", "xl_dy", self.direction.y), edit("Direction vector Y", "xl_dy", self.direction.y),
edit("Dir Z", "xl_dz", self.direction.z), edit("Direction vector Z", "xl_dz", self.direction.z),
], ],
}] }]
} }

View file

@ -7,7 +7,7 @@
use acadrust::entities::Solid; use acadrust::entities::Solid;
use crate::command::EntityTransform; 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::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
@ -98,21 +98,25 @@ impl Grippable for Solid {
impl PropertyEditable for Solid { impl PropertyEditable for Solid {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
// 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 { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
edit("P1 X", "sl_p1x", self.first_corner.x), edit("Point 1 X", "sl_p1x", self.first_corner.x),
edit("P1 Y", "sl_p1y", self.first_corner.y), edit("Point 1 Y", "sl_p1y", self.first_corner.y),
edit("P1 Z", "sl_p1z", self.first_corner.z), edit("Point 1 Z", "sl_p1z", self.first_corner.z),
edit("P2 X", "sl_p2x", self.second_corner.x), edit("Point 2 X", "sl_p2x", self.second_corner.x),
edit("P2 Y", "sl_p2y", self.second_corner.y), edit("Point 2 Y", "sl_p2y", self.second_corner.y),
edit("P2 Z", "sl_p2z", self.second_corner.z), edit("Point 2 Z", "sl_p2z", self.second_corner.z),
edit("P3 X", "sl_p3x", self.third_corner.x), edit("Point 3 X", "sl_p3x", self.third_corner.x),
edit("P3 Y", "sl_p3y", self.third_corner.y), edit("Point 3 Y", "sl_p3y", self.third_corner.y),
edit("P3 Z", "sl_p3z", self.third_corner.z), edit("Point 3 Z", "sl_p3z", self.third_corner.z),
edit("P4 X", "sl_p4x", self.fourth_corner.x), edit("Point 4 X", "sl_p4x", self.fourth_corner.x),
edit("P4 Y", "sl_p4y", self.fourth_corner.y), edit("Point 4 Y", "sl_p4y", self.fourth_corner.y),
edit("P4 Z", "sl_p4z", self.fourth_corner.z), edit("Point 4 Z", "sl_p4z", self.fourth_corner.z),
ro("Elevation", "sl_elev", format!("{:.4}", elevation)),
edit("Thickness", "sl_thick", self.thickness), edit("Thickness", "sl_thick", self.thickness),
], ],
}] }]

View file

@ -10,7 +10,7 @@ use acadrust::entities::{Body, Region, Solid3D, Surface};
use glam::Vec3; use glam::Vec3;
use crate::command::EntityTransform; 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::entities::traits::{Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
@ -52,17 +52,6 @@ fn translate_wires(wires: &mut Vec<acadrust::entities::Wire>, 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 ─────────────────────────────────────────────────────────────────── // ── Solid3D ───────────────────────────────────────────────────────────────────
impl Grippable for Solid3D { impl Grippable for Solid3D {
@ -85,58 +74,40 @@ impl Grippable for Solid3D {
impl PropertyEditable for Solid3D { impl PropertyEditable for Solid3D {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let size = acis_size_str( let history = match self.history_handle {
self.acis_data.has_data(), Some(h) if !h.is_null() => format!("{:X}", h.value()),
self.acis_data.sat_data.len(), _ => "None".to_string(),
self.acis_data.sab_data.len(), };
self.acis_data.is_binary, vec![
); PropSection {
vec![PropSection { title: "Solid History".into(),
title: "Geometry".into(), props: vec![
props: vec![ ro("History", "s3d_history", history),
ro( ro("Show History", "s3d_show_history", String::new()),
"Ref Pt X", ],
"s3d_px", },
format!("{:.4}", self.point_of_reference.x), PropSection {
), title: "Geometry".into(),
ro( props: vec![
"Ref Pt Y", edit("Position X", "s3d_px", self.point_of_reference.x),
"s3d_py", edit("Position Y", "s3d_py", self.point_of_reference.y),
format!("{:.4}", self.point_of_reference.y), edit("Position Z", "s3d_pz", self.point_of_reference.z),
), ],
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(),
},
),
],
}]
} }
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 ──────────────────────────────────────────────────────────────────── // ── Region ────────────────────────────────────────────────────────────────────
@ -161,45 +132,11 @@ 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 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 { vec![PropSection {
title: "Geometry".into(), title: "Geometry".into(),
props: vec![ props: vec![
ro( ro("Area", "rgn_area", String::new()),
"Ref Pt X", ro("Perimeter", "rgn_perimeter", String::new()),
"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(),
),
], ],
}] }]
} }
@ -229,45 +166,11 @@ impl Grippable for Body {
impl PropertyEditable for Body { impl PropertyEditable for Body {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
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 { vec![PropSection {
title: "Geometry".into(), title: "Solid History".into(),
props: vec![ props: vec![
ro( ro("History", "bdy_history", String::new()),
"Ref Pt X", ro("Show History", "bdy_show_history", String::new()),
"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(),
),
], ],
}] }]
} }

View file

@ -5,7 +5,7 @@ use truck_modeling::{
}; };
use crate::command::EntityTransform; 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::entities::traits::TruckConvertible;
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
@ -107,21 +107,110 @@ fn grips(spline: &Spline) -> Vec<GripDef> {
} }
fn properties(spline: &Spline) -> Vec<PropSection> { fn properties(spline: &Spline) -> Vec<PropSection> {
vec![PropSection { let show = if spline.fit_points.is_empty() {
title: "Geometry".into(), "Control Vertices"
props: vec![ } else {
ro("Degree", "degree", spline.degree.to_string()), "Fit Points"
ro( };
"Control Pts", let cp0 = spline.control_points.first();
"ctrl_pts", let fp0 = spline.fit_points.first();
spline.control_points.len().to_string(), let w0 = spline.weights.first().copied();
),
ro("Fit Pts", "fit_pts", spline.fit_points.len().to_string()), // 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) { fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
if let Some(cp) = spline.control_points.get_mut(grip_id) { if let Some(cp) = spline.control_points.get_mut(grip_id) {

View file

@ -819,112 +819,106 @@ impl Grippable for Table {
impl PropertyEditable for Table { impl PropertyEditable for Table {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
use crate::entities::common::edit_prop as edit;
use acadrust::entities::table::BreakOptionFlags;
let fmt_h = |oh: &Option<acadrust::types::Handle>| -> String { let fmt_h = |oh: &Option<acadrust::types::Handle>| -> String {
match oh { match oh {
Some(h) if !h.is_null() => format!("{:X}", h.value()), Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(), _ => "(none)".to_string(),
} }
}; };
vec![PropSection { let yes_no = |b: bool| -> String {
title: "Table".into(), if b { "Yes".into() } else { "No".into() }
props: vec![ };
ro("Rows", "tbl_rows", self.rows.len().to_string()), // Direction = angle of the horizontal direction vector in the XY plane.
ro("Columns", "tbl_cols", self.columns.len().to_string()), let direction_deg =
ro( (self.horizontal_direction.y.atan2(self.horizontal_direction.x)).to_degrees();
"Insert X",
"tbl_ix", vec![
format!("{:.4}", self.insertion_point.x), PropSection {
), title: "Table".into(),
ro( props: vec![
"Insert Y", ro(
"tbl_iy", "Table style",
format!("{:.4}", self.insertion_point.y), "tbl_style_handle",
), fmt_h(&self.table_style_handle),
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
), ),
), 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 { impl Transformable for Table {

View file

@ -1,7 +1,7 @@
use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA}; use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA};
use crate::command::EntityTransform; 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::{ use crate::entities::text_support::{
resolve_dxf_special_chars, resolve_text_style, text_local_bounds, resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
}; };
@ -170,61 +170,95 @@ fn grips(t: &Text) -> Vec<GripDef> {
} }
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> { fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { // Text alignment point (second alignment / justify point). Falls back to
title: "Geometry".into(), // the insertion point for Left/Baseline text where no second point exists.
props: vec![ let ap = t.alignment_point.unwrap_or(t.insertion_point);
edit("Insert X", "ins_x", t.insertion_point.x), vec![
edit("Insert Y", "ins_y", t.insertion_point.y), PropSection {
edit("Insert Z", "ins_z", t.insertion_point.z), title: "Text".into(),
edit("Height", "height", t.height), props: vec![
edit("Rotation", "rotation", t.rotation.to_degrees()), Property {
edit("Width Factor", "width_factor", t.width_factor), label: "Contents".into(),
edit( field: "content",
"Oblique Angle", value: PropValue::EditText(t.value.clone()),
"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(),
}, },
}, Property {
Property { label: "Style".into(),
label: "V-Align".into(), field: "style",
field: "v_align", value: PropValue::Choice {
value: PropValue::Choice { selected: if t.style.trim().is_empty() {
selected: text_valign_str(&t.vertical_alignment).to_string(), "Standard".into()
options: ["Baseline", "Bottom", "Middle", "Top"] } else {
.into_iter() t.style.clone()
.map(str::to_string) },
.collect(), options: text_style_names.to_vec(),
},
},
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()
}, },
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) { fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {

View file

@ -294,50 +294,40 @@ impl Grippable for Tolerance {
impl PropertyEditable for Tolerance { impl PropertyEditable for Tolerance {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
vec![PropSection { vec![
title: "Geometry".into(), PropSection {
props: vec![ title: "Text".into(),
ro("Text", "tol_text", self.text.clone()), props: vec![
edit("Insert X", "tol_ix", self.insertion_point.x), ro("Text style", "tol_text_style", String::new()),
edit("Insert Y", "tol_iy", self.insertion_point.y), edit("Text height", "tol_text_height", self.text_height),
edit("Insert Z", "tol_iz", self.insertion_point.z), ],
ro( },
"Dim Style", PropSection {
"tol_dim_style", title: "Geometry".into(),
if self.dimension_style_name.is_empty() { props: vec![
"(default)".to_string() edit("Position X", "tol_ix", self.insertion_point.x),
} else { edit("Position Y", "tol_iy", self.insertion_point.y),
self.dimension_style_name.clone() edit("Position Z", "tol_iz", self.insertion_point.z),
}, ],
), },
ro( PropSection {
"Dim Style Handle", title: "Misc".into(),
"tol_dim_style_handle", props: vec![
match self.dimension_style_handle { ro(
Some(h) if !h.is_null() => format!("{:X}", h.value()), "Dimension style",
_ => "(none)".to_string(), "tol_dim_style",
}, if self.dimension_style_name.is_empty() {
), "(default)".to_string()
edit("Text Height", "tol_text_height", self.text_height), } else {
edit("Dim Gap", "tol_dim_gap", self.dimension_gap), self.dimension_style_name.clone()
ro( },
"Direction",
"tol_direction",
format!(
"{:.3}, {:.3}, {:.3}",
self.direction.x, self.direction.y, self.direction.z
), ),
), edit("Direction X", "tol_dir_x", self.direction.x),
ro( edit("Direction Y", "tol_dir_y", self.direction.y),
"Normal", edit("Direction Z", "tol_dir_z", self.direction.z),
"tol_normal", ],
format!( },
"{:.3}, {:.3}, {:.3}", ]
self.normal.x, self.normal.y, self.normal.z
),
),
],
}]
} }
fn apply_geom_prop(&mut self, field: &str, value: &str) { 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_iy" => self.insertion_point.y = v,
"tol_iz" => self.insertion_point.z = v, "tol_iz" => self.insertion_point.z = v,
"tol_text_height" if v > 0.0 => self.text_height = 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,
_ => {} _ => {}
} }
} }

View file

@ -147,58 +147,74 @@ impl Grippable for Underlay {
impl PropertyEditable for Underlay { impl PropertyEditable for Underlay {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let type_str = match self.underlay_type { let yes_no = |b: bool| if b { "Yes" } else { "No" };
acadrust::entities::UnderlayType::Pdf => "PDF",
acadrust::entities::UnderlayType::Dwf => "DWF", // Width / Height derived from the world-space clip boundary bounds
acadrust::entities::UnderlayType::Dgn => "DGN", // (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(), let show = self.flags.contains(UnderlayDisplayFlags::ON);
props: vec![ let clipping = self.flags.contains(UnderlayDisplayFlags::CLIPPING);
ro("Type", "ul_type", type_str), let monochrome = self.flags.contains(UnderlayDisplayFlags::MONOCHROME);
edit("Ins X", "ul_ix", self.insertion_point.x), let adjust_bg = self.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND);
edit("Ins Y", "ul_iy", self.insertion_point.y),
edit("Ins Z", "ul_iz", self.insertion_point.z), vec![
edit("X Scale", "ul_sx", self.x_scale), PropSection {
edit("Y Scale", "ul_sy", self.y_scale), title: "Geometry".into(),
edit("Z Scale", "ul_sz", self.z_scale), props: vec![
edit("Rotation", "ul_rot", self.rotation.to_degrees()), edit("Position X", "ul_ix", self.insertion_point.x),
edit("Contrast", "ul_contrast", self.contrast as f64), edit("Position Y", "ul_iy", self.insertion_point.y),
edit("Fade", "ul_fade", self.fade as f64), edit("Position Z", "ul_iz", self.insertion_point.z),
ro( edit("Scale X", "ul_sx", self.x_scale),
"On", edit("Scale Y", "ul_sy", self.y_scale),
"ul_on", edit("Scale Z", "ul_sz", self.z_scale),
if self.flags.contains(UnderlayDisplayFlags::ON) { ro("Width", "ul_width", format!("{:.4}", width)),
"Yes" ro("Height", "ul_height", format!("{:.4}", height)),
} else { edit("Rotation", "ul_rot", self.rotation.to_degrees()),
"No" ],
}, },
), PropSection {
ro( title: "Underlay Adjust".into(),
"Clipping", props: vec![
"ul_clip", edit("Contrast", "ul_contrast", self.contrast as f64),
if self.flags.contains(UnderlayDisplayFlags::CLIPPING) { edit("Fade", "ul_fade", self.fade as f64),
"Yes" ro("Monochrome", "ul_mono", yes_no(monochrome)),
} else { ro(
"No" "Adjust Colors for Background",
}, "ul_adjust_bg",
), yes_no(adjust_bg),
ro( ),
"Monochrome", ],
"ul_mono", },
if self.flags.contains(UnderlayDisplayFlags::MONOCHROME) { PropSection {
"Yes" title: "Misc".into(),
} else { props: vec![
"No" // Underlay name/path/layers live on the separate
}, // UnderlayDefinition object, not reachable from the entity.
), ro("Underlay name", "ul_name", String::new()),
ro( ro("Underlay path", "ul_path", String::new()),
"Clip Inverted", ro("Show underlay", "ul_on", yes_no(show)),
"ul_clip_inverted", ro("Clipping", "ul_clip", yes_no(clipping)),
if self.clip_inverted { "Yes" } else { "No" }, 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) { fn apply_geom_prop(&mut self, field: &str, value: &str) {

View file

@ -122,196 +122,94 @@ fn properties(vp: &Viewport) -> Vec<PropSection> {
crate::scene::vp_effective_scale(vp.custom_scale, vp.view_height, vp.height); crate::scene::vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
let current_scale_label = scale_label(effective_scale); let current_scale_label = scale_label(effective_scale);
let view_opts: Vec<String> = STD_VIEWS.iter().map(|s| s.to_string()).collect();
let current_view = viewport_view_label(vp);
let render_opts: Vec<String> = RENDER_MODES.iter().map(|(s, _)| s.to_string()).collect();
let current_render = render_mode_label(&vp.render_mode).to_string();
let shade_opts: Vec<String> = SHADE_PLOT_LABELS.iter().map(|s| s.to_string()).collect(); let shade_opts: Vec<String> = SHADE_PLOT_LABELS.iter().map(|s| s.to_string()).collect();
let current_shade = shade_plot_label(vp.shade_plot_mode).to_string(); let current_shade = shade_plot_label(vp.shade_plot_mode).to_string();
vec![PropSection { let annotation_scale = if effective_scale.abs() > 1e-9 {
title: "Geometry".into(), format!("1:{}", scale_label(effective_scale))
props: vec![ } else {
edit("Center X", "center_x", vp.center.x), String::new()
edit("Center Y", "center_y", vp.center.y), };
edit("Center Z", "center_z", vp.center.z),
edit("Width", "vp_w", vp.width), vec![
edit("Height", "vp_h", vp.height), PropSection {
// Numeric scale entry (derived from view_height; writes both). title: "Geometry".into(),
edit("Scale (num)", "vscale", effective_scale), props: vec![
// Standard scale picker. edit("Center X", "center_x", vp.center.x),
Property { edit("Center Y", "center_y", vp.center.y),
label: "Scale".into(), edit("Center Z", "center_z", vp.center.z),
field: "vscale_std", edit("Height", "vp_h", vp.height),
value: PropValue::Choice { edit("Width", "vp_w", vp.width),
selected: current_scale_label, ],
options: scale_opts, },
}, PropSection {
}, title: "Misc".into(),
// Standard view picker. props: vec![
Property { Property {
label: "View".into(), label: "On".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 {
field: "vp_on", field: "vp_on",
value: vp.status.is_on, value: PropValue::BoolToggle {
field: "vp_on",
value: vp.status.is_on,
},
}, },
}, ro("Clipped", "vp_clipped", String::new()),
Property { Property {
label: "Perspective".into(), label: "Display locked".into(),
field: "vp_perspective", field: "vp_locked",
value: PropValue::BoolToggle { value: PropValue::BoolToggle {
field: "vp_perspective", field: "vp_locked",
value: vp.status.perspective, value: vp.status.locked,
},
}, },
}, ro("Annotation scale", "vp_anno_scale", annotation_scale),
Property { Property {
label: "Hide Plot".into(), label: "Standard scale".into(),
field: "vp_hide_plot", field: "vscale_std",
value: PropValue::BoolToggle { value: PropValue::Choice {
field: "vp_hide_plot", selected: current_scale_label,
value: vp.status.hide_plot, options: scale_opts,
},
}, },
}, edit("Custom scale", "vscale", effective_scale),
Property { Property {
label: "UCS Icon".into(), label: "UCS per viewport".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 {
field: "vp_ucs_per_vp", field: "vp_ucs_per_vp",
value: vp.ucs_per_viewport, value: PropValue::BoolToggle {
field: "vp_ucs_per_vp",
value: vp.ucs_per_viewport,
},
}, },
}, ro(
edit("UCS Origin X", "vp_ucs_ox", vp.ucs_origin.x), "Layer property overrides",
edit("UCS Origin Y", "vp_ucs_oy", vp.ucs_origin.y), "vp_layer_overrides",
edit("UCS Origin Z", "vp_ucs_oz", vp.ucs_origin.z), if vp.frozen_layers.is_empty() {
edit("UCS X-Axis X", "vp_ucs_xx", vp.ucs_x_axis.x), "No".to_string()
edit("UCS X-Axis Y", "vp_ucs_xy", vp.ucs_x_axis.y), } else {
edit("UCS X-Axis Z", "vp_ucs_xz", vp.ucs_x_axis.z), format!("Yes ({})", vp.frozen_layers.len())
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), ro(
// Snap / grid metadata (drives the snap-aware UI; not rendered "Visual style",
// into the viewport contents directly). "vp_visual_style_handle",
edit("Snap Base X", "vp_snap_bx", vp.snap_base.x), if vp.visual_style_handle.is_null() {
edit("Snap Base Y", "vp_snap_by", vp.snap_base.y), "(none)".to_string()
edit("Snap Spacing X", "vp_snap_sx", vp.snap_spacing.x), } else {
edit("Snap Spacing Y", "vp_snap_sy", vp.snap_spacing.y), format!("{:X}", vp.visual_style_handle.value())
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), Property {
edit("Back Clip Z", "vp_back_clip", vp.back_clip_z), label: "Shade plot".into(),
edit("Circle Sides", "vp_circle_sides", vp.circle_sides as f64), field: "vp_shade_plot",
ro( value: PropValue::Choice {
"Grid Flags", selected: current_shade,
"vp_grid_flags", options: shade_opts,
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("Linked to Sheet View", "vp_sheet_view", String::new()),
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),
),
],
}]
} }
/// Identify which standard view matches the viewport's view direction. /// Identify which standard view matches the viewport's view direction.