From dafa079b6a2d40ec1708c39effa7ce88cd8fd458 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 19:06:20 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Phase=205=20=E2=80=94=20MLine,=20Tolera?= =?UTF-8?q?nce=20render;=20DimStyle=20Choice=20picker=20on=20dimensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mline.rs: MLine renders center spine + two parallel offset lines at ±scale/2 using the vertex miter directions; vertex grips; scale, closed and style-name properties - tolerance.rs: Tolerance renders the GDT text string via font tessellation at insertion_point with rotation from direction vector; insertion grip and position properties - properties.rs: Dimension entity's style_name EditText is upgraded to a Choice dropdown when document.dim_styles is non-empty, enabling point-and-click DimStyle assignment without a separate panel Co-Authored-By: Claude Sonnet 4.6 --- src/app/properties.rs | 26 ++++++ src/entities/mline.rs | 174 ++++++++++++++++++++++++++++++++++++++ src/entities/mod.rs | 2 + src/entities/tolerance.rs | 115 +++++++++++++++++++++++++ src/entities/traits.rs | 18 ++++ 5 files changed, 335 insertions(+) create mode 100644 src/entities/mline.rs create mode 100644 src/entities/tolerance.rs diff --git a/src/app/properties.rs b/src/app/properties.rs index 605fdc8b1..1bd56e1b8 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -103,6 +103,32 @@ impl H7CAD { } } + // Inject DimStyle picker for Dimension entities. + if let acadrust::EntityType::Dimension(_) = entity { + let dim_style_names: Vec = self.tabs[i].scene.document.dim_styles + .iter() + .map(|s| s.name.clone()) + .filter(|n| !n.is_empty()) + .collect(); + if !dim_style_names.is_empty() { + // Current style is already shown as EditText in the geom section; + // replace/upgrade it to a Choice if we have a list. + if let Some(geom) = sections.last_mut() { + // Find and replace the style_name EditText with a Choice. + if let Some(prop) = geom.props.iter_mut().find(|p| p.field == "style_name") { + let current = match &prop.value { + crate::scene::object::PropValue::EditText(s) => s.clone(), + _ => String::new(), + }; + prop.value = crate::scene::object::PropValue::Choice { + selected: current, + options: dim_style_names, + }; + } + } + } + } + if !group_names.is_empty() { let label = group_names.join(", "); if let Some(general) = sections.first_mut() { diff --git a/src/entities/mline.rs b/src/entities/mline.rs new file mode 100644 index 000000000..49c38f449 --- /dev/null +++ b/src/entities/mline.rs @@ -0,0 +1,174 @@ +use acadrust::entities::MLine; +use glam::Vec3; + +use crate::command::EntityTransform; +use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; +use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; + +impl TruckConvertible for MLine { + fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + if self.vertices.is_empty() { + return None; + } + + let n = self.vertices.len(); + let closed = self.flags.contains(acadrust::entities::MLineFlags::CLOSED); + + // Spine: center line connecting all vertex positions. + // Also attempt to draw parallel offset lines (±scale/2 in miter direction) + // when scale_factor is non-zero. + let scale = self.scale_factor as f32; + + let mut pts: Vec<[f32; 3]> = Vec::new(); + + // Center spine. + for v in &self.vertices { + pts.push([v.position.x as f32, v.position.y as f32, v.position.z as f32]); + } + if closed && n >= 2 { + pts.push([ + self.vertices[0].position.x as f32, + self.vertices[0].position.y as f32, + self.vertices[0].position.z as f32, + ]); + } + + // Parallel offset lines — one at +scale/2 and one at -scale/2 + // along each vertex's miter direction. + if scale.abs() > 1e-6 { + for sign in [-0.5_f32, 0.5_f32] { + let offset = scale * sign; + pts.push([f32::NAN; 3]); + for v in &self.vertices { + let mx = v.miter.x as f32; + let my = v.miter.y as f32; + let mz = v.miter.z as f32; + pts.push([ + v.position.x as f32 + mx * offset, + v.position.y as f32 + my * offset, + v.position.z as f32 + mz * offset, + ]); + } + if closed && n >= 2 { + let v0 = &self.vertices[0]; + let mx = v0.miter.x as f32; + let my = v0.miter.y as f32; + let mz = v0.miter.z as f32; + pts.push([ + v0.position.x as f32 + mx * offset, + v0.position.y as f32 + my * offset, + v0.position.z as f32 + mz * offset, + ]); + } + } + } + + let key_verts: Vec<[f32; 3]> = self + .vertices + .iter() + .map(|v| [v.position.x as f32, v.position.y as f32, v.position.z as f32]) + .collect(); + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: key_verts, + }) + } +} + +impl Grippable for MLine { + fn grips(&self) -> Vec { + self.vertices + .iter() + .enumerate() + .map(|(i, v)| { + square_grip( + i, + Vec3::new(v.position.x as f32, v.position.y as f32, v.position.z as f32), + ) + }) + .collect() + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if let Some(v) = self.vertices.get_mut(grip_id) { + match apply { + GripApply::Translate(d) => { + v.position.x += d.x as f64; + v.position.y += d.y as f64; + v.position.z += d.z as f64; + } + GripApply::Absolute(p) => { + v.position.x = p.x as f64; + v.position.y = p.y as f64; + v.position.z = p.z as f64; + } + } + } + } +} + +impl PropertyEditable for MLine { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Style", "ml_style", self.style_name.clone()), + ro("Vertices", "ml_verts", self.vertices.len().to_string()), + edit("Scale", "ml_scale", self.scale_factor), + 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) { + match field { + "ml_closed" => { + let closed = if value == "toggle" { + !self.flags.contains(acadrust::entities::MLineFlags::CLOSED) + } else { + value == "true" + }; + self.flags.set(acadrust::entities::MLineFlags::CLOSED, closed); + return; + } + _ => {} + } + let Ok(v) = value.trim().parse::() else { return }; + if field == "ml_scale" && v != 0.0 { + self.scale_factor = v; + } + } +} + +impl Transformable for MLine { + fn apply_transform(&mut self, t: &EntityTransform) { + crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { + for v in &mut entity.vertices { + crate::scene::transform::reflect_xy_point( + &mut v.position.x, + &mut v.position.y, + p1, + p2, + ); + } + crate::scene::transform::reflect_xy_point( + &mut entity.start_point.x, + &mut entity.start_point.y, + p1, + p2, + ); + }); + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 8efe2316d..d4518bc01 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -10,6 +10,7 @@ pub mod leader; pub mod line; pub mod multileader; pub mod lwpolyline; +pub mod mline; pub mod mtext; pub mod point; pub mod polyline; @@ -18,5 +19,6 @@ pub mod ray; pub mod spline; pub mod text; mod text_support; +pub mod tolerance; pub mod traits; pub mod viewport; diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs new file mode 100644 index 000000000..e2393e90f --- /dev/null +++ b/src/entities/tolerance.rs @@ -0,0 +1,115 @@ +use acadrust::entities::Tolerance; +use glam::Vec3; + +use crate::command::EntityTransform; +use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; +use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::object::{GripApply, GripDef, PropSection}; +use crate::scene::wire_model::SnapHint; +use crate::scene::{cxf, transform}; + +impl TruckConvertible for Tolerance { + fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + if self.text.is_empty() { + return None; + } + // GDT tolerance text — render with the standard font. + // Special GDT symbols (Ⓗ, ⊕ etc.) will display as-is if the font + // supports them, otherwise as placeholder glyphs. + let snap_pt = Vec3::new( + self.insertion_point.x as f32, + self.insertion_point.y as f32, + self.insertion_point.z as f32, + ); + + // Determine rotation from the direction vector. + let angle = (self.direction.y as f32).atan2(self.direction.x as f32); + // Use a default height of 2.5 (standard annotation size). + let height = 2.5_f32; + + let strokes = cxf::tessellate_text_ex( + [self.insertion_point.x as f32, self.insertion_point.y as f32], + height, + angle, + 1.0, + 0.0, + "txt", // standard CAD font + &self.text, + ); + + Some(TruckEntity { + object: TruckObject::Text(strokes), + snap_pts: vec![(snap_pt, SnapHint::Insertion)], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} + +impl Grippable for Tolerance { + fn grips(&self) -> Vec { + vec![square_grip( + 0, + Vec3::new( + self.insertion_point.x as f32, + self.insertion_point.y as f32, + self.insertion_point.z as f32, + ), + )] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if grip_id == 0 { + match apply { + GripApply::Translate(d) => { + self.insertion_point.x += d.x as f64; + self.insertion_point.y += d.y as f64; + self.insertion_point.z += d.z as f64; + } + GripApply::Absolute(p) => { + self.insertion_point.x = p.x as f64; + self.insertion_point.y = p.y as f64; + self.insertion_point.z = p.z as f64; + } + } + } + } +} + +impl PropertyEditable for Tolerance { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Text", "tol_text", self.text.clone()), + 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), + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Ok(v) = value.trim().parse::() else { return }; + match field { + "tol_ix" => self.insertion_point.x = v, + "tol_iy" => self.insertion_point.y = v, + "tol_iz" => self.insertion_point.z = v, + _ => {} + } + } +} + +impl Transformable for Tolerance { + fn apply_transform(&mut self, t: &EntityTransform) { + transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { + transform::reflect_xy_point( + &mut entity.insertion_point.x, + &mut entity.insertion_point.y, + p1, + p2, + ); + }); + } +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index f6dbfdada..d9497ef2c 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -50,6 +50,8 @@ impl EntityTypeOps for EntityType { EntityType::Wipeout(wo) => TruckConvertible::to_truck(wo, document), EntityType::AttributeDefinition(a) => TruckConvertible::to_truck(a, document), EntityType::AttributeEntity(a) => TruckConvertible::to_truck(a, document), + EntityType::MLine(ml) => TruckConvertible::to_truck(ml, document), + EntityType::Tolerance(tol) => TruckConvertible::to_truck(tol, document), EntityType::Text(text) => TruckConvertible::to_truck(text, document), EntityType::MText(text) => TruckConvertible::to_truck(text, document), EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document), @@ -74,6 +76,8 @@ impl EntityTypeOps for EntityType { EntityType::Wipeout(wo) => Grippable::grips(wo), EntityType::AttributeDefinition(a) => Grippable::grips(a), EntityType::AttributeEntity(a) => Grippable::grips(a), + EntityType::MLine(ml) => Grippable::grips(ml), + EntityType::Tolerance(tol) => Grippable::grips(tol), EntityType::Point(pt) => Grippable::grips(pt), EntityType::Spline(spline) => Grippable::grips(spline), EntityType::Text(text) => Grippable::grips(text), @@ -143,6 +147,14 @@ impl EntityTypeOps for EntityType { a, text_style_names, )), + EntityType::MLine(ml) => Some(PropertyEditable::geometry_properties( + ml, + text_style_names, + )), + EntityType::Tolerance(tol) => Some(PropertyEditable::geometry_properties( + tol, + text_style_names, + )), EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties( hatch, text_style_names, @@ -202,6 +214,8 @@ impl EntityTypeOps for EntityType { EntityType::Wipeout(wo) => PropertyEditable::apply_geom_prop(wo, field, value), EntityType::AttributeDefinition(a) => PropertyEditable::apply_geom_prop(a, field, value), EntityType::AttributeEntity(a) => PropertyEditable::apply_geom_prop(a, field, value), + EntityType::MLine(ml) => PropertyEditable::apply_geom_prop(ml, field, value), + EntityType::Tolerance(tol) => PropertyEditable::apply_geom_prop(tol, field, value), EntityType::Hatch(hatch) => PropertyEditable::apply_geom_prop(hatch, field, value), EntityType::Point(pt) => PropertyEditable::apply_geom_prop(pt, field, value), EntityType::Spline(spline) => PropertyEditable::apply_geom_prop(spline, field, value), @@ -232,6 +246,8 @@ impl EntityTypeOps for EntityType { EntityType::Wipeout(wo) => Grippable::apply_grip(wo, grip_id, apply), EntityType::AttributeDefinition(a) => Grippable::apply_grip(a, grip_id, apply), EntityType::AttributeEntity(a) => Grippable::apply_grip(a, grip_id, apply), + EntityType::MLine(ml) => Grippable::apply_grip(ml, grip_id, apply), + EntityType::Tolerance(tol) => Grippable::apply_grip(tol, grip_id, apply), EntityType::Point(pt) => Grippable::apply_grip(pt, grip_id, apply), EntityType::Spline(spline) => Grippable::apply_grip(spline, grip_id, apply), EntityType::Text(text) => Grippable::apply_grip(text, grip_id, apply), @@ -262,6 +278,8 @@ impl EntityTypeOps for EntityType { EntityType::Wipeout(wo) => Transformable::apply_transform(wo, t), EntityType::AttributeDefinition(a) => Transformable::apply_transform(a, t), EntityType::AttributeEntity(a) => Transformable::apply_transform(a, t), + EntityType::MLine(ml) => Transformable::apply_transform(ml, t), + EntityType::Tolerance(tol) => Transformable::apply_transform(tol, t), EntityType::MText(text) => Transformable::apply_transform(text, t), EntityType::Point(pt) => Transformable::apply_transform(pt, t), EntityType::Spline(spline) => Transformable::apply_transform(spline, t),