diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs new file mode 100644 index 00000000..1aafe481 --- /dev/null +++ b/src/entities/attribute.rs @@ -0,0 +1,219 @@ +use acadrust::entities::{AttributeDefinition, AttributeEntity}; +use glam::Vec3; + +use crate::command::EntityTransform; +use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::text_support::resolve_text_style; +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}; + +// ── AttributeDefinition ─────────────────────────────────────────────────────── + +impl TruckConvertible for AttributeDefinition { + fn to_truck(&self, document: &acadrust::CadDocument) -> Option { + let snap_pt = Vec3::new( + self.insertion_point.x as f32, + self.insertion_point.y as f32, + self.insertion_point.z as f32, + ); + let resolved = resolve_text_style(&self.text_style, document); + let display = if self.default_value.is_empty() { + format!("[{}]", self.tag) + } else { + self.default_value.clone() + }; + let wf = (self.width_factor as f32).max(0.01); + let strokes = cxf::tessellate_text_ex( + [self.insertion_point.x as f32, self.insertion_point.y as f32], + self.height as f32, + self.rotation as f32, + wf * resolved.width_factor.max(0.01), + self.oblique_angle as f32 + resolved.oblique_angle, + &resolved.font_name, + &display, + ); + Some(TruckEntity { + object: TruckObject::Text(strokes), + snap_pts: vec![(snap_pt, SnapHint::Insertion)], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} + +impl Grippable for AttributeDefinition { + 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 AttributeDefinition { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Tag", "att_tag", self.tag.clone()), + ro("Prompt", "att_prompt", self.prompt.clone()), + edit("Default", "att_default", 0.0), // String — handled as text + edit("Insert X", "att_ix", self.insertion_point.x), + edit("Insert Y", "att_iy", self.insertion_point.y), + edit("Insert Z", "att_iz", self.insertion_point.z), + edit("Height", "att_h", self.height), + edit("Rotation", "att_rot", self.rotation.to_degrees()), + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Ok(v) = value.trim().parse::() else { return }; + match field { + "att_ix" => self.insertion_point.x = v, + "att_iy" => self.insertion_point.y = v, + "att_iz" => self.insertion_point.z = v, + "att_h" if v > 0.0 => self.height = v, + "att_rot" => self.rotation = v.to_radians(), + _ => {} + } + } +} + +impl Transformable for AttributeDefinition { + 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, + ); + }); + } +} + +// ── AttributeEntity ─────────────────────────────────────────────────────────── + +impl TruckConvertible for AttributeEntity { + fn to_truck(&self, document: &acadrust::CadDocument) -> Option { + let snap_pt = Vec3::new( + self.insertion_point.x as f32, + self.insertion_point.y as f32, + self.insertion_point.z as f32, + ); + let resolved = resolve_text_style(&self.text_style, document); + let wf = (self.width_factor as f32).max(0.01); + let strokes = cxf::tessellate_text_ex( + [self.insertion_point.x as f32, self.insertion_point.y as f32], + self.height as f32, + self.rotation as f32, + wf * resolved.width_factor.max(0.01), + self.oblique_angle as f32 + resolved.oblique_angle, + &resolved.font_name, + &self.value, + ); + Some(TruckEntity { + object: TruckObject::Text(strokes), + snap_pts: vec![(snap_pt, SnapHint::Insertion)], + tangent_geoms: vec![], + key_vertices: vec![], + }) + } +} + +impl Grippable for AttributeEntity { + 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 AttributeEntity { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Tag", "atte_tag", self.tag.clone()), + ro("Value", "atte_val", self.value.clone()), + edit("Insert X", "atte_ix", self.insertion_point.x), + edit("Insert Y", "atte_iy", self.insertion_point.y), + edit("Insert Z", "atte_iz", self.insertion_point.z), + edit("Height", "atte_h", self.height), + edit("Rotation", "atte_rot", self.rotation.to_degrees()), + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Ok(v) = value.trim().parse::() else { return }; + match field { + "atte_ix" => self.insertion_point.x = v, + "atte_iy" => self.insertion_point.y = v, + "atte_iz" => self.insertion_point.z = v, + "atte_h" if v > 0.0 => self.height = v, + "atte_rot" => self.rotation = v.to_radians(), + _ => {} + } + } +} + +impl Transformable for AttributeEntity { + 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/mod.rs b/src/entities/mod.rs index b3778a87..8efe2316 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,4 +1,5 @@ pub mod arc; +pub mod attribute; pub mod circle; mod common; pub mod dimension; @@ -12,6 +13,7 @@ pub mod lwpolyline; pub mod mtext; pub mod point; pub mod polyline; +pub mod raster_image; pub mod ray; pub mod spline; pub mod text; diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs new file mode 100644 index 00000000..04b03866 --- /dev/null +++ b/src/entities/raster_image.rs @@ -0,0 +1,350 @@ +use acadrust::entities::{RasterImage, Wipeout}; +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, PropValue, Property}; + +// ── Shared geometry helpers ─────────────────────────────────────────────────── + +/// Compute the four world-space corners of an image/wipeout from its +/// insertion_point, u_vector, v_vector and pixel size. +/// +/// Returns (p0, p1, p2, p3) in counter-clockwise order: +/// p0 = origin +/// p1 = origin + U*W +/// p2 = origin + U*W + V*H +/// p3 = origin + V*H +fn image_corners( + origin: &acadrust::types::Vector3, + u: &acadrust::types::Vector3, + v: &acadrust::types::Vector3, + w: f64, + h: f64, +) -> [[f32; 3]; 4] { + let ox = origin.x as f32; + let oy = origin.y as f32; + let oz = origin.z as f32; + let ux = (u.x * w) as f32; + let uy = (u.y * w) as f32; + let uz = (u.z * w) as f32; + let vx = (v.x * h) as f32; + let vy = (v.y * h) as f32; + let vz = (v.z * h) as f32; + + [ + [ox, oy, oz], + [ox + ux, oy + uy, oz + uz], + [ox + ux + vx, oy + uy + vy, oz + uz + vz], + [ox + vx, oy + vy, oz + vz], + ] +} + +/// Rectangle border + X diagonals — used as a placeholder for images. +fn image_wire(corners: [[f32; 3]; 4], with_x: bool) -> Vec<[f32; 3]> { + let [p0, p1, p2, p3] = corners; + let mut pts = vec![p0, p1, p2, p3, p0]; + if with_x { + pts.push([f32::NAN; 3]); + pts.push(p0); + pts.push(p2); + pts.push([f32::NAN; 3]); + pts.push(p1); + pts.push(p3); + } + pts +} + +fn reflect_vec3( + vx: &mut f64, + vy: &mut f64, + ax: f64, + ay: f64, + len2: f64, +) { + let dot = *vx * ax + *vy * ay; + *vx = 2.0 * dot * ax / len2 - *vx; + *vy = 2.0 * dot * ay / len2 - *vy; +} + +// ── RasterImage ─────────────────────────────────────────────────────────────── + +impl TruckConvertible for RasterImage { + fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + let corners = image_corners( + &self.insertion_point, + &self.u_vector, + &self.v_vector, + self.size.x, + self.size.y, + ); + Some(TruckEntity { + object: TruckObject::Lines(image_wire(corners, true)), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: corners.to_vec(), + }) + } +} + +impl Grippable for RasterImage { + fn grips(&self) -> Vec { + let corners = image_corners( + &self.insertion_point, + &self.u_vector, + &self.v_vector, + self.size.x, + self.size.y, + ); + vec![ + square_grip(0, Vec3::from(corners[0])), + diamond_grip(1, Vec3::from(corners[1])), + diamond_grip(2, Vec3::from(corners[2])), + diamond_grip(3, Vec3::from(corners[3])), + ] + } + + 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; + } + } + } + // Corner grips 1-3 are display-only (resizing changes u/v vectors, + // which requires careful normalization — deferred). + } +} + +impl PropertyEditable for RasterImage { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + ro("File", "ri_file", self.file_path.clone()), + edit("Insert X", "ri_ox", self.insertion_point.x), + edit("Insert Y", "ri_oy", self.insertion_point.y), + edit("Insert Z", "ri_oz", self.insertion_point.z), + edit("Brightness", "ri_bright", self.brightness as f64), + edit("Contrast", "ri_contrast", self.contrast as f64), + edit("Fade", "ri_fade", self.fade as f64), + Property { + label: "Clipping".into(), + field: "ri_clip", + value: PropValue::BoolToggle { + field: "ri_clip", + value: self.clipping_enabled, + }, + }, + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "ri_clip" => { + self.clipping_enabled = + if value == "toggle" { !self.clipping_enabled } else { value == "true" }; + return; + } + _ => {} + } + let Ok(v) = value.trim().parse::() else { return }; + match field { + "ri_ox" => self.insertion_point.x = v, + "ri_oy" => self.insertion_point.y = v, + "ri_oz" => self.insertion_point.z = v, + "ri_bright" => self.brightness = v.clamp(0.0, 100.0) as u8, + "ri_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8, + "ri_fade" => self.fade = v.clamp(0.0, 100.0) as u8, + _ => {} + } + } +} + +impl Transformable for RasterImage { + fn apply_transform(&mut self, t: &EntityTransform) { + crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { + crate::scene::transform::reflect_xy_point( + &mut entity.insertion_point.x, + &mut entity.insertion_point.y, + p1, + p2, + ); + let ax = (p2.x - p1.x) as f64; + let ay = (p2.y - p1.y) as f64; + let len2 = ax * ax + ay * ay; + if len2 > 1e-12 { + reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2); + reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2); + } + }); + } +} + +// ── Wipeout ─────────────────────────────────────────────────────────────────── + +impl TruckConvertible for Wipeout { + fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + let corners = image_corners( + &self.insertion_point, + &self.u_vector, + &self.v_vector, + self.size.x, + self.size.y, + ); + + // If clipping is enabled and there's a polygon boundary, show that. + let pts = if self.clipping_enabled + && self.clip_boundary_vertices.len() >= 3 + && matches!( + self.clip_type, + acadrust::entities::WipeoutClipType::Polygonal + ) + { + // Convert pixel-space boundary vertices to world space: + // world = insertion_point + u_vector * v.x * size.x + v_vector * v.y * size.y + let ox = self.insertion_point.x as f32; + let oy = self.insertion_point.y as f32; + let oz = self.insertion_point.z as f32; + let mut poly: Vec<[f32; 3]> = self + .clip_boundary_vertices + .iter() + .map(|v| { + let wx = (self.u_vector.x * v.x * self.size.x + + self.v_vector.x * v.y * self.size.y) as f32; + let wy = (self.u_vector.y * v.x * self.size.x + + self.v_vector.y * v.y * self.size.y) as f32; + let wz = (self.u_vector.z * v.x * self.size.x + + self.v_vector.z * v.y * self.size.y) as f32; + [ox + wx, oy + wy, oz + wz] + }) + .collect(); + // Close the polygon. + if let Some(&first) = poly.first() { + poly.push(first); + } + poly + } else { + // Rectangular boundary — just the border, no diagonals (mask area). + image_wire(corners, false) + }; + + Some(TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: corners.to_vec(), + }) + } +} + +impl Grippable for Wipeout { + fn grips(&self) -> Vec { + let corners = image_corners( + &self.insertion_point, + &self.u_vector, + &self.v_vector, + self.size.x, + self.size.y, + ); + vec![ + square_grip(0, Vec3::from(corners[0])), + diamond_grip(1, Vec3::from(corners[1])), + diamond_grip(2, Vec3::from(corners[2])), + diamond_grip(3, Vec3::from(corners[3])), + ] + } + + 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 Wipeout { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Insert X", "wo_ox", self.insertion_point.x), + edit("Insert Y", "wo_oy", self.insertion_point.y), + edit("Insert Z", "wo_oz", self.insertion_point.z), + edit("Brightness", "wo_bright", self.brightness as f64), + edit("Contrast", "wo_contrast", self.contrast as f64), + edit("Fade", "wo_fade", self.fade as f64), + Property { + label: "Clipping".into(), + field: "wo_clip", + value: PropValue::BoolToggle { + field: "wo_clip", + value: self.clipping_enabled, + }, + }, + ], + } + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "wo_clip" => { + self.clipping_enabled = + if value == "toggle" { !self.clipping_enabled } else { value == "true" }; + return; + } + _ => {} + } + let Ok(v) = value.trim().parse::() else { return }; + match field { + "wo_ox" => self.insertion_point.x = v, + "wo_oy" => self.insertion_point.y = v, + "wo_oz" => self.insertion_point.z = v, + "wo_bright" => self.brightness = v.clamp(0.0, 100.0) as u8, + "wo_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8, + "wo_fade" => self.fade = v.clamp(0.0, 100.0) as u8, + _ => {} + } + } +} + +impl Transformable for Wipeout { + fn apply_transform(&mut self, t: &EntityTransform) { + crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| { + crate::scene::transform::reflect_xy_point( + &mut entity.insertion_point.x, + &mut entity.insertion_point.y, + p1, + p2, + ); + let ax = (p2.x - p1.x) as f64; + let ay = (p2.y - p1.y) as f64; + let len2 = ax * ax + ay * ay; + if len2 > 1e-12 { + reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2); + reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2); + } + }); + } +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index a201474d..f6dbfdad 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -46,6 +46,10 @@ impl EntityTypeOps for EntityType { EntityType::Polyline3D(pl) => TruckConvertible::to_truck(pl, document), EntityType::Ray(ray) => TruckConvertible::to_truck(ray, document), EntityType::XLine(xl) => TruckConvertible::to_truck(xl, document), + EntityType::RasterImage(img) => TruckConvertible::to_truck(img, document), + 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::Text(text) => TruckConvertible::to_truck(text, document), EntityType::MText(text) => TruckConvertible::to_truck(text, document), EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document), @@ -66,6 +70,10 @@ impl EntityTypeOps for EntityType { EntityType::Polyline3D(pl) => Grippable::grips(pl), EntityType::Ray(ray) => Grippable::grips(ray), EntityType::XLine(xl) => Grippable::grips(xl), + EntityType::RasterImage(img) => Grippable::grips(img), + EntityType::Wipeout(wo) => Grippable::grips(wo), + EntityType::AttributeDefinition(a) => Grippable::grips(a), + EntityType::AttributeEntity(a) => Grippable::grips(a), EntityType::Point(pt) => Grippable::grips(pt), EntityType::Spline(spline) => Grippable::grips(spline), EntityType::Text(text) => Grippable::grips(text), @@ -119,6 +127,22 @@ impl EntityTypeOps for EntityType { xl, text_style_names, )), + EntityType::RasterImage(img) => Some(PropertyEditable::geometry_properties( + img, + text_style_names, + )), + EntityType::Wipeout(wo) => Some(PropertyEditable::geometry_properties( + wo, + text_style_names, + )), + EntityType::AttributeDefinition(a) => Some(PropertyEditable::geometry_properties( + a, + text_style_names, + )), + EntityType::AttributeEntity(a) => Some(PropertyEditable::geometry_properties( + a, + text_style_names, + )), EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties( hatch, text_style_names, @@ -174,6 +198,10 @@ impl EntityTypeOps for EntityType { EntityType::Polyline3D(pl) => PropertyEditable::apply_geom_prop(pl, field, value), EntityType::Ray(ray) => PropertyEditable::apply_geom_prop(ray, field, value), EntityType::XLine(xl) => PropertyEditable::apply_geom_prop(xl, field, value), + EntityType::RasterImage(img) => PropertyEditable::apply_geom_prop(img, field, value), + 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::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), @@ -200,6 +228,10 @@ impl EntityTypeOps for EntityType { EntityType::Polyline3D(pl) => Grippable::apply_grip(pl, grip_id, apply), EntityType::Ray(ray) => Grippable::apply_grip(ray, grip_id, apply), EntityType::XLine(xl) => Grippable::apply_grip(xl, grip_id, apply), + EntityType::RasterImage(img) => Grippable::apply_grip(img, grip_id, apply), + 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::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), @@ -226,6 +258,10 @@ impl EntityTypeOps for EntityType { EntityType::Polyline3D(pl) => Transformable::apply_transform(pl, t), EntityType::Ray(ray) => Transformable::apply_transform(ray, t), EntityType::XLine(xl) => Transformable::apply_transform(xl, t), + EntityType::RasterImage(img) => Transformable::apply_transform(img, t), + 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::MText(text) => Transformable::apply_transform(text, t), EntityType::Point(pt) => Transformable::apply_transform(pt, t), EntityType::Spline(spline) => Transformable::apply_transform(spline, t),