fix(explode): use codec block explosion for attributes and arrays

Delegate INSERT and MINSERT explosion to the document-aware codec implementation, removing duplicate attribute and base-point handling. Update workspace, kernel and graph pins together and cover rotated arrays with a nonzero base.
This commit is contained in:
Karim Jerbi committed 2026-10-13 11:10:11 +01:00
1 parent 9c08e8a3ed
commit 8d287decd8
7 files changed
+77 -79

No files matched your search

+7 -64
View File
@@ -23,8 +23,8 @@ use codec::entities::{
};
use codec::entities::{Polyline, Polyline2D};
use codec::tables::BlockRecord;
use codec::types::{Transform, Vector3};
use codec::{CadDocument, Entity, EntityType, Handle};
use codec::types::Vector3;
use codec::{CadDocument, EntityType, Handle};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::entities::curve::lwpolyline_world_xy;
@@ -98,68 +98,11 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
{
vec![]
}
EntityType::Insert(ins) => {
let explodable = document
.block_records
.get(&ins.block_name)
.map_or(true, |br| br.explodable);
if !explodable {
vec![]
} else {
// The codec applies only `Insert::get_transform`, which does
// not know the block's base point, so shift the definition's
// contents by -base first: get_transform(P - base) is exactly
// world = insert_point + Rot*Scale*(P - base), the placement
// the render path uses. MINSERT cells and nested references
// get the same treatment because the shift happens on the
// contents, not on the insert.
let base = crate::scene::render_graph::block_base_point(document, &ins.block_name);
let to_block = Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z));
let mut attdefs: Vec<EntityType> = Vec::new();
let entities: Vec<EntityType> = document
.entities_in_block(&ins.block_name)
.map(|entity| {
let mut shifted = entity.clone();
Entity::apply_transform(shifted.as_entity_mut(), &to_block);
if let EntityType::AttributeDefinition(_) = &shifted {
attdefs.push(shifted.clone());
}
shifted
})
.collect();
let mut pieces: Vec<EntityType> = ins
.explode(&entities)
.into_iter()
.map(normalize_insert_entity)
.collect();
// Exploding an INSERT drops attribute values (ATTRIBs)
// and the attribute definitions (ATTDEF tags) reappear as ATTDEF entities.
// The codec's `ins.explode` skips `AttributeDefinition` entities, so we
// transform and append them here.
if !attdefs.is_empty() {
let insert_xform = ins.get_transform();
for ad_ent in &attdefs {
let mut exploded_ad = ad_ent.clone();
Entity::apply_transform(exploded_ad.as_entity_mut(), &insert_xform);
// Resolve common properties (layer "0", ByBlock color / line weight)
let common = exploded_ad.common_mut();
if common.layer == "0" {
common.layer = ins.common.layer.clone();
}
if common.color == codec::types::Color::ByBlock {
common.color = ins.common.color;
}
if common.line_weight == codec::types::LineWeight::ByBlock {
common.line_weight = ins.common.line_weight;
}
pieces.push(exploded_ad);
}
}
pieces
}
}
EntityType::Insert(ins) => ins
.explode_from_document(document)
.into_iter()
.map(normalize_insert_entity)
.collect(),
EntityType::MLine(ml) => explode_mline(ml),
EntityType::Dimension(dim) => explode_dimension(dim, document),
EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, document),
+58 -3
View File
@@ -671,9 +671,8 @@ fn locked_block_definition_dialog_prefills_the_existing_definition_base_point()
#[test]
fn locked_explode_places_geometry_at_convention_positions() {
// base-point: `explode_entity` shifts the definition's contents by -base before
// handing them to the codec's explode, so the pieces land where the render
// path puts them (see the module header for the derivation).
// The document-aware codec explosion uses the definition's base point,
// so the pieces land where the render path puts them.
let mut scene = scene_with_bp_test();
let insert = placed_instance(&mut scene);
let pieces = crate::modules::draw::modify::explode::explode_entity(
@@ -739,6 +738,62 @@ fn locked_explode_places_geometry_at_convention_positions() {
);
}
#[test]
fn locked_minsert_explode_restores_attdefs_in_each_rotated_cell() {
let scene = scene_with_bp_test();
let mut insert = Insert::new(BP_TEST, v(100.0, 100.0, 0.0))
.with_uniform_scale(2.0)
.with_rotation(std::f64::consts::FRAC_PI_2)
.with_array(2, 2, 10.0, 20.0);
insert
.attributes
.push(AttributeEntity::new("TAG1".into(), "Instance value".into()));
let pieces = crate::modules::draw::modify::explode::explode_entity(
&EntityType::Insert(insert),
&scene.document,
);
let definitions: Vec<_> = pieces
.iter()
.filter_map(|entity| match entity {
EntityType::AttributeDefinition(definition) => Some(definition),
_ => None,
})
.collect();
assert_eq!(definitions.len(), 4);
for (definition, expected) in definitions.iter().zip([
[94.0, 104.0, 0.0],
[94.0, 114.0, 0.0],
[74.0, 104.0, 0.0],
[74.0, 114.0, 0.0],
]) {
assert_close(definition.insertion_point, expected, 1e-6, "array ATTDEF");
assert_eq!(definition.tag, "TAG1");
assert_eq!(definition.default_value, "");
}
let lines: Vec<_> = pieces
.iter()
.filter_map(|entity| match entity {
EntityType::Line(line) => Some(line),
_ => None,
})
.collect();
assert_eq!(lines.len(), 4);
for (line, expected) in lines.iter().zip([
[100.0, 100.0, 0.0],
[100.0, 110.0, 0.0],
[80.0, 100.0, 0.0],
[80.0, 110.0, 0.0],
]) {
assert_close(line.start, expected, 1e-6, "array LINE");
}
assert_close(
block_attdefs(&scene.document, BP_TEST)[0].insertion_point,
[12.0, 8.0, 0.0],
1e-6,
"source ATTDEF stays in definition coordinates",
);
}
#[test]
fn locked_xclip_boundary_round_trips_through_the_base_point_convention() {
// base-point follow-up fix: the XCLIP ring is stored as WCS points plus the