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
+75 -77

No files matched your search

Generated
+4 -4
View File
@@ -4361,7 +4361,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadcodec" name = "opencadcodec"
version = "0.6.0" version = "0.6.0"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=ddf0fd2#ddf0fd2d7541bebce137c1bf960db54fcd12bcd2" source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8c4b9a3#8c4b9a3783cf68a1cbb383eaa61cac043066cf22"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.13.1", "bitflags 2.13.1",
@@ -4384,7 +4384,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadgraph" name = "opencadgraph"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=45c9649#45c96493fb55d8825561bca9e9d7e5e700dcd3fa" source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=6a2babc#6a2babc82532143500d66a92ed9fbc14d14061fa"
dependencies = [ dependencies = [
"opencadkernel", "opencadkernel",
"serde_json", "serde_json",
@@ -4393,7 +4393,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadkernel" name = "opencadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=b80079a#b80079a1e61b3f3519dc759e6553761e24787f67" source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=ee970e4#ee970e41decd4f96548f472e5078d8a4335f73ce"
dependencies = [ dependencies = [
"cavalier_contours", "cavalier_contours",
"opencadcodec", "opencadcodec",
@@ -4404,7 +4404,7 @@ dependencies = [
[[package]] [[package]]
name = "opencadkernel-constraints" name = "opencadkernel-constraints"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=b80079a#b80079a1e61b3f3519dc759e6553761e24787f67" source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=ee970e4#ee970e41decd4f96548f472e5078d8a4335f73ce"
dependencies = [ dependencies = [
"nalgebra", "nalgebra",
] ]
+4 -4
View File
@@ -37,10 +37,10 @@ rfd = "0.17"
clap = { version = "4", features = ["derive", "string"] } clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11" env_logger = "0.11"
log = "0.4" log = "0.4"
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ddf0fd2", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8c4b9a3", features = ["serde"] }
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "b80079a", features = ["acis", "offset"] } kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "ee970e4", features = ["acis", "offset"] }
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "b80079a" } kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "ee970e4" }
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "45c9649" } graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "6a2babc" }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
# Shared SAVE serialization dispatch + web-worker wire format: one # Shared SAVE serialization dispatch + web-worker wire format: one
# implementation for the inline (fallback/native) path and the worker path. # implementation for the inline (fallback/native) path and the worker path.
+2 -2
View File
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine # `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply. # crates and external tooling can depend on the manifest/ribbon contract cheaply.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ddf0fd2", optional = true, features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8c4b9a3", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only). # Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true } interprocess = { version = "2", optional = true }
@@ -39,7 +39,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
cargo-lock = "11" cargo-lock = "11"
# opencadcodec is scanned at build time to generate the embedded type registry. # opencadcodec is scanned at build time to generate the embedded type registry.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ddf0fd2", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8c4b9a3", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
serde_json = "1" serde_json = "1"
+1 -1
View File
@@ -10,6 +10,6 @@ publish = false
# exactly one implementation. No UI/app dependencies — only the codec + serde. # exactly one implementation. No UI/app dependencies — only the codec + serde.
[dependencies] [dependencies]
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ddf0fd2", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8c4b9a3", features = ["serde"] }
bincode = "1.3" bincode = "1.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
+1 -1
View File
@@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ddf0fd2", features = ["serde"] } codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8c4b9a3", features = ["serde"] }
bincode = "1.3" bincode = "1.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
+5 -62
View File
@@ -23,8 +23,8 @@ use codec::entities::{
}; };
use codec::entities::{Polyline, Polyline2D}; use codec::entities::{Polyline, Polyline2D};
use codec::tables::BlockRecord; use codec::tables::BlockRecord;
use codec::types::{Transform, Vector3}; use codec::types::Vector3;
use codec::{CadDocument, Entity, EntityType, Handle}; use codec::{CadDocument, EntityType, Handle};
use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::entities::curve::lwpolyline_world_xy; use crate::entities::curve::lwpolyline_world_xy;
@@ -98,68 +98,11 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
{ {
vec![] vec![]
} }
EntityType::Insert(ins) => { EntityType::Insert(ins) => ins
let explodable = document .explode_from_document(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() .into_iter()
.map(normalize_insert_entity) .map(normalize_insert_entity)
.collect(); .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::MLine(ml) => explode_mline(ml), EntityType::MLine(ml) => explode_mline(ml),
EntityType::Dimension(dim) => explode_dimension(dim, document), EntityType::Dimension(dim) => explode_dimension(dim, document),
EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, 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] #[test]
fn locked_explode_places_geometry_at_convention_positions() { fn locked_explode_places_geometry_at_convention_positions() {
// base-point: `explode_entity` shifts the definition's contents by -base before // The document-aware codec explosion uses the definition's base point,
// handing them to the codec's explode, so the pieces land where the render // so the pieces land where the render path puts them.
// path puts them (see the module header for the derivation).
let mut scene = scene_with_bp_test(); let mut scene = scene_with_bp_test();
let insert = placed_instance(&mut scene); let insert = placed_instance(&mut scene);
let pieces = crate::modules::draw::modify::explode::explode_entity( 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] #[test]
fn locked_xclip_boundary_round_trips_through_the_base_point_convention() { 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 // base-point follow-up fix: the XCLIP ring is stored as WCS points plus the