feat(entities): surface remaining 2D + Region fields in properties

Closes out the 2D-entity audit (and the Region partner that ships next
to Solid3D / Body). Round-trip-only metadata fields plus a few editable
ones gain Properties-panel rows so the values the parser populates are
visible and don't get silently dropped on save:

- Hatch: seed_points count, pixel_size, normal
- Polyline2D: smooth_surface, default_start_width, default_end_width,
  thickness, normal (with editors for the widths/thickness)
- Region: uid, silhouettes (shared file with Solid3D / Body)
- Underlay: clip_inverted
- RasterImage: class_version, definition_handle, definition_reactor_handle
- Wipeout: class_version, clip_mode, definition_handle,
  definition_reactor_handle
- Leader: annotation_handle, override_color
- Tolerance: dimension_style_name, dimension_style_handle, dimension_gap,
  text_height (editable), direction, normal
- MLine: style_handle, style_element_count, justification picker
- Ole2Frame: version, source_application, binary_data size, dwg_mode,
  dwg_trailing_byte, is_paper_space
- Viewport: snap_base/spacing/angle, twist_angle, front/back clip Z,
  circle_sides, grid_flags, grid_major, base_ucs_handle, ucs_ortho_type,
  background_handle, shade_plot_handle, visual_style_handle,
  default_lighting, default_lighting_type, ambient_color (with editors
  for the snap / clip / circle_sides values)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-19 21:28:16 +03:00
commit 0b4facc50d
10 changed files with 361 additions and 1 deletions

View file

@ -63,6 +63,17 @@ fn properties(h: &Hatch) -> PropSection {
if h.is_associative { "Yes" } else { "No" },
),
edit("Elevation", "elevation", h.elevation),
ro("Seed Points", "seed_count", h.seed_points.len().to_string()),
ro(
"Pixel Size",
"pixel_size",
format!("{:.6}", h.pixel_size),
),
ro(
"Normal",
"normal",
format!("{:.3}, {:.3}, {:.3}", h.normal.x, h.normal.y, h.normal.z),
),
],
}
}

View file

@ -227,6 +227,25 @@ fn properties(leader: &Leader) -> PropSection {
// 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])

View file

@ -156,12 +156,41 @@ impl Grippable for MLine {
impl PropertyEditable for MLine {
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
let just_str = match self.justification {
acadrust::entities::MLineJustification::Top => "Top",
acadrust::entities::MLineJustification::Zero => "Zero",
acadrust::entities::MLineJustification::Bottom => "Bottom",
};
PropSection {
title: "Geometry".into(),
props: vec![
ro("Style", "ml_style", self.style_name.clone()),
ro(
"Style Handle",
"ml_style_handle",
match self.style_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
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",
@ -186,6 +215,14 @@ impl PropertyEditable for MLine {
.set(acadrust::entities::MLineFlags::CLOSED, closed);
return;
}
"ml_justification" => {
self.justification = match value {
"Top" => acadrust::entities::MLineJustification::Top,
"Bottom" => acadrust::entities::MLineJustification::Bottom,
_ => acadrust::entities::MLineJustification::Zero,
};
return;
}
_ => {}
}
let Ok(v) = value.trim().parse::<f64>() else {

View file

@ -88,6 +88,32 @@ fn properties(ole: &Ole2Frame) -> PropSection {
edit("Upper Left Y", "ole_uly", ole.upper_left_corner.y),
edit("Lower Right X", "ole_lrx", ole.lower_right_corner.x),
edit("Lower Right Y", "ole_lry", ole.lower_right_corner.y),
ro("Version", "ole_version", ole.version.to_string()),
ro(
"Source App",
"ole_source_app",
if ole.source_application.is_empty() {
"(unknown)".to_string()
} else {
ole.source_application.clone()
},
),
ro(
"Data Size",
"ole_data_size",
format!("{} bytes", ole.binary_data.len()),
),
ro("DWG Mode", "ole_dwg_mode", ole.dwg_mode.to_string()),
ro(
"DWG Trailing Byte",
"ole_dwg_trailing",
format!("{:#04x}", ole.dwg_trailing_byte),
),
ro(
"Paper Space",
"ole_paper_space",
if ole.is_paper_space { "Yes" } else { "No" },
),
],
}
}

View file

@ -347,11 +347,29 @@ impl Grippable for Polyline2D {
impl PropertyEditable for Polyline2D {
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
let smooth = match self.smooth_surface {
acadrust::entities::SmoothSurfaceType::None => "None",
acadrust::entities::SmoothSurfaceType::QuadraticBSpline => "Quadratic",
acadrust::entities::SmoothSurfaceType::CubicBSpline => "Cubic",
acadrust::entities::SmoothSurfaceType::Bezier => "Bezier",
};
PropSection {
title: "Geometry".into(),
props: vec![
ro("Vertices", "vertices", self.vertices.len().to_string()),
edit("Elevation", "pl2_elevation", self.elevation),
edit("Default Start W", "pl2_start_w", self.start_width),
edit("Default End W", "pl2_end_w", self.end_width),
edit("Thickness", "pl2_thickness", self.thickness),
ro("Smooth Surface", "pl2_smooth", smooth),
ro(
"Normal",
"pl2_normal",
format!(
"{:.3}, {:.3}, {:.3}",
self.normal.x, self.normal.y, self.normal.z
),
),
Property {
label: "Closed".into(),
field: "pl2_closed",
@ -383,6 +401,25 @@ impl PropertyEditable for Polyline2D {
self.elevation = v;
}
}
"pl2_start_w" => {
if let Ok(v) = value.trim().parse::<f64>() {
if v >= 0.0 {
self.start_width = v;
}
}
}
"pl2_end_w" => {
if let Ok(v) = value.trim().parse::<f64>() {
if v >= 0.0 {
self.end_width = v;
}
}
}
"pl2_thickness" => {
if let Ok(v) = value.trim().parse::<f64>() {
self.thickness = v;
}
}
_ => {}
}
}

View file

@ -186,6 +186,27 @@ impl PropertyEditable for RasterImage {
value: self.clipping_enabled,
},
},
ro(
"Class Version",
"ri_class_version",
self.class_version.to_string(),
),
ro(
"Definition",
"ri_def_handle",
match self.definition_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
ro(
"Def Reactor",
"ri_def_reactor_handle",
match self.definition_reactor_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
],
}
}
@ -421,6 +442,32 @@ impl PropertyEditable for Wipeout {
value: self.clipping_enabled,
},
},
ro(
"Class Version",
"wo_class_version",
self.class_version.to_string(),
),
ro(
"Clip Mode",
"wo_clip_mode",
format!("{:?}", self.clip_mode),
),
ro(
"Definition",
"wo_def_handle",
match self.definition_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
ro(
"Def Reactor",
"wo_def_reactor_handle",
match self.definition_reactor_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
],
}
}

View file

@ -87,6 +87,28 @@ impl PropertyEditable for Solid3D {
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(),
},
),
],
}
}
@ -141,6 +163,20 @@ impl PropertyEditable for Region {
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(),
),
],
}
}
@ -195,6 +231,20 @@ impl PropertyEditable for Body {
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

@ -290,6 +290,41 @@ impl PropertyEditable for Tolerance {
edit("Insert X", "tol_ix", self.insertion_point.x),
edit("Insert Y", "tol_iy", self.insertion_point.y),
edit("Insert Z", "tol_iz", self.insertion_point.z),
ro(
"Dim Style",
"tol_dim_style",
if self.dimension_style_name.is_empty() {
"(default)".to_string()
} else {
self.dimension_style_name.clone()
},
),
ro(
"Dim Style Handle",
"tol_dim_style_handle",
match self.dimension_style_handle {
Some(h) if !h.is_null() => format!("{:X}", h.value()),
_ => "(none)".to_string(),
},
),
edit("Text Height", "tol_text_height", self.text_height),
edit("Dim Gap", "tol_dim_gap", self.dimension_gap),
ro(
"Direction",
"tol_direction",
format!(
"{:.3}, {:.3}, {:.3}",
self.direction.x, self.direction.y, self.direction.z
),
),
ro(
"Normal",
"tol_normal",
format!(
"{:.3}, {:.3}, {:.3}",
self.normal.x, self.normal.y, self.normal.z
),
),
],
}
}
@ -302,6 +337,8 @@ impl PropertyEditable for Tolerance {
"tol_ix" => self.insertion_point.x = v,
"tol_iy" => self.insertion_point.y = v,
"tol_iz" => self.insertion_point.z = v,
"tol_text_height" if v > 0.0 => self.text_height = v,
"tol_dim_gap" => self.dimension_gap = v,
_ => {}
}
}

View file

@ -195,6 +195,11 @@ impl PropertyEditable for Underlay {
"No"
},
),
ro(
"Clip Inverted",
"ul_clip_inverted",
if self.clip_inverted { "Yes" } else { "No" },
),
],
}
}

View file

@ -2,7 +2,9 @@ use acadrust::entities::{Viewport, ViewportRenderMode};
use glam::Vec3;
use crate::command::EntityTransform;
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip};
use crate::entities::common::{
diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
@ -230,6 +232,84 @@ fn properties(vp: &Viewport) -> PropSection {
edit("UCS Y-Axis X", "vp_ucs_yx", vp.ucs_y_axis.x),
edit("UCS Y-Axis Y", "vp_ucs_yy", vp.ucs_y_axis.y),
edit("UCS Y-Axis Z", "vp_ucs_yz", vp.ucs_y_axis.z),
// Snap / grid metadata (drives the snap-aware UI; not rendered
// into the viewport contents directly).
edit("Snap Base X", "vp_snap_bx", vp.snap_base.x),
edit("Snap Base Y", "vp_snap_by", vp.snap_base.y),
edit("Snap Spacing X", "vp_snap_sx", vp.snap_spacing.x),
edit("Snap Spacing Y", "vp_snap_sy", vp.snap_spacing.y),
edit("Snap Angle", "vp_snap_ang", vp.snap_angle.to_degrees()),
edit("Twist Angle", "vp_twist", vp.twist_angle.to_degrees()),
edit("Front Clip Z", "vp_front_clip", vp.front_clip_z),
edit("Back Clip Z", "vp_back_clip", vp.back_clip_z),
edit(
"Circle Sides",
"vp_circle_sides",
vp.circle_sides as f64,
),
ro(
"Grid Flags",
"vp_grid_flags",
format!("{:#06b}", vp.grid_flags.to_bits()),
),
ro("Grid Major", "vp_grid_major", vp.grid_major.to_string()),
ro(
"Base UCS",
"vp_base_ucs_handle",
if vp.base_ucs_handle.is_null() {
"(none)".to_string()
} else {
format!("{:X}", vp.base_ucs_handle.value())
},
),
ro(
"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),
),
],
}
}
@ -404,6 +484,17 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
"vp_ucs_yx" => vp.ucs_y_axis.x = v,
"vp_ucs_yy" => vp.ucs_y_axis.y = v,
"vp_ucs_yz" => vp.ucs_y_axis.z = v,
"vp_snap_bx" => vp.snap_base.x = v,
"vp_snap_by" => vp.snap_base.y = v,
"vp_snap_sx" if v >= 0.0 => vp.snap_spacing.x = v,
"vp_snap_sy" if v >= 0.0 => vp.snap_spacing.y = v,
"vp_snap_ang" => vp.snap_angle = v.to_radians(),
"vp_twist" => vp.twist_angle = v.to_radians(),
"vp_front_clip" => vp.front_clip_z = v,
"vp_back_clip" => vp.back_clip_z = v,
"vp_circle_sides" if v >= 0.0 && v <= i16::MAX as f64 => {
vp.circle_sides = v as i16;
}
_ => {}
}
}