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:
parent
749f3b99dc
commit
e38aa6b611
4 changed files with 214 additions and 20 deletions
|
|
@ -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
148
src/entities/solid.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue