Feat: Solid entity (2D) — render, grips, properties, transform

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-03 19:22:10 +03:00
commit e38aa6b611
4 changed files with 214 additions and 20 deletions

View file

@ -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;

148
src/entities/solid.rs Normal file
View file

@ -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<TruckEntity> {
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<GripDef> {
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::<f64>() 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,
);
}
});
}
}

View file

@ -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),

View file

@ -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));
}
_ => {}
}
}