feat: add Underlay entity support (PDF/DWF/DGN)
- New src/entities/underlay.rs: TruckConvertible (clip boundary polygon or cross marker), Grippable (insertion + boundary vertex grips), PropertyEditable (position, scale, rotation, contrast, fade, flags), Transformable (translate, mirror, scale, rotate) - Wire EntityType::Underlay into all four match arms in traits.rs - UNDERLAY command: FADE/CONTRAST/ON/OFF/CLIP ON|OFF/MONO ON|OFF subcommands to edit selected underlay entities - Entity type name map entry for UNDERLAY Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1db3ad6a33
commit
6f19b4fd81
4 changed files with 366 additions and 0 deletions
|
|
@ -2821,6 +2821,98 @@ impl H7CAD {
|
|||
}
|
||||
}
|
||||
}
|
||||
// UNDERLAY — edit properties of selected PDF/DWF/DGN underlay entities.
|
||||
// Usage:
|
||||
// UNDERLAY FADE <0-80>
|
||||
// UNDERLAY CONTRAST <0-100>
|
||||
// UNDERLAY ON | OFF
|
||||
// UNDERLAY CLIP ON | OFF
|
||||
// UNDERLAY MONO ON | OFF
|
||||
cmd if cmd == "UNDERLAY" || cmd.starts_with("UNDERLAY ") => {
|
||||
let sub = cmd.split_once(' ')
|
||||
.map(|(_, r)| r.trim().to_uppercase())
|
||||
.unwrap_or_default();
|
||||
let handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.collect();
|
||||
if handles.is_empty() {
|
||||
self.command_line.push_error("UNDERLAY: select underlay entities first.");
|
||||
} else {
|
||||
let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect();
|
||||
let action = parts.first().copied().unwrap_or("");
|
||||
let arg = parts.get(1).copied().unwrap_or("").trim();
|
||||
let mut changed = 0usize;
|
||||
self.push_undo_snapshot(i, "UNDERLAY");
|
||||
for h in &handles {
|
||||
if let Some(acadrust::EntityType::Underlay(ul)) = self.tabs[i].scene
|
||||
.document.entities_mut()
|
||||
.find(|e| e.common().handle == *h)
|
||||
{
|
||||
match action {
|
||||
"FADE" => {
|
||||
if let Ok(v) = arg.parse::<u8>() {
|
||||
ul.set_fade(v);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
"CONTRAST" => {
|
||||
if let Ok(v) = arg.parse::<u8>() {
|
||||
ul.set_contrast(v);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
"ON" => { ul.set_on(true); changed += 1; }
|
||||
"OFF" => { ul.set_on(false); changed += 1; }
|
||||
"CLIP" => {
|
||||
match arg {
|
||||
"ON" => {
|
||||
ul.flags |= acadrust::entities::UnderlayDisplayFlags::CLIPPING;
|
||||
changed += 1;
|
||||
}
|
||||
"OFF" => {
|
||||
ul.clear_clip();
|
||||
changed += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"MONO" => {
|
||||
match arg {
|
||||
"ON" => { ul.set_monochrome(true); changed += 1; }
|
||||
"OFF" => { ul.set_monochrome(false); changed += 1; }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// No sub-command: print status.
|
||||
self.command_line.push_output(&format!(
|
||||
"Underlay {:x}: fade={}, contrast={}, on={}, clip={}, mono={}",
|
||||
h.value(),
|
||||
ul.fade,
|
||||
ul.contrast,
|
||||
ul.is_on(),
|
||||
ul.is_clipping(),
|
||||
ul.is_monochrome(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_info(&format!(
|
||||
"Updated {changed} underlay(s)."
|
||||
));
|
||||
} else if !action.is_empty() {
|
||||
self.command_line.push_error(
|
||||
"Usage: UNDERLAY [FADE <n>|CONTRAST <n>|ON|OFF|CLIP ON|OFF|MONO ON|OFF]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"PAGESETUP" => {
|
||||
if self.tabs[i].scene.current_layout == "Model" {
|
||||
self.command_line.push_error("PAGESETUP: switch to a paper space layout first.");
|
||||
|
|
@ -2978,6 +3070,7 @@ fn entity_type_name(entity: &acadrust::EntityType) -> &'static str {
|
|||
acadrust::EntityType::MLine(_) => "MLINE",
|
||||
acadrust::EntityType::RasterImage(_) => "RASTERIMAGE",
|
||||
acadrust::EntityType::Wipeout(_) => "WIPEOUT",
|
||||
acadrust::EntityType::Underlay(_) => "UNDERLAY",
|
||||
acadrust::EntityType::AttributeDefinition(_)=> "ATTDEF",
|
||||
acadrust::EntityType::AttributeEntity(_) => "ATTRIB",
|
||||
acadrust::EntityType::Leader(_) => "LEADER",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub mod mtext;
|
|||
pub mod point;
|
||||
pub mod polyline;
|
||||
pub mod raster_image;
|
||||
pub mod underlay;
|
||||
pub mod ray;
|
||||
pub mod solid;
|
||||
pub mod spline;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::MText(text) => TruckConvertible::to_truck(text, document),
|
||||
EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document),
|
||||
EntityType::MultiLeader(ml) => TruckConvertible::to_truck(ml, document),
|
||||
EntityType::Underlay(ul) => TruckConvertible::to_truck(ul, document),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +99,7 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::MultiLeader(ml) => Grippable::grips(ml),
|
||||
EntityType::Dimension(dim) => Grippable::grips(dim),
|
||||
EntityType::Hatch(hatch) => Grippable::grips(hatch),
|
||||
EntityType::Underlay(ul) => Grippable::grips(ul),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +226,10 @@ impl EntityTypeOps for EntityType {
|
|||
ml,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Underlay(ul) => Some(PropertyEditable::geometry_properties(
|
||||
ul,
|
||||
text_style_names,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -263,6 +269,7 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::Dimension(dim) => PropertyEditable::apply_geom_prop(dim, field, value),
|
||||
EntityType::Leader(leader) => PropertyEditable::apply_geom_prop(leader, field, value),
|
||||
EntityType::MultiLeader(ml) => PropertyEditable::apply_geom_prop(ml, field, value),
|
||||
EntityType::Underlay(ul) => PropertyEditable::apply_geom_prop(ul, field, value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -300,6 +307,7 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::MultiLeader(ml) => Grippable::apply_grip(ml, grip_id, apply),
|
||||
EntityType::Dimension(dim) => Grippable::apply_grip(dim, grip_id, apply),
|
||||
EntityType::Hatch(hatch) => Grippable::apply_grip(hatch, grip_id, apply),
|
||||
EntityType::Underlay(ul) => Grippable::apply_grip(ul, grip_id, apply),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,6 +345,7 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::Dimension(dim) => Transformable::apply_transform(dim, t),
|
||||
EntityType::Leader(leader) => Transformable::apply_transform(leader, t),
|
||||
EntityType::MultiLeader(ml) => Transformable::apply_transform(ml, t),
|
||||
EntityType::Underlay(ul) => Transformable::apply_transform(ul, t),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
263
src/entities/underlay.rs
Normal file
263
src/entities/underlay.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
// Underlay entity — PDF/DWF/DGN reference.
|
||||
//
|
||||
// Render: clip boundary polygon (or cross at insertion if no boundary).
|
||||
// Grips: insertion point + clip boundary vertices.
|
||||
// Props: position, scales, rotation, contrast, fade, flags.
|
||||
|
||||
use acadrust::entities::{Underlay, UnderlayDisplayFlags};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, 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;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn v3(v: &acadrust::types::Vector3) -> [f32; 3] {
|
||||
[v.x as f32, v.y as f32, v.z as f32]
|
||||
}
|
||||
|
||||
/// Small cross marker at the insertion point (used when no clip boundary).
|
||||
fn cross_wire(origin: [f32; 3], size: f32) -> Vec<[f32; 3]> {
|
||||
let [ox, oy, oz] = origin;
|
||||
vec![
|
||||
[ox - size, oy, oz],
|
||||
[ox + size, oy, oz],
|
||||
[f32::NAN; 3],
|
||||
[ox, oy - size, oz],
|
||||
[ox, oy + size, oz],
|
||||
]
|
||||
}
|
||||
|
||||
// ── TruckConvertible ──────────────────────────────────────────────────────────
|
||||
|
||||
impl TruckConvertible for Underlay {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let origin = v3(&self.insertion_point);
|
||||
|
||||
if !self.clip_boundary_vertices.is_empty() {
|
||||
// Draw clip boundary polygon + close it.
|
||||
let world_verts = self.world_clip_boundary();
|
||||
let mut pts: Vec<[f32; 3]> = world_verts
|
||||
.iter()
|
||||
.map(|v| [v.x as f32, v.y as f32, v.z as f32])
|
||||
.collect();
|
||||
// Close polygon.
|
||||
if let Some(&first) = pts.first() {
|
||||
pts.push(first);
|
||||
}
|
||||
// Insertion grip.
|
||||
let key: Vec<[f32; 3]> = pts.clone();
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![(Vec3::from(origin), SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: key,
|
||||
})
|
||||
} else {
|
||||
// No clip boundary: draw a cross at insertion point.
|
||||
let pts = cross_wire(origin, 1.0);
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![(Vec3::from(origin), SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![origin],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grippable ─────────────────────────────────────────────────────────────────
|
||||
|
||||
impl Grippable for Underlay {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
let origin = Vec3::from(v3(&self.insertion_point));
|
||||
let mut grips = vec![square_grip(0, origin)];
|
||||
|
||||
if !self.clip_boundary_vertices.is_empty() {
|
||||
let world_verts = self.world_clip_boundary();
|
||||
for (i, v) in world_verts.iter().enumerate() {
|
||||
grips.push(diamond_grip(
|
||||
i + 1,
|
||||
Vec3::new(v.x as f32, v.y as f32, v.z as f32),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
grips
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if grip_id == 0 {
|
||||
// Insertion point grip.
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Clip boundary vertex grip (grip_id = vertex_index + 1).
|
||||
let idx = grip_id - 1;
|
||||
if idx >= self.clip_boundary_vertices.len() {
|
||||
return;
|
||||
}
|
||||
// Clip boundary vertices are in local (underlay) space.
|
||||
// We need to invert the world transform to apply the grip.
|
||||
let cos_r = self.rotation.cos();
|
||||
let sin_r = self.rotation.sin();
|
||||
let new_world = match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
// world → local: translate, un-rotate, un-scale
|
||||
let wx = p.x as f64 - self.insertion_point.x;
|
||||
let wy = p.y as f64 - self.insertion_point.y;
|
||||
let lx = (wx * cos_r + wy * sin_r) / self.x_scale.max(1e-10);
|
||||
let ly = (-wx * sin_r + wy * cos_r) / self.y_scale.max(1e-10);
|
||||
(lx, ly)
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
let v = &self.clip_boundary_vertices[idx];
|
||||
let wx = d.x as f64 / self.x_scale.max(1e-10);
|
||||
let wy = d.y as f64 / self.y_scale.max(1e-10);
|
||||
let lx = wx * cos_r + wy * sin_r;
|
||||
let ly = -wx * sin_r + wy * cos_r;
|
||||
(v.x + lx, v.y + ly)
|
||||
}
|
||||
};
|
||||
self.clip_boundary_vertices[idx].x = new_world.0;
|
||||
self.clip_boundary_vertices[idx].y = new_world.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── PropertyEditable ──────────────────────────────────────────────────────────
|
||||
|
||||
impl PropertyEditable for Underlay {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
let type_str = match self.underlay_type {
|
||||
acadrust::entities::UnderlayType::Pdf => "PDF",
|
||||
acadrust::entities::UnderlayType::Dwf => "DWF",
|
||||
acadrust::entities::UnderlayType::Dgn => "DGN",
|
||||
};
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Type", "ul_type", type_str),
|
||||
edit("Ins X", "ul_ix", self.insertion_point.x),
|
||||
edit("Ins Y", "ul_iy", self.insertion_point.y),
|
||||
edit("Ins Z", "ul_iz", self.insertion_point.z),
|
||||
edit("X Scale", "ul_sx", self.x_scale),
|
||||
edit("Y Scale", "ul_sy", self.y_scale),
|
||||
edit("Z Scale", "ul_sz", self.z_scale),
|
||||
edit("Rotation", "ul_rot", self.rotation.to_degrees()),
|
||||
edit("Contrast", "ul_contrast", self.contrast as f64),
|
||||
edit("Fade", "ul_fade", self.fade as f64),
|
||||
ro(
|
||||
"On",
|
||||
"ul_on",
|
||||
if self.flags.contains(UnderlayDisplayFlags::ON) {
|
||||
"Yes"
|
||||
} else {
|
||||
"No"
|
||||
},
|
||||
),
|
||||
ro(
|
||||
"Clipping",
|
||||
"ul_clip",
|
||||
if self.flags.contains(UnderlayDisplayFlags::CLIPPING) {
|
||||
"Yes"
|
||||
} else {
|
||||
"No"
|
||||
},
|
||||
),
|
||||
ro(
|
||||
"Monochrome",
|
||||
"ul_mono",
|
||||
if self.flags.contains(UnderlayDisplayFlags::MONOCHROME) {
|
||||
"Yes"
|
||||
} else {
|
||||
"No"
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
if let Ok(v) = value.trim().parse::<f64>() {
|
||||
match field {
|
||||
"ul_ix" => self.insertion_point.x = v,
|
||||
"ul_iy" => self.insertion_point.y = v,
|
||||
"ul_iz" => self.insertion_point.z = v,
|
||||
"ul_sx" => self.x_scale = v,
|
||||
"ul_sy" => self.y_scale = v,
|
||||
"ul_sz" => self.z_scale = v,
|
||||
"ul_rot" => self.rotation = v.to_radians(),
|
||||
"ul_contrast" => self.set_contrast(v.clamp(0.0, 100.0) as u8),
|
||||
"ul_fade" => self.set_fade(v.clamp(0.0, 80.0) as u8),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transformable ─────────────────────────────────────────────────────────────
|
||||
|
||||
impl Transformable for Underlay {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
use crate::scene::transform::reflect_xy_point;
|
||||
match t {
|
||||
EntityTransform::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;
|
||||
}
|
||||
EntityTransform::Mirror { p1, p2 } => {
|
||||
reflect_xy_point(
|
||||
&mut self.insertion_point.x,
|
||||
&mut self.insertion_point.y,
|
||||
*p1,
|
||||
*p2,
|
||||
);
|
||||
// Reflect rotation angle.
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let axis_angle = dy.atan2(dx);
|
||||
self.rotation = 2.0 * axis_angle - self.rotation;
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => {
|
||||
let bx = center.x as f64;
|
||||
let by = center.y as f64;
|
||||
let bz = center.z as f64;
|
||||
let f = *factor as f64;
|
||||
self.insertion_point.x = bx + (self.insertion_point.x - bx) * f;
|
||||
self.insertion_point.y = by + (self.insertion_point.y - by) * f;
|
||||
self.insertion_point.z = bz + (self.insertion_point.z - bz) * f;
|
||||
self.x_scale *= f;
|
||||
self.y_scale *= f;
|
||||
self.z_scale *= f;
|
||||
}
|
||||
EntityTransform::Rotate { center, angle_rad } => {
|
||||
let bx = center.x as f64;
|
||||
let by = center.y as f64;
|
||||
let a = *angle_rad as f64;
|
||||
let cos_a = a.cos();
|
||||
let sin_a = a.sin();
|
||||
let dx = self.insertion_point.x - bx;
|
||||
let dy = self.insertion_point.y - by;
|
||||
self.insertion_point.x = bx + dx * cos_a - dy * sin_a;
|
||||
self.insertion_point.y = by + dx * sin_a + dy * cos_a;
|
||||
self.rotation += a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue