use crate::entities::{Arc, Ellipse};
use crate::entities::{AttributeEntity, Entity, EntityCommon, EntityType};
use crate::types::{
BoundingBox3D, Color, Handle, LineWeight, Matrix3, Matrix4, Transform, 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,
dwg_minsert: bool,
pub attributes: Vec<AttributeEntity>,
pub view_rep_handle: Option<Handle>,
pub seqend_handle: Option<Handle>,
}
impl Insert {
const MAX_EXPLODE_ARRAY_INSTANCES: usize = 1_000_000;
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,
dwg_minsert: false,
attributes: Vec::new(),
view_rep_handle: None,
seqend_handle: None,
}
}
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.dwg_minsert = true;
self
}
pub fn mark_as_minsert(&mut self) {
self.dwg_minsert = true;
}
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.dwg_minsert || self.is_array()
}
pub fn instance_count(&self) -> usize {
(self.column_count as usize).saturating_mul(self.row_count as usize)
}
fn array_capacity_hint(&self) -> usize {
const MAX_PRERESERVE: usize = 4096;
self.instance_count().min(MAX_PRERESERVE)
}
pub fn array_points(&self) -> Vec<Vector3> {
if self.instance_count() > Self::MAX_EXPLODE_ARRAY_INSTANCES {
return Vec::new();
}
let mut points = Vec::with_capacity(self.array_capacity_hint());
for row in 0..self.row_count {
for col in 0..self.column_count {
points.push(self.array_point(row, col));
}
}
points
}
fn array_point(&self, row: u16, column: u16) -> Vector3 {
let x = column as f64 * self.column_spacing;
let y = row as f64 * self.row_spacing;
let (sin, cos) = self.rotation.sin_cos();
self.insert_point + Vector3::new(x * cos - y * sin, x * sin + y * cos, 0.0)
}
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();
common.layer_handle = self.common.layer_handle;
}
if common.color == Color::ByBlock {
common.color = self.common.color;
common.color_name = self.common.color_name.clone();
common.color_book_handle = self.common.color_book_handle;
}
if common.line_weight == LineWeight::ByBlock {
common.line_weight = self.common.line_weight;
}
if common.transparency.is_by_block() {
common.transparency = self.common.transparency;
}
if common.linetype.eq_ignore_ascii_case("byblock") {
common.linetype = self.common.linetype.clone();
common.linetype_handle = self.common.linetype_handle;
}
}
pub(crate) fn resolve_exploded_properties(&self, common: &mut EntityCommon) {
self.resolve_properties(common);
}
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 {
for mut part in self.explode_single_many(entity, transform) {
Self::detach_exploded_identity(&mut part);
result.push(part);
}
}
}
result
}
pub(crate) fn detach_exploded_identity(entity: &mut EntityType) {
let common = entity.common_mut();
common.handle = Handle::NULL;
common.owner_handle = Handle::NULL;
common.xdictionary_handle = None;
common.reactors.clear();
common.graphic_data = None;
common.raw_record = None;
if let EntityType::Insert(insert) = entity {
for attribute in &mut insert.attributes {
attribute.common.handle = Handle::NULL;
attribute.common.owner_handle = Handle::NULL;
attribute.common.xdictionary_handle = None;
attribute.common.reactors.clear();
attribute.common.graphic_data = None;
attribute.common.raw_record = None;
}
insert.view_rep_handle = None;
insert.seqend_handle = None;
}
}
pub(crate) fn explode_with_transform(
&self,
block_entities: &[EntityType],
transform: &Transform,
detach: bool,
) -> Vec<EntityType> {
let mut result = Vec::new();
for entity in block_entities {
for mut part in self.explode_single_many(entity, transform) {
if detach {
Self::detach_exploded_identity(&mut part);
}
result.push(part);
}
}
result
}
fn array_transforms(&self) -> Vec<Transform> {
if !self.is_minsert() {
return vec![self.get_transform()];
}
if self.instance_count() > Self::MAX_EXPLODE_ARRAY_INSTANCES {
return Vec::new();
}
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.array_capacity_hint());
for row in 0..self.row_count {
for col in 0..self.column_count {
let cell_pt = self.array_point(row, col);
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(_) => None,
EntityType::Arc(arc) => {
if Self::plane_is_similarity(transform, arc.normal) {
Some(self.explode_arc_uniform(arc, transform))
} else {
Some(self.explode_arc_to_ellipse(arc, transform))
}
}
EntityType::Circle(circle) => {
if Self::plane_is_similarity(transform, circle.normal) {
let mut transformed = circle.clone();
super::transform::transform_circle(&mut transformed, transform);
transformed.thickness *= transform.apply_rotation(circle.normal).length();
self.resolve_properties(&mut transformed.common);
Some(EntityType::Circle(transformed))
} else {
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,
};
super::transform::transform_ellipse(&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_single_many(&self, entity: &EntityType, transform: &Transform) -> Vec<EntityType> {
if matches!(
entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_)
) && Self::linear_is_similarity(transform)
{
return entity
.explode()
.into_iter()
.map(|mut piece| {
piece.as_entity_mut().apply_transform(transform);
piece
})
.collect();
}
let normal = match entity {
EntityType::LwPolyline(polyline) => polyline.normal,
EntityType::Polyline2D(polyline) => polyline.normal,
_ => Vector3::UNIT_Z,
};
let has_bulge = match entity {
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
.any(|vertex| vertex.bulge.abs() > 1e-10),
EntityType::Polyline2D(polyline) => polyline
.vertices
.iter()
.any(|vertex| vertex.bulge.abs() > 1e-10),
_ => false,
};
if has_bulge && !Self::plane_is_similarity(transform, normal) {
return entity
.explode()
.iter()
.filter_map(|piece| self.explode_single(piece, transform))
.collect();
}
self.explode_single(entity, transform).into_iter().collect()
}
fn plane_is_similarity(transform: &Transform, normal: Vector3) -> bool {
let basis = Matrix3::arbitrary_axis(normal);
let x = transform.apply_rotation(basis * Vector3::UNIT_X);
let y = transform.apply_rotation(basis * Vector3::UNIT_Y);
let lx = x.length();
let ly = y.length();
lx > SCALE_EPSILON
&& ly > SCALE_EPSILON
&& (lx - ly).abs() <= 1e-10 * lx.max(ly)
&& x.dot(&y).abs() <= 1e-10 * lx * ly
}
fn linear_is_similarity(transform: &Transform) -> bool {
let x = transform.apply_rotation(Vector3::UNIT_X);
let y = transform.apply_rotation(Vector3::UNIT_Y);
let z = transform.apply_rotation(Vector3::UNIT_Z);
let vectors = [x, y, z];
let lengths = vectors.map(|vector| vector.length());
lengths.iter().all(|length| *length > SCALE_EPSILON)
&& (lengths[0] - lengths[1]).abs() <= 1e-10 * lengths[0].max(lengths[1])
&& (lengths[1] - lengths[2]).abs() <= 1e-10 * lengths[1].max(lengths[2])
&& x.dot(&y).abs() <= 1e-10 * lengths[0] * lengths[1]
&& x.dot(&z).abs() <= 1e-10 * lengths[0] * lengths[2]
&& y.dot(&z).abs() <= 1e-10 * lengths[1] * lengths[2]
}
pub(crate) fn has_unrepresentable_shear(transform: &Transform) -> bool {
let x = transform.apply_rotation(Vector3::UNIT_X);
let y = transform.apply_rotation(Vector3::UNIT_Y);
let z = transform.apply_rotation(Vector3::UNIT_Z);
let vectors = [x, y, z];
for i in 0..vectors.len() {
for j in (i + 1)..vectors.len() {
let scale = vectors[i].length() * vectors[j].length();
if scale > SCALE_EPSILON && vectors[i].dot(&vectors[j]).abs() > 1e-10 * scale {
return true;
}
}
}
false
}
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.thickness = arc.thickness * transform.apply_rotation(arc.normal).length();
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,
};
super::transform::transform_ellipse(&mut ellipse, transform);
self.resolve_properties(&mut ellipse.common);
EntityType::Ellipse(ellipse)
}
pub fn block_entities_from_document(
&self,
document: &crate::document::CadDocument,
) -> Option<Vec<EntityType>> {
let br = document.block_records.get(&self.block_name)?;
if !br.explodable
|| br.flags.is_xref
|| br.flags.is_xref_overlay
|| br.flags.is_external
|| !br.xref_path.is_empty()
{
return None;
}
let base = document
.get_entity(br.block_entity_handle)
.and_then(|entity| match entity {
EntityType::Block(block) => Some(block.base_point),
_ => None,
})
.unwrap_or(br.base_point);
let to_base = Transform::from_translation(base * -1.0);
Some(
br.entity_handles
.iter()
.filter_map(|h| {
document.entity_index.get(h).map(|&idx| {
let mut entity = document.entities[idx].as_ref().clone();
entity.as_entity_mut().apply_transform(&to_base);
entity
})
})
.collect(),
)
}
pub fn explode_from_document(
&self,
document: &crate::document::CadDocument,
) -> Vec<EntityType> {
if self.is_minsert() {
return Vec::new();
}
self.block_entities_from_document(document)
.map(|entities| self.explode(&entities))
.unwrap_or_default()
}
}
impl Entity for Insert {
fn common(&self) -> &super::EntityCommon {
&self.common
}
fn common_mut(&mut self) -> &mut super::EntityCommon {
&mut self.common
}
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);
}
fn apply_mirror(&mut self, transform: &Transform) {
super::mirror::mirror_insert(self, transform);
}
fn apply_mirror_with_options(&mut self, transform: &Transform, options: super::MirrorOptions) {
super::mirror::mirror_insert_with_options(self, transform, options);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::{AttributeDefinition, Block, BlockEnd, Circle, Line};
use crate::types::Transparency;
use std::f64::consts::{FRAC_PI_2, PI, TAU};
#[test]
fn huge_minsert_does_not_prereserve_full_array() {
let mut insert = Insert::new("B", Vector3::ZERO);
insert.row_count = u16::MAX;
insert.column_count = u16::MAX;
assert_eq!(insert.instance_count(), 65535 * 65535);
assert!(insert.array_capacity_hint() <= 4096);
assert!(insert.array_points().is_empty());
assert!(insert.explode(&[EntityType::Line(Line::new())]).is_empty());
}
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_restores_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 mut insert = Insert::new("TestBlock", Vector3::ZERO);
insert
.attributes
.push(crate::entities::AttributeEntity::new(
"TAG".into(),
"instance value".into(),
));
let result = insert.explode(&block_entities);
assert_eq!(result.len(), 2);
assert!(matches!(result[0], EntityType::Line(_)));
let EntityType::AttributeDefinition(definition) = &result[1] else {
panic!("expected the attribute definition");
};
assert_eq!(definition.default_value, "default");
assert_eq!(insert.attributes[0].get_value(), "instance value");
}
#[test]
fn explode_minsert_transforms_attdefs_at_every_cell() {
let mut definition =
AttributeDefinition::new("TAG".into(), "Prompt".into(), "Default".into());
definition.insertion_point = Vector3::new(2.0, 3.0, 0.0);
definition.alignment_point = Vector3::new(4.0, 3.0, 0.0);
definition.height = 1.5;
definition.common.color = Color::ByBlock;
definition.common.line_weight = LineWeight::ByBlock;
definition.flags.constant = true;
let input = vec![EntityType::AttributeDefinition(definition.clone())];
let mut insert = Insert::new("B", Vector3::new(100.0, 100.0, 0.0))
.with_uniform_scale(2.0)
.with_rotation(FRAC_PI_2)
.with_array(2, 2, 10.0, 20.0);
insert.common.layer = "Labels".into();
insert.common.color = Color::from_index(3);
insert.common.line_weight = LineWeight::from_value(50);
let pieces = insert.explode(&input);
assert_eq!(pieces.len(), 4);
let expected = [(94.0, 104.0), (94.0, 114.0), (74.0, 104.0), (74.0, 114.0)];
for (piece, (x, y)) in pieces.iter().zip(expected) {
let EntityType::AttributeDefinition(actual) = piece else {
panic!("expected an attribute definition in every array cell");
};
assert!(approx_vec(actual.insertion_point, Vector3::new(x, y, 0.0)));
assert!(approx_vec(
actual.alignment_point,
Vector3::new(x, y + 4.0, 0.0)
));
assert!(approx(actual.height, 3.0));
assert!(approx(actual.rotation, FRAC_PI_2));
assert_eq!(actual.common.layer, "Labels");
assert_eq!(actual.common.color, insert.common.color);
assert_eq!(actual.common.line_weight, insert.common.line_weight);
assert!(actual.flags.constant);
assert_eq!(actual.tag, "TAG");
assert_eq!(actual.prompt, "Prompt");
assert_eq!(actual.default_value, "Default");
}
let EntityType::AttributeDefinition(unchanged) = &input[0] else {
unreachable!()
};
assert_eq!(unchanged.insertion_point, definition.insertion_point);
assert_eq!(unchanged.height, definition.height);
assert_eq!(unchanged.common.layer, definition.common.layer);
}
#[test]
fn explode_from_document_uses_block_base_and_explodable_flag() {
let mut document = crate::CadDocument::new();
let mut record = crate::tables::BlockRecord::new("B");
record.handle = document.allocate_handle();
record.base_point = Vector3::new(40.0, 50.0, 0.0);
let owner = record.handle;
document.block_records.add(record).unwrap();
let mut block = Block::new("B", Vector3::new(10.0, 5.0, 0.0));
block.common.owner_handle = owner;
let marker = document.add_entity(EntityType::Block(block)).unwrap();
document
.block_records
.get_mut("B")
.unwrap()
.block_entity_handle = marker;
let mut definition =
AttributeDefinition::new("TAG".into(), "Prompt".into(), "Default".into());
definition.common.owner_handle = owner;
definition.insertion_point = Vector3::new(12.0, 8.0, 0.0);
document
.add_entity(EntityType::AttributeDefinition(definition))
.unwrap();
let insert = Insert::new("b", Vector3::new(100.0, 100.0, 0.0))
.with_uniform_scale(2.0)
.with_rotation(FRAC_PI_2);
let pieces = insert.explode_from_document(&document);
assert_eq!(pieces.len(), 1);
let EntityType::AttributeDefinition(actual) = &pieces[0] else {
panic!("expected an attribute definition");
};
assert!(approx_vec(
actual.insertion_point,
Vector3::new(94.0, 104.0, 0.0)
));
document
.block_records
.get_mut("B")
.unwrap()
.block_entity_handle = Handle::NULL;
let fallback = insert.explode_from_document(&document);
let EntityType::AttributeDefinition(actual) = &fallback[0] else {
panic!("expected an attribute definition using the record base");
};
assert!(approx_vec(
actual.insertion_point,
Vector3::new(184.0, 44.0, 0.0)
));
document.block_records.get_mut("B").unwrap().explodable = false;
assert!(insert.explode_from_document(&document).is_empty());
assert!(Insert::new("missing", Vector3::ZERO)
.explode_from_document(&document)
.is_empty());
}
#[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_uniform_preserves_circle() {
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::Circle(_)));
if let EntityType::Circle(e) = &result[0] {
assert!(approx(e.radius, 5.0));
} else {
panic!("expected Circle");
}
}
#[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::Circle(_))); 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_byblock_transparency_inherits_insert_transparency() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.transparency = Transparency::ByBlock;
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.transparency = Transparency::new(96);
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().transparency, Transparency::new(96));
}
#[test]
fn explode_byblock_linetype_inherits_insert_linetype() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.linetype = "ByBlock".to_string();
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.linetype = "DASHED".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().linetype, "DASHED");
}
#[test]
fn explode_byblock_linetype_matches_name_case_insensitively() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.linetype = "byblock".to_string();
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.linetype = "DASHED".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().linetype, "DASHED");
}
#[test]
fn explode_byblock_linetype_keeps_the_entities_own_scale() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.linetype = "ByBlock".to_string();
line.common.linetype_scale = 2.0;
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.linetype = "DASHED".to_string();
insert.common.linetype_scale = 0.5;
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().linetype, "DASHED");
assert_eq!(result[0].common().linetype_scale, 2.0);
}
#[test]
fn explode_non_byblock_transparency_preserved() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.transparency = Transparency::new(25);
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.transparency = Transparency::new(96);
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().transparency, Transparency::new(25));
}
#[test]
fn explode_non_byblock_linetype_preserved() {
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(1.0, 0.0, 0.0));
line.common.linetype = "CONTINUOUS".to_string();
let block_entities = vec![EntityType::Line(line)];
let mut insert = Insert::new("B", Vector3::ZERO);
insert.common.linetype = "DASHED".to_string();
let result = insert.explode(&block_entities);
assert_eq!(result[0].common().linetype, "CONTINUOUS");
}
#[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_minsert_positions_follow_rotated_spacing_without_scaling_it() {
let input = vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::ZERO,
))];
let insert = Insert::new("B", Vector3::new(100.0, 100.0, 0.0))
.with_rotation(FRAC_PI_2)
.with_uniform_scale(3.0)
.with_array(2, 2, 10.0, 20.0);
let pieces = insert.explode(&input);
assert_eq!(pieces.len(), 4);
for (piece, (x, y)) in
pieces
.iter()
.zip([(100.0, 100.0), (100.0, 110.0), (80.0, 100.0), (80.0, 110.0)])
{
let EntityType::Line(line) = piece else {
panic!("expected LINE");
};
assert!(approx_vec(line.start, Vector3::new(x, y, 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::Circle(e) = &result[0] {
assert_eq!(e.common.color, Color::from_index(5));
} else {
panic!("expected Circle");
}
}
#[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");
}
}
}