use crate::entities::{Entity, EntityCommon, EntityType, AttributeEntity};
use crate::entities::{Arc, Ellipse};
use crate::types::{
BoundingBox3D, Color, Handle, LineWeight, Matrix3, Matrix4, Transform,
Transparency, Vector3,
};
const SCALE_EPSILON: f64 = 1e-12;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Insert {
pub common: EntityCommon,
pub block_name: String,
pub insert_point: Vector3,
x_scale: f64,
y_scale: f64,
z_scale: f64,
pub rotation: f64,
pub normal: Vector3,
pub column_count: u16,
pub row_count: u16,
pub column_spacing: f64,
pub row_spacing: f64,
pub attributes: Vec<AttributeEntity>,
}
impl Insert {
pub fn new(block_name: impl Into<String>, insert_point: Vector3) -> Self {
Self {
common: EntityCommon::default(),
block_name: block_name.into(),
insert_point,
x_scale: 1.0,
y_scale: 1.0,
z_scale: 1.0,
rotation: 0.0,
normal: Vector3::new(0.0, 0.0, 1.0),
column_count: 1,
row_count: 1,
column_spacing: 0.0,
row_spacing: 0.0,
attributes: Vec::new(),
}
}
pub fn x_scale(&self) -> f64 {
self.x_scale
}
pub fn set_x_scale(&mut self, value: f64) {
self.x_scale = if value.abs() < SCALE_EPSILON {
SCALE_EPSILON
} else {
value
};
}
pub fn y_scale(&self) -> f64 {
self.y_scale
}
pub fn set_y_scale(&mut self, value: f64) {
self.y_scale = if value.abs() < SCALE_EPSILON {
SCALE_EPSILON
} else {
value
};
}
pub fn z_scale(&self) -> f64 {
self.z_scale
}
pub fn set_z_scale(&mut self, value: f64) {
self.z_scale = if value.abs() < SCALE_EPSILON {
SCALE_EPSILON
} else {
value
};
}
pub fn with_scale(mut self, x: f64, y: f64, z: f64) -> Self {
self.set_x_scale(x);
self.set_y_scale(y);
self.set_z_scale(z);
self
}
pub fn with_uniform_scale(mut self, scale: f64) -> Self {
self.set_x_scale(scale);
self.set_y_scale(scale);
self.set_z_scale(scale);
self
}
pub fn with_rotation(mut self, angle: f64) -> Self {
self.rotation = angle;
self
}
pub fn with_normal(mut self, normal: Vector3) -> Self {
self.normal = normal;
self
}
pub fn with_array(mut self, columns: u16, rows: u16, col_spacing: f64, row_spacing: f64) -> Self {
self.column_count = columns;
self.row_count = rows;
self.column_spacing = col_spacing;
self.row_spacing = row_spacing;
self
}
pub fn has_attributes(&self) -> bool {
!self.attributes.is_empty()
}
pub fn is_array(&self) -> bool {
self.column_count > 1 || self.row_count > 1
}
pub fn is_minsert(&self) -> bool {
self.is_array()
}
pub fn instance_count(&self) -> usize {
(self.column_count as usize) * (self.row_count as usize)
}
pub fn array_points(&self) -> Vec<Vector3> {
let mut points = Vec::with_capacity(self.instance_count());
for row in 0..self.row_count {
for col in 0..self.column_count {
let offset_x = col as f64 * self.column_spacing;
let offset_y = row as f64 * self.row_spacing;
let cos_r = self.rotation.cos();
let sin_r = self.rotation.sin();
let rotated_x = offset_x * cos_r - offset_y * sin_r;
let rotated_y = offset_x * sin_r + offset_y * cos_r;
let point = self.insert_point + Vector3::new(rotated_x, rotated_y, 0.0);
points.push(point);
}
}
points
}
pub fn has_uniform_scale(&self) -> bool {
(self.x_scale - self.y_scale).abs() < 1e-10
&& (self.y_scale - self.z_scale).abs() < 1e-10
}
pub fn uniform_scale(&self) -> Option<f64> {
if self.has_uniform_scale() {
Some(self.x_scale)
} else {
None
}
}
pub fn subclass_marker(&self) -> &'static str {
if self.is_minsert() {
"AcDbMInsertBlock"
} else {
"AcDbBlockReference"
}
}
pub fn get_transform(&self) -> Transform {
let ocs = Matrix4::from_matrix3(Matrix3::arbitrary_axis(self.normal));
let translation = Matrix4::translation(
self.insert_point.x,
self.insert_point.y,
self.insert_point.z,
);
let rotation = Matrix4::rotation_z(self.rotation);
let scale = Matrix4::scaling(self.x_scale, self.y_scale, self.z_scale);
Transform::from_matrix(ocs * translation * rotation * scale)
}
fn transform_normal(transform: &Transform, normal: Vector3) -> Vector3 {
let m4 = transform.matrix;
let upper3x3 = Matrix3::from_rows(
[m4.m[0][0], m4.m[0][1], m4.m[0][2]],
[m4.m[1][0], m4.m[1][1], m4.m[1][2]],
[m4.m[2][0], m4.m[2][1], m4.m[2][2]],
);
if let Some(inv) = upper3x3.inverse() {
let inv_t = inv.transpose();
let transformed = inv_t.transform_point(normal);
let len = transformed.length();
if len < 1e-10 {
normal
} else {
transformed * (1.0 / len)
}
} else {
normal }
}
fn resolve_properties(&self, common: &mut EntityCommon) {
if common.layer == "0" {
common.layer = self.common.layer.clone();
}
if common.color == Color::ByBlock {
common.color = self.common.color;
}
if common.line_weight == LineWeight::ByBlock {
common.line_weight = self.common.line_weight;
}
}
pub fn explode(&self, block_entities: &[EntityType]) -> Vec<EntityType> {
let transforms = self.array_transforms();
let mut result = Vec::new();
for transform in &transforms {
for entity in block_entities {
if let Some(exploded) = self.explode_single(entity, transform) {
result.push(exploded);
}
}
}
result
}
fn array_transforms(&self) -> Vec<Transform> {
if !self.is_minsert() {
return vec![self.get_transform()];
}
let ocs = Matrix4::from_matrix3(Matrix3::arbitrary_axis(self.normal));
let rotation = Matrix4::rotation_z(self.rotation);
let scale = Matrix4::scaling(self.x_scale, self.y_scale, self.z_scale);
let mut transforms = Vec::with_capacity(self.instance_count());
for row in 0..self.row_count {
for col in 0..self.column_count {
let offset_x = col as f64 * self.column_spacing;
let offset_y = row as f64 * self.row_spacing;
let cell_pt = Vector3::new(
self.insert_point.x + offset_x,
self.insert_point.y + offset_y,
self.insert_point.z,
);
let translation = Matrix4::translation(cell_pt.x, cell_pt.y, cell_pt.z);
transforms.push(Transform::from_matrix(ocs * translation * rotation * scale));
}
}
transforms
}
fn explode_single(&self, entity: &EntityType, transform: &Transform) -> Option<EntityType> {
match entity {
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_) => None,
EntityType::Arc(arc) => {
let sx = self.x_scale.abs();
let sy = self.y_scale.abs();
let is_uniform_xy = (sx - sy).abs() < 1e-10;
if is_uniform_xy {
Some(self.explode_arc_uniform(arc, transform))
} else {
Some(self.explode_arc_to_ellipse(arc, transform))
}
}
EntityType::Circle(circle) => {
let ocs_to_wcs = Matrix3::arbitrary_axis(circle.normal);
let mut ellipse = Ellipse {
common: circle.common.clone(),
center: ocs_to_wcs * circle.center,
major_axis: ocs_to_wcs * (Vector3::UNIT_X * circle.radius),
minor_axis_ratio: 1.0,
start_parameter: 0.0,
end_parameter: std::f64::consts::TAU,
normal: circle.normal,
};
Self::apply_full_ellipse_transform(&mut ellipse, transform);
self.resolve_properties(&mut ellipse.common);
Some(EntityType::Ellipse(ellipse))
}
_ => {
let mut cloned = entity.clone();
cloned.as_entity_mut().apply_transform(transform);
self.resolve_properties(cloned.common_mut());
Some(cloned)
}
}
}
fn explode_arc_uniform(&self, arc: &Arc, transform: &Transform) -> EntityType {
let ocs_to_wcs = Matrix3::arbitrary_axis(arc.normal);
let center_wcs = ocs_to_wcs * arc.center;
let wcs_start = center_wcs
+ ocs_to_wcs
* Vector3::new(
arc.radius * arc.start_angle.cos(),
arc.radius * arc.start_angle.sin(),
0.0,
);
let wcs_end = center_wcs
+ ocs_to_wcs
* Vector3::new(
arc.radius * arc.end_angle.cos(),
arc.radius * arc.end_angle.sin(),
0.0,
);
let new_center_wcs = transform.apply(center_wcs);
let new_start_wcs = transform.apply(wcs_start);
let new_end_wcs = transform.apply(wcs_end);
let new_radius = (new_start_wcs - new_center_wcs).length();
let mut new_normal = Self::transform_normal(transform, arc.normal);
if Self::upper3x3_determinant(transform) < 0.0 {
new_normal = new_normal * -1.0;
}
let new_ocs_to_wcs = Matrix3::arbitrary_axis(new_normal);
let new_wcs_to_ocs = new_ocs_to_wcs.transpose();
let new_center_ocs = new_wcs_to_ocs * new_center_wcs;
let ds_ocs = new_wcs_to_ocs * (new_start_wcs - new_center_wcs);
let de_ocs = new_wcs_to_ocs * (new_end_wcs - new_center_wcs);
let new_start_angle = ds_ocs.y.atan2(ds_ocs.x);
let new_end_angle = de_ocs.y.atan2(de_ocs.x);
let mut new_arc = Arc::from_center_radius_angles(
new_center_ocs,
new_radius,
new_start_angle,
new_end_angle,
);
new_arc.normal = new_normal;
new_arc.common = arc.common.clone();
self.resolve_properties(&mut new_arc.common);
EntityType::Arc(new_arc)
}
fn upper3x3_determinant(transform: &Transform) -> f64 {
let m = transform.matrix.m;
Matrix3::from_rows(
[m[0][0], m[0][1], m[0][2]],
[m[1][0], m[1][1], m[1][2]],
[m[2][0], m[2][1], m[2][2]],
)
.determinant()
}
fn explode_arc_to_ellipse(&self, arc: &Arc, transform: &Transform) -> EntityType {
let ocs_to_wcs = Matrix3::arbitrary_axis(arc.normal);
let mut ellipse = Ellipse {
common: arc.common.clone(),
center: ocs_to_wcs * arc.center,
major_axis: ocs_to_wcs * (Vector3::UNIT_X * arc.radius),
minor_axis_ratio: 1.0,
start_parameter: arc.start_angle,
end_parameter: arc.end_angle,
normal: arc.normal,
};
Self::apply_full_ellipse_transform(&mut ellipse, transform);
self.resolve_properties(&mut ellipse.common);
EntityType::Ellipse(ellipse)
}
fn apply_full_ellipse_transform(ellipse: &mut Ellipse, transform: &Transform) {
let center_wcs = ellipse.center;
let major_wcs = ellipse.major_axis;
let original_minor_dir = ellipse.normal.cross(&major_wcs).normalize();
let original_minor_len = major_wcs.length() * ellipse.minor_axis_ratio;
let original_minor = original_minor_dir * original_minor_len;
let new_center_wcs = transform.apply(center_wcs);
let new_major_wcs = transform.apply_rotation(major_wcs);
let new_minor_wcs = transform.apply_rotation(original_minor);
let new_major_len = new_major_wcs.length();
let new_minor_len = new_minor_wcs.length();
let (final_major_wcs, final_minor_wcs, swapped) = if new_minor_len > new_major_len + 1e-12
{
(new_minor_wcs, new_major_wcs, true)
} else {
(new_major_wcs, new_minor_wcs, false)
};
let cross = final_major_wcs.cross(&final_minor_wcs);
let cross_len = cross.length();
let new_normal = if cross_len > 1e-12 {
cross * (1.0 / cross_len)
} else {
Self::transform_normal(transform, ellipse.normal)
};
ellipse.center = new_center_wcs;
ellipse.major_axis = final_major_wcs;
ellipse.minor_axis_ratio = if final_major_wcs.length() > 1e-12 {
final_minor_wcs.length() / final_major_wcs.length()
} else {
1.0
};
ellipse.normal = new_normal;
if swapped {
ellipse.start_parameter -= std::f64::consts::FRAC_PI_2;
ellipse.end_parameter -= std::f64::consts::FRAC_PI_2;
}
}
pub fn explode_from_document(&self, document: &crate::document::CadDocument) -> Vec<EntityType> {
match document.block_records.get(&self.block_name) {
Some(br) => {
let entities: Vec<EntityType> = br
.entity_handles
.iter()
.filter_map(|h| document.entity_index.get(h).map(|&idx| document.entities[idx].clone()))
.collect();
self.explode(&entities)
}
None => Vec::new(),
}
}
}
impl Entity for Insert {
fn handle(&self) -> Handle {
self.common.handle
}
fn set_handle(&mut self, handle: Handle) {
self.common.handle = handle;
}
fn layer(&self) -> &str {
&self.common.layer
}
fn set_layer(&mut self, layer: String) {
self.common.layer = layer;
}
fn color(&self) -> Color {
self.common.color
}
fn set_color(&mut self, color: Color) {
self.common.color = color;
}
fn line_weight(&self) -> LineWeight {
self.common.line_weight
}
fn set_line_weight(&mut self, weight: LineWeight) {
self.common.line_weight = weight;
}
fn transparency(&self) -> Transparency {
self.common.transparency
}
fn set_transparency(&mut self, transparency: Transparency) {
self.common.transparency = transparency;
}
fn is_invisible(&self) -> bool {
self.common.invisible
}
fn set_invisible(&mut self, invisible: bool) {
self.common.invisible = invisible;
}
fn bounding_box(&self) -> BoundingBox3D {
BoundingBox3D::from_point(self.insert_point)
}
fn translate(&mut self, offset: Vector3) {
super::translate::translate_insert(self, offset);
}
fn entity_type(&self) -> &'static str {
if self.is_minsert() { "MINSERT" } else { "INSERT" }
}
fn apply_transform(&mut self, transform: &Transform) {
super::transform::transform_insert(self, transform);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::{
Block, BlockEnd, Circle, Line,
AttributeDefinition,
};
use std::f64::consts::{FRAC_PI_2, PI, TAU};
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
fn approx_vec(a: Vector3, b: Vector3) -> bool {
approx(a.x, b.x) && approx(a.y, b.y) && approx(a.z, b.z)
}
#[test]
fn explode_empty_block_returns_empty() {
let insert = Insert::new("TestBlock", Vector3::ZERO);
let result = insert.explode(&[]);
assert!(result.is_empty());
}
#[test]
fn explode_skips_block_markers_and_attdefs() {
let block_entities = vec![
EntityType::Block(Block::new("TestBlock", Vector3::ZERO)),
EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(1.0, 0.0, 0.0),
)),
EntityType::AttributeDefinition(AttributeDefinition::new(
"TAG".into(),
"prompt".into(),
"default".into(),
)),
EntityType::BlockEnd(BlockEnd::new()),
];
let insert = Insert::new("TestBlock", Vector3::ZERO);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 1);
assert!(matches!(result[0], EntityType::Line(_)));
}
#[test]
fn explode_identity_insert_preserves_line() {
let line = Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::ZERO);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 1);
if let EntityType::Line(l) = &result[0] {
assert!(approx_vec(l.start, Vector3::new(0.0, 0.0, 0.0)));
assert!(approx_vec(l.end, Vector3::new(10.0, 0.0, 0.0)));
} else {
panic!("expected Line");
}
}
#[test]
fn explode_with_translation() {
let line = Line::from_points(Vector3::ZERO, Vector3::new(5.0, 0.0, 0.0));
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::new(10.0, 20.0, 0.0));
let result = insert.explode(&block_entities);
if let EntityType::Line(l) = &result[0] {
assert!(approx_vec(l.start, Vector3::new(10.0, 20.0, 0.0)));
assert!(approx_vec(l.end, Vector3::new(15.0, 20.0, 0.0)));
} else {
panic!("expected Line");
}
}
#[test]
fn explode_with_uniform_scale() {
let line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::ZERO).with_uniform_scale(3.0);
let result = insert.explode(&block_entities);
if let EntityType::Line(l) = &result[0] {
assert!(approx_vec(l.end, Vector3::new(3.0, 0.0, 0.0)));
} else {
panic!("expected Line");
}
}
#[test]
fn explode_with_90_degree_rotation() {
let line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::ZERO).with_rotation(FRAC_PI_2);
let result = insert.explode(&block_entities);
if let EntityType::Line(l) = &result[0] {
assert!(approx_vec(l.end, Vector3::new(0.0, 1.0, 0.0)));
} else {
panic!("expected Line");
}
}
#[test]
fn explode_with_scale_and_translation() {
let line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 1.0, 0.0));
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::new(5.0, 5.0, 0.0))
.with_scale(2.0, 3.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Line(l) = &result[0] {
assert!(approx_vec(l.start, Vector3::new(5.0, 5.0, 0.0)));
assert!(approx_vec(l.end, Vector3::new(7.0, 8.0, 0.0)));
} else {
panic!("expected Line");
}
}
#[test]
fn explode_circle_becomes_ellipse() {
let circle = Circle::from_center_radius(Vector3::ZERO, 5.0);
let block_entities = vec![EntityType::Circle(circle)];
let insert = Insert::new("B", Vector3::ZERO);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 1);
assert!(matches!(result[0], EntityType::Ellipse(_)));
if let EntityType::Ellipse(e) = &result[0] {
assert!(approx(e.start_parameter, 0.0));
assert!(approx(e.end_parameter, TAU));
} else {
panic!("expected Ellipse");
}
}
#[test]
fn explode_circle_with_non_uniform_scale() {
let circle = Circle::from_center_radius(Vector3::ZERO, 1.0);
let block_entities = vec![EntityType::Circle(circle)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(2.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Ellipse(e) = &result[0] {
let major_len = e.major_axis.length();
let minor_len = major_len * e.minor_axis_ratio;
assert!(approx(major_len, 2.0));
assert!(approx(minor_len, 1.0));
assert!(approx(e.minor_axis_ratio, 0.5));
} else {
panic!("expected Ellipse");
}
}
#[test]
fn explode_circle_non_uniform_scale_minor_becomes_major() {
let circle = Circle::from_center_radius(Vector3::ZERO, 1.0);
let block_entities = vec![EntityType::Circle(circle)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(1.0, 3.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Ellipse(e) = &result[0] {
let major_len = e.major_axis.length();
let minor_len = major_len * e.minor_axis_ratio;
assert!(approx(major_len, 3.0));
assert!(approx(minor_len, 1.0));
} else {
panic!("expected Ellipse");
}
}
#[test]
fn explode_arc_identity() {
let arc = Arc::from_center_radius_angles(
Vector3::ZERO,
5.0,
0.0,
FRAC_PI_2,
);
let block_entities = vec![EntityType::Arc(arc.clone())];
let insert = Insert::new("B", Vector3::ZERO);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 1);
if let EntityType::Arc(a) = &result[0] {
assert!(approx(a.radius, 5.0));
assert!(approx(a.start_angle, 0.0));
assert!(approx(a.end_angle, FRAC_PI_2));
assert!(approx_vec(a.center, Vector3::ZERO));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_with_translation() {
let arc = Arc::from_center_radius_angles(
Vector3::ZERO,
10.0,
0.0,
PI,
);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::new(100.0, 200.0, 0.0));
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert!(approx_vec(a.center, Vector3::new(100.0, 200.0, 0.0)));
assert!(approx(a.radius, 10.0));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_with_uniform_scale() {
let arc = Arc::from_center_radius_angles(
Vector3::new(1.0, 1.0, 0.0),
2.0,
0.0,
FRAC_PI_2,
);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_uniform_scale(3.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert!(approx(a.radius, 6.0)); assert!(approx_vec(a.center, Vector3::new(3.0, 3.0, 0.0)));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_non_uniform_becomes_ellipse() {
let arc = Arc::from_center_radius_angles(
Vector3::ZERO,
5.0,
0.0,
FRAC_PI_2,
);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(2.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 1);
if let EntityType::Ellipse(e) = &result[0] {
let major_len = e.major_axis.length();
let minor_len = major_len * e.minor_axis_ratio;
assert!(approx(major_len, 10.0));
assert!(approx(minor_len, 5.0));
assert!(!e.is_full());
} else {
panic!("expected Ellipse for non-uniform arc");
}
}
#[test]
fn explode_multiple_entities() {
let block_entities = vec![
EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(1.0, 0.0, 0.0),
)),
EntityType::Circle(Circle::from_center_radius(Vector3::ZERO, 1.0)),
EntityType::Arc(Arc::from_center_radius_angles(
Vector3::ZERO, 1.0, 0.0, PI,
)),
];
let insert = Insert::new("B", Vector3::new(10.0, 0.0, 0.0));
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 3);
assert!(matches!(result[0], EntityType::Line(_)));
assert!(matches!(result[1], EntityType::Ellipse(_))); assert!(matches!(result[2], EntityType::Arc(_))); }
#[test]
fn explode_layer_zero_inherits_insert_layer() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.layer = "0".to_string(); let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.layer = "Walls".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().layer, "Walls");
}
#[test]
fn explode_named_layer_preserved() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.layer = "Hidden".to_string();
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.layer = "Walls".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().layer, "Hidden");
}
#[test]
fn explode_byblock_color_inherits_insert_color() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.color = Color::ByBlock;
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.color = Color::from_index(1); let result = insert.explode(&block_entities);
assert_eq!(result[0].common().color, Color::from_index(1));
}
#[test]
fn explode_byblock_lineweight_inherits_insert_lineweight() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.line_weight = LineWeight::ByBlock;
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.line_weight = LineWeight::from_value(50); let result = insert.explode(&block_entities);
assert_eq!(result[0].common().line_weight, LineWeight::from_value(50));
}
#[test]
fn explode_non_byblock_color_preserved() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.color = Color::from_index(3); let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.color = Color::from_index(1); let result = insert.explode(&block_entities);
assert_eq!(result[0].common().color, Color::from_index(3));
}
#[test]
fn explode_minsert_produces_array_copies() {
let line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::new(0.0, 0.0, 0.0))
.with_array(2, 3, 10.0, 20.0);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 6);
for e in &result {
assert!(matches!(e, EntityType::Line(_)));
}
}
#[test]
fn explode_minsert_positions_correct() {
let line = Line::from_points(Vector3::ZERO, Vector3::ZERO); let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::new(5.0, 0.0, 0.0))
.with_array(2, 1, 10.0, 0.0);
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 2);
if let EntityType::Line(l0) = &result[0] {
assert!(approx_vec(l0.start, Vector3::new(5.0, 0.0, 0.0)));
}
if let EntityType::Line(l1) = &result[1] {
assert!(approx_vec(l1.start, Vector3::new(15.0, 0.0, 0.0)));
}
}
#[test]
fn explode_arc_layer_zero_inherits() {
let mut arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, PI);
arc.common.layer = "0".to_string();
let block_entities = vec![EntityType::Arc(arc)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.layer = "Pipes".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().layer, "Pipes");
}
#[test]
fn explode_circle_byblock_color_inherits() {
let mut circle = Circle::from_center_radius(Vector3::ZERO, 1.0);
circle.common.color = Color::ByBlock;
let block_entities = vec![EntityType::Circle(circle)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.color = Color::from_index(5); let result = insert.explode(&block_entities);
if let EntityType::Ellipse(e) = &result[0] {
assert_eq!(e.common.color, Color::from_index(5));
} else {
panic!("expected Ellipse");
}
}
#[test]
fn explode_preserves_entity_layer() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.layer = "MyLayer".to_string();
let block_entities = vec![EntityType::Line(line)];
let insert = Insert::new("B", Vector3::ZERO);
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().layer, "MyLayer");
}
#[test]
fn explode_arc_preserves_layer() {
let mut arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, PI);
arc.common.layer = "ArcLayer".to_string();
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert_eq!(a.common.layer, "ArcLayer");
} else {
panic!("expected Arc");
}
}
fn arc_sample_points(arc: &Arc) -> (Vector3, Vector3, Vector3) {
let basis = Matrix3::arbitrary_axis(arc.normal);
let center_wcs = basis * arc.center;
let ccw_end = if arc.end_angle >= arc.start_angle {
arc.end_angle
} else {
arc.end_angle + TAU
};
let mid = arc.start_angle + (ccw_end - arc.start_angle) * 0.5;
let pt = |a: f64| {
center_wcs
+ basis * Vector3::new(arc.radius * a.cos(), arc.radius * a.sin(), 0.0)
};
(pt(arc.start_angle), pt(mid), pt(arc.end_angle))
}
#[test]
fn explode_arc_mirror_x_preserves_sweep_direction() {
let arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, FRAC_PI_2);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(-1.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
let (start, mid, end) = arc_sample_points(a);
assert!(approx_vec(start, Vector3::new(-1.0, 0.0, 0.0)));
assert!(approx_vec(end, Vector3::new(0.0, 1.0, 0.0)));
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(mid, Vector3::new(-inv_sqrt2, inv_sqrt2, 0.0)));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_mirror_y_preserves_sweep_direction() {
let arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, FRAC_PI_2);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(1.0, -1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
let (start, mid, end) = arc_sample_points(a);
assert!(approx_vec(start, Vector3::new(1.0, 0.0, 0.0)));
assert!(approx_vec(end, Vector3::new(0.0, -1.0, 0.0)));
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(mid, Vector3::new(inv_sqrt2, -inv_sqrt2, 0.0)));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_double_mirror_is_rotation() {
let arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, FRAC_PI_2);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(-1.0, -1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert!(approx_vec(a.normal, Vector3::new(0.0, 0.0, 1.0)));
let (start, mid, end) = arc_sample_points(a);
assert!(approx_vec(start, Vector3::new(-1.0, 0.0, 0.0)));
assert!(approx_vec(end, Vector3::new(0.0, -1.0, 0.0)));
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(mid, Vector3::new(-inv_sqrt2, -inv_sqrt2, 0.0)));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_mirror_x_offset_center_position_preserved() {
let arc = Arc::from_center_radius_angles(
Vector3::new(5.0, 3.0, 0.0),
1.0,
0.0,
FRAC_PI_2,
);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(-1.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert!(approx_vec(a.normal, Vector3::new(0.0, 0.0, -1.0)));
let (start, mid, end) = arc_sample_points(a);
assert!(approx_vec(start, Vector3::new(-6.0, 3.0, 0.0)));
assert!(approx_vec(end, Vector3::new(-5.0, 4.0, 0.0)));
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(
mid,
Vector3::new(-5.0 - inv_sqrt2, 3.0 + inv_sqrt2, 0.0)
));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_mirror_x_with_translation_position_preserved() {
let arc = Arc::from_center_radius_angles(
Vector3::new(2.0, 0.0, 0.0),
1.0,
0.0,
FRAC_PI_2,
);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::new(10.0, 20.0, 0.0)).with_scale(-1.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
let (start, mid, end) = arc_sample_points(a);
assert!(approx_vec(start, Vector3::new(7.0, 20.0, 0.0)));
assert!(approx_vec(end, Vector3::new(8.0, 21.0, 0.0)));
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(
mid,
Vector3::new(8.0 - inv_sqrt2, 20.0 + inv_sqrt2, 0.0)
));
} else {
panic!("expected Arc");
}
}
#[test]
fn explode_arc_mirror_flips_normal() {
let arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, FRAC_PI_2);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(-1.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Arc(a) = &result[0] {
assert!(approx_vec(a.normal, Vector3::new(0.0, 0.0, -1.0)));
} else {
panic!("expected Arc");
}
}
fn ellipse_sample_points(e: &Ellipse) -> (Vector3, Vector3, Vector3) {
let center_wcs = e.center;
let major_wcs = e.major_axis;
let major_len = major_wcs.length();
let u = major_wcs * (1.0 / major_len.max(1e-12));
let v = e.normal.cross(&u);
let minor_len = major_len * e.minor_axis_ratio;
let mut t1 = e.end_parameter;
if t1 <= e.start_parameter {
t1 += TAU;
}
let pt = |t: f64| {
center_wcs + u * (major_len * t.cos()) + v * (minor_len * t.sin())
};
let mid = e.start_parameter + (t1 - e.start_parameter) * 0.5;
(pt(e.start_parameter), pt(mid), pt(t1))
}
#[test]
fn explode_arc_mirror_x_nonuniform_to_ellipse() {
let arc = Arc::from_center_radius_angles(Vector3::ZERO, 1.0, 0.0, FRAC_PI_2);
let block_entities = vec![EntityType::Arc(arc)];
let insert = Insert::new("B", Vector3::ZERO).with_scale(-2.0, 1.0, 1.0);
let result = insert.explode(&block_entities);
if let EntityType::Ellipse(e) = &result[0] {
let (start, mid, end) = ellipse_sample_points(e);
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!(approx_vec(start, Vector3::new(-2.0, 0.0, 0.0)));
assert!(approx_vec(mid, Vector3::new(-2.0 * inv_sqrt2, inv_sqrt2, 0.0)));
assert!(approx_vec(end, Vector3::new(0.0, 1.0, 0.0)));
} else {
panic!("expected Ellipse for non-uniform mirrored scale");
}
}
}