From e38aa6b6110f0b28fabbb5548c7d53ad38a19ebe Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 3 Apr 2026 19:22:10 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Solid=20entity=20(2D)=20=E2=80=94=20ren?= =?UTF-8?q?der,=20grips,=20properties,=20transform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TruckConvertible renders the 4 perimeter edges as wireframe outline. The scene injects a solid-fill HatchModel for the filled quad using the existing hatch GPU pipeline (DXF Z-order: p0→p1→p3→p2). Grippable provides 4 square corner grips; grip moves rebuild the fill model. PropertyEditable exposes all 4 corners and thickness. Transformable handles move/rotate/scale/mirror via the standard entity transform path. Co-Authored-By: Claude Sonnet 4.6 --- src/entities/mod.rs | 1 + src/entities/solid.rs | 148 +++++++++++++++++++++++++++++++++++++++++ src/entities/traits.rs | 9 +++ src/scene/mod.rs | 76 +++++++++++++++------ 4 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 src/entities/solid.rs diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 86db71ea6..79c9f2f0d 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -17,6 +17,7 @@ pub mod point; pub mod polyline; pub mod raster_image; pub mod ray; +pub mod solid; pub mod spline; pub mod text; mod text_support; diff --git a/src/entities/solid.rs b/src/entities/solid.rs new file mode 100644 index 000000000..7402815cb --- /dev/null +++ b/src/entities/solid.rs @@ -0,0 +1,148 @@ +// SOLID entity — 2D filled quadrilateral (or triangle when p3 == p4). +// +// Wireframe: the 4 perimeter edges as TruckObject::Lines. +// Filled: the scene injects a solid-fill HatchModel (like hatch.rs). +// Grips: 4 corner grip points. + +use acadrust::entities::Solid; +use glam::Vec3; + +use crate::command::EntityTransform; +use crate::entities::common::{edit_prop as edit, 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; + +fn v3(v: &acadrust::types::Vector3) -> [f32; 3] { + [v.x as f32, v.y as f32, v.z as f32] +} + +impl TruckConvertible for Solid { + fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + let p0 = v3(&self.first_corner); + let p1 = v3(&self.second_corner); + let p2 = v3(&self.third_corner); + let p3 = v3(&self.fourth_corner); + + // DXF SOLID vertex order: 1-2-4-3 (Z-shaped), render as closed quad outline. + // AutoCAD stores corners in "Z" order: p0-p1 are top edge, p2-p3 are bottom edge, + // so the visual quad is p0→p1→p3→p2→p0. + let pts = vec![ + p0, p1, [f32::NAN; 3], + p1, p3, [f32::NAN; 3], + p3, p2, [f32::NAN; 3], + p2, p0, + ]; + + let snap = vec![ + (Vec3::from(p0), SnapHint::Node), + (Vec3::from(p1), SnapHint::Node), + (Vec3::from(p2), SnapHint::Node), + (Vec3::from(p3), SnapHint::Node), + ]; + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: snap, + tangent_geoms: vec![], + key_vertices: vec![p0, p1, p2, p3], + }) + } +} + +impl Grippable for Solid { + fn grips(&self) -> Vec { + vec![ + square_grip(0, Vec3::from(v3(&self.first_corner))), + square_grip(1, Vec3::from(v3(&self.second_corner))), + square_grip(2, Vec3::from(v3(&self.third_corner))), + square_grip(3, Vec3::from(v3(&self.fourth_corner))), + ] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + let corner = match grip_id { + 0 => &mut self.first_corner, + 1 => &mut self.second_corner, + 2 => &mut self.third_corner, + 3 => &mut self.fourth_corner, + _ => return, + }; + match apply { + GripApply::Translate(d) => { + corner.x += d.x as f64; + corner.y += d.y as f64; + corner.z += d.z as f64; + } + GripApply::Absolute(p) => { + corner.x = p.x as f64; + corner.y = p.y as f64; + corner.z = p.z as f64; + } + } + } +} + +impl PropertyEditable for Solid { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + edit("P1 X", "sl_p1x", self.first_corner.x), + edit("P1 Y", "sl_p1y", self.first_corner.y), + edit("P1 Z", "sl_p1z", self.first_corner.z), + edit("P2 X", "sl_p2x", self.second_corner.x), + edit("P2 Y", "sl_p2y", self.second_corner.y), + edit("P2 Z", "sl_p2z", self.second_corner.z), + edit("P3 X", "sl_p3x", self.third_corner.x), + edit("P3 Y", "sl_p3y", self.third_corner.y), + edit("P3 Z", "sl_p3z", self.third_corner.z), + edit("P4 X", "sl_p4x", self.fourth_corner.x), + edit("P4 Y", "sl_p4y", self.fourth_corner.y), + edit("P4 Z", "sl_p4z", self.fourth_corner.z), + edit("Thickness", "sl_thick", self.thickness), + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Ok(v) = value.trim().parse::() else { return }; + match field { + "sl_p1x" => self.first_corner.x = v, + "sl_p1y" => self.first_corner.y = v, + "sl_p1z" => self.first_corner.z = v, + "sl_p2x" => self.second_corner.x = v, + "sl_p2y" => self.second_corner.y = v, + "sl_p2z" => self.second_corner.z = v, + "sl_p3x" => self.third_corner.x = v, + "sl_p3y" => self.third_corner.y = v, + "sl_p3z" => self.third_corner.z = v, + "sl_p4x" => self.fourth_corner.x = v, + "sl_p4y" => self.fourth_corner.y = v, + "sl_p4z" => self.fourth_corner.z = v, + "sl_thick" => self.thickness = v, + _ => {} + } + } +} + +impl Transformable for Solid { + fn apply_transform(&mut self, t: &EntityTransform) { + crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { + for corner in [ + &mut entity.first_corner, + &mut entity.second_corner, + &mut entity.third_corner, + &mut entity.fourth_corner, + ] { + crate::scene::transform::reflect_xy_point( + &mut corner.x, + &mut corner.y, + p1, + p2, + ); + } + }); + } +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index 3b3c8e0fb..593dc3e37 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -52,6 +52,7 @@ impl EntityTypeOps for EntityType { 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::Solid(solid) => TruckConvertible::to_truck(solid, document), EntityType::Face3D(f) => TruckConvertible::to_truck(f, document), EntityType::PolygonMesh(pm) => TruckConvertible::to_truck(pm, document), EntityType::PolyfaceMesh(pfm) => TruckConvertible::to_truck(pfm, document), @@ -82,6 +83,7 @@ impl EntityTypeOps for EntityType { EntityType::AttributeEntity(a) => Grippable::grips(a), EntityType::MLine(ml) => Grippable::grips(ml), EntityType::Tolerance(tol) => Grippable::grips(tol), + EntityType::Solid(solid) => Grippable::grips(solid), EntityType::Face3D(f) => Grippable::grips(f), EntityType::PolygonMesh(pm) => Grippable::grips(pm), EntityType::PolyfaceMesh(pfm) => Grippable::grips(pfm), @@ -165,6 +167,10 @@ impl EntityTypeOps for EntityType { tol, text_style_names, )), + EntityType::Solid(solid) => Some(PropertyEditable::geometry_properties( + solid, + text_style_names, + )), EntityType::Face3D(f) => Some(PropertyEditable::geometry_properties( f, text_style_names, @@ -242,6 +248,7 @@ impl EntityTypeOps for EntityType { 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::Solid(solid) => PropertyEditable::apply_geom_prop(solid, field, value), EntityType::Face3D(f) => PropertyEditable::apply_geom_prop(f, field, value), EntityType::PolygonMesh(pm) => PropertyEditable::apply_geom_prop(pm, field, value), EntityType::PolyfaceMesh(pfm) => PropertyEditable::apply_geom_prop(pfm, field, value), @@ -278,6 +285,7 @@ impl EntityTypeOps for EntityType { 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::Solid(solid) => Grippable::apply_grip(solid, grip_id, apply), EntityType::Face3D(f) => Grippable::apply_grip(f, grip_id, apply), EntityType::PolygonMesh(pm) => Grippable::apply_grip(pm, grip_id, apply), EntityType::PolyfaceMesh(pfm) => Grippable::apply_grip(pfm, grip_id, apply), @@ -316,6 +324,7 @@ impl EntityTypeOps for EntityType { 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::Solid(solid) => Transformable::apply_transform(solid, t), EntityType::Face3D(f) => Transformable::apply_transform(f, t), EntityType::PolygonMesh(pm) => Transformable::apply_transform(pm, t), EntityType::PolyfaceMesh(pfm) => Transformable::apply_transform(pfm, t), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e26bc3627..2f7656cdc 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -31,7 +31,7 @@ pub use selection::SelectionState; pub use wire_model::WireModel; use crate::command::EntityTransform; -use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge}; +use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid}; use acadrust::entities::{Block, BlockEnd, Insert as DxfInsert}; use acadrust::objects::ObjectType; use acadrust::types::Vector2; @@ -1015,6 +1015,9 @@ impl Scene { let hatch_seed = if let EntityType::Hatch(dxf) = &entity { let color = self.render_style(&entity).0; Self::hatch_model_from_dxf(dxf, color) + } else if let EntityType::Solid(solid) = &entity { + let color = self.render_style(&entity).0; + Some(Self::solid_hatch_model(solid, color)) } else { None }; @@ -1357,28 +1360,54 @@ impl Scene { pub fn populate_hatches_from_document(&mut self) { self.hatches.clear(); - let entries: Vec<(Handle, [f32; 4])> = self + let entries: Vec<(Handle, EntityType)> = self .document .entities() - .filter_map(|e| { - if let EntityType::Hatch(dxf) = e { - let color = tessellate::aci_to_rgba(&dxf.common.color); - Some((dxf.common.handle, color)) - } else { - None - } + .filter_map(|e| match e { + EntityType::Hatch(h) => Some((h.common.handle, e.clone())), + EntityType::Solid(s) => Some((s.common.handle, e.clone())), + _ => None, }) .collect(); - for (handle, color) in entries { - if let Some(EntityType::Hatch(dxf)) = self.document.get_entity(handle) { - if let Some(model) = Self::hatch_model_from_dxf(dxf, color) { - self.hatches.insert(handle, model); + for (handle, kind) in entries { + let model = match &kind { + EntityType::Hatch(dxf) => { + let color = tessellate::aci_to_rgba(&dxf.common.color); + Self::hatch_model_from_dxf(dxf, color) } + EntityType::Solid(solid) => { + let color = tessellate::aci_to_rgba(&solid.common.color); + Some(Self::solid_hatch_model(solid, color)) + } + _ => None, + }; + if let Some(m) = model { + self.hatches.insert(handle, m); } } } + /// Build a solid-fill HatchModel for a DXF Solid entity. + /// DXF SOLID corners are in "Z-order": p0-p1 top, p2-p3 bottom. + /// Visual quad is p0→p1→p3→p2 (closed). + fn solid_hatch_model(solid: &DxfSolid, color: [f32; 4]) -> HatchModel { + let boundary = vec![ + [solid.first_corner.x as f32, solid.first_corner.y as f32], + [solid.second_corner.x as f32, solid.second_corner.y as f32], + [solid.fourth_corner.x as f32, solid.fourth_corner.y as f32], + [solid.third_corner.x as f32, solid.third_corner.y as f32], + ]; + HatchModel { + boundary, + pattern: hatch_model::HatchPattern::Solid, + name: "SOLID".into(), + color, + angle_offset: 0.0, + scale: 1.0, + } + } + pub fn add_hatch(&mut self, model: HatchModel) -> Handle { let mut dxf = DxfHatch::new(); dxf.is_solid = matches!( @@ -1660,14 +1689,21 @@ impl Scene { if let Some(entity) = self.document.get_entity_mut(handle) { dispatch::apply_grip(entity, grip_id, apply); } - // Rebuild the GPU hatch model when a hatch boundary vertex moves. - if let Some(EntityType::Hatch(dxf)) = self.document.get_entity(handle) { - let color = tessellate::aci_to_rgba(&dxf.common.color); - if let Some(model) = Self::hatch_model_from_dxf(dxf, color) { - self.hatches.insert(handle, model); - } else { - self.hatches.remove(&handle); + // Rebuild GPU hatch/solid model when a boundary vertex or corner moves. + match self.document.get_entity(handle) { + Some(EntityType::Hatch(dxf)) => { + let color = tessellate::aci_to_rgba(&dxf.common.color); + if let Some(model) = Self::hatch_model_from_dxf(dxf, color) { + self.hatches.insert(handle, model); + } else { + self.hatches.remove(&handle); + } } + Some(EntityType::Solid(solid)) => { + let color = tessellate::aci_to_rgba(&solid.common.color); + self.hatches.insert(handle, Self::solid_hatch_model(solid, color)); + } + _ => {} } }