mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 05:16:49 -06:00
DXF/DWG: keep a file's own arrowhead blocks with standard names (_Dot, _DotSmall) (#1694)
* DXF/DWG: keep a file's own arrowhead blocks with standard names Reading dropped every block with a standard arrowhead name (`_Dot`, `_DotSmall`, ...) that no INSERT used, since CADCraft regenerates its own on save. A block only a dimension style names (DIMBLK) was dropped too, so AutoCAD's `_Dot` (a polyline with width) or a user's redefinition came back as CADCraft's disc of 24 SOLIDs. Now only blocks whose content is exactly what CADCraft writes are dropped; any other content is kept and saved as it was, through DXF and DWG. PURGE keeps arrowhead blocks that dimension styles or dimension overrides name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Arrowheads: compare all properties, trim names in PURGE, test every standard arrow is_generated_arrow now requires the whole Common to match, so only blocks exactly as CADCraft writes them are regenerated. PURGE trims arrowhead names from dimension styles and overrides. A new test writes every standard arrowhead to DXF and DWG and checks none reads back as a user block. Review of #1694. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
ffishman
parent
a68d0a458b
commit
47e978b58e
@@ -117,14 +117,35 @@ fn entities(d: &Drawing) -> impl Iterator<Item = &std::sync::Arc<cadcraft_doc::E
|
||||
d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter())).take(5_000_000)
|
||||
}
|
||||
|
||||
/// Upper-case names of blocks reachable from model space, the layouts and dimensions.
|
||||
/// The arrowhead block names a dimension's overrides set.
|
||||
fn override_arrows(dm: &cadcraft_doc::Dimension) -> impl Iterator<Item = String> + '_ {
|
||||
dm.overrides
|
||||
.iter()
|
||||
.filter(|(k, _)| cadcraft_doc::DimStyle::field_name(k).is_some_and(|f| f.starts_with("arrowBlock") || f == "leaderArrowBlock"))
|
||||
.filter_map(|(_, v)| v.as_str().map(|n| n.trim().to_string()))
|
||||
.filter(|n| !n.is_empty())
|
||||
}
|
||||
|
||||
/// Upper-case names of blocks reachable from model space, the layouts and dimensions, and the
|
||||
/// arrowhead blocks dimension styles and dimension overrides name.
|
||||
fn used_blocks(d: &Drawing) -> HashSet<String> {
|
||||
let mut used = HashSet::new();
|
||||
let mut frontier: Vec<String> = Vec::new();
|
||||
for st in &d.dim_styles {
|
||||
frontier.extend(
|
||||
[&st.arrow_block, &st.arrow_block1, &st.arrow_block2, &st.leader_arrow_block]
|
||||
.into_iter()
|
||||
.map(|n| n.trim().to_string())
|
||||
.filter(|n| !n.is_empty()),
|
||||
);
|
||||
}
|
||||
for e in d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())) {
|
||||
match &e.kind {
|
||||
EntityKind::Insert(i) => frontier.push(i.block.clone()),
|
||||
EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
|
||||
EntityKind::Dimension(dm) => {
|
||||
frontier.extend(dm.block.clone());
|
||||
frontier.extend(override_arrows(dm));
|
||||
}
|
||||
EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
|
||||
_ => {}
|
||||
}
|
||||
@@ -139,7 +160,10 @@ fn used_blocks(d: &Drawing) -> HashSet<String> {
|
||||
for e in b.entities.iter() {
|
||||
match &e.kind {
|
||||
EntityKind::Insert(i) => frontier.push(i.block.clone()),
|
||||
EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
|
||||
EntityKind::Dimension(dm) => {
|
||||
frontier.extend(dm.block.clone());
|
||||
frontier.extend(override_arrows(dm));
|
||||
}
|
||||
EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//! PURGE keeps arrowhead blocks a dimension style or a dimension's overrides name (a file's own
|
||||
//! `_Dot`, a user's arrow), and still removes blocks nothing references.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Block, Circle, DimStyle, Entity, EntityKind, Handle};
|
||||
use cadcraft_engine::geom::Vec3;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn purge_keeps_arrowhead_blocks_dimension_styles_name() {
|
||||
let mut s = Session::new();
|
||||
let d = s.doc_mut().unwrap();
|
||||
for (i, name) in ["_Dot", "MyArrow", "Unused"].into_iter().enumerate() {
|
||||
let mut b = Block::new(name);
|
||||
b.entities.push(Entity::new(Handle(0x9000 + i as u64), EntityKind::Circle(Circle { center: Vec3::ZERO, radius: 0.5 })));
|
||||
d.blocks.insert(name.into(), Arc::new(b));
|
||||
}
|
||||
d.dim_styles.push(DimStyle { name: "Dots".into(), arrow_block1: " _DOT ".into(), leader_arrow_block: "MyArrow".into(), ..DimStyle::default() });
|
||||
let r = s.execute("purge", &json!({ "type": "blocks" })).unwrap();
|
||||
assert_eq!(r["purged"]["blocks"], json!(["Unused"]), "{r}");
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.block("_Dot").is_some() && d.block("MyArrow").is_some());
|
||||
}
|
||||
@@ -1816,8 +1816,13 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
|
||||
continue;
|
||||
}
|
||||
// Generated geometry we rebuild on save: table blocks, and standard arrowhead blocks
|
||||
// nothing inserts.
|
||||
if rx.table_blocks.contains(&up) || (crate::dxf_ext::is_standard_arrow(&name) && !rx.inserted.contains(&up)) {
|
||||
// nothing inserts whose content is the one CADCraft writes. An arrowhead block with other
|
||||
// content (AutoCAD's `_Dot`, a user's redefinition) is kept, so dimensions keep it.
|
||||
let generated_arrow = || {
|
||||
let own: Vec<&Entity> = ents.iter().map(|(_, _, e)| e).collect();
|
||||
crate::dxf_ext::is_standard_arrow(&name) && !rx.inserted.contains(&up) && crate::dxf_write::is_generated_arrow(&name, &own)
|
||||
};
|
||||
if rx.table_blocks.contains(&up) || generated_arrow() {
|
||||
continue;
|
||||
}
|
||||
let mut b = Block::new(&name);
|
||||
|
||||
@@ -1514,6 +1514,16 @@ fn arrow_block_entities(kind: cadcraft_render::Arrowhead) -> Vec<Entity> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether the entities of the block `name` are the arrowhead block CADCraft writes for that
|
||||
/// standard arrow name ([`arrow_block_entities`]): the same lines and solids with the same
|
||||
/// properties (layer 0, ByBlock). Any other content (AutoCAD's own `_Dot`, a user's
|
||||
/// redefinition, an older CADCraft's geometry) is the file's.
|
||||
pub(crate) fn is_generated_arrow(name: &str, entities: &[&Entity]) -> bool {
|
||||
let Some((_, kind)) = dxf_ext::arrow_block_name(name) else { return false };
|
||||
let ours = arrow_block_entities(kind);
|
||||
ours.len() == entities.len() && ours.iter().zip(entities).all(|(a, b)| a.kind == b.kind && a.common == b.common)
|
||||
}
|
||||
|
||||
/// Whether a DIMASSOC point reference to `k` names a point of it without naming a segment:
|
||||
/// true for objects made of one curve. Polylines are referenced by segment ([`polyline_vertex_ref`]);
|
||||
/// 3D polylines and multilines can't be yet, and references to them are left out, as AutoCAD
|
||||
|
||||
@@ -876,6 +876,62 @@ fn user_block_arrowheads_are_referenced_not_regenerated() {
|
||||
assert_eq!(back.blocks.len(), d.blocks.len());
|
||||
}
|
||||
|
||||
/// Every standard arrowhead CADCraft generates reads back as its own geometry, through DXF and
|
||||
/// DWG, so reopening a drawing CADCraft wrote never turns them into user blocks.
|
||||
#[test]
|
||||
fn generated_arrowhead_blocks_are_not_kept_as_user_blocks() {
|
||||
let mut d = Drawing::new_metric();
|
||||
for (i, (_, name)) in crate::dxf_ext::ARROWS.iter().enumerate() {
|
||||
d.dim_styles.push(DimStyle { name: format!("S{i}"), arrow_block: (*name).into(), ..DimStyle::default() });
|
||||
}
|
||||
let dxf = write_dxf(&d);
|
||||
for (_, name) in crate::dxf_ext::ARROWS.iter().filter(|(k, _)| *k != cadcraft_render::Arrowhead::ClosedFilled) {
|
||||
assert!(dxf.contains(&format!("\r\n{name}\r\n")), "{name} is written");
|
||||
}
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
for (back, what) in [(read_dxf(dxf.as_bytes()).unwrap(), "DXF"), (from_dwg, "DWG")] {
|
||||
assert!(back.blocks.keys().all(|k| !k.starts_with('_')), "{what}: {:?}", back.blocks.keys().collect::<Vec<_>>());
|
||||
assert!(roundtrip(&back).blocks.keys().all(|k| !k.starts_with('_')), "{what}: second save");
|
||||
}
|
||||
}
|
||||
|
||||
/// A file's own arrowhead block with a standard name (AutoCAD's `_Dot` is a closed polyline
|
||||
/// with width, not CADCraft's disc of solids) is kept when only a dimension style names it, and
|
||||
/// saved as it was, through DXF and DWG, instead of being replaced by CADCraft's geometry.
|
||||
#[test]
|
||||
fn file_arrowhead_blocks_with_standard_names_are_kept() {
|
||||
let mut d = Drawing::new_metric();
|
||||
let donut = LwPolyline {
|
||||
vertices: vec![
|
||||
PolyVertex { p: Vec2::new(-0.25, 0.0), bulge: 1.0, start_width: 0.5, end_width: 0.5 },
|
||||
PolyVertex { p: Vec2::new(0.25, 0.0), bulge: 1.0, start_width: 0.5, end_width: 0.5 },
|
||||
],
|
||||
closed: true,
|
||||
const_width: 0.0,
|
||||
elevation: 0.0,
|
||||
plinegen: false,
|
||||
};
|
||||
for (name, kind) in [("_Dot", EntityKind::LwPolyline(donut)), ("_DotSmall", EntityKind::Circle(Circle { center: Vec3::ZERO, radius: 0.0625 }))] {
|
||||
let mut b = Block::new(name);
|
||||
b.entities.push(Entity::new(Handle(0), kind));
|
||||
d.blocks.insert(name.into(), std::sync::Arc::new(b));
|
||||
}
|
||||
d.dim_styles.push(DimStyle { name: "Dots".into(), arrow_block1: "_Dot".into(), arrow_block2: "_DotSmall".into(), ..DimStyle::default() });
|
||||
let kinds = |d: &Drawing, name: &str| d.block(name).map(|b| b.entities.iter().map(|e| e.kind.clone()).collect::<Vec<_>>()).unwrap_or_default();
|
||||
let dxf = write_dxf(&d);
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
for (back, what) in [(read_dxf(dxf.as_bytes()).unwrap(), "DXF"), (from_dwg, "DWG")] {
|
||||
for name in ["_Dot", "_DotSmall"] {
|
||||
assert_eq!(kinds(&back, name), kinds(&d, name), "{what}: {name}");
|
||||
}
|
||||
let again = write_dxf(&back);
|
||||
assert!(!again.contains("\nSOLID\r"), "{what}: no arrowhead geometry of CADCraft's is written");
|
||||
assert_eq!(kinds(&roundtrip(&back), "_Dot"), kinds(&d, "_Dot"), "{what}: a second save keeps it too");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_overrides_roundtrip_as_dstyle_xdata() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (DWG read leaves out orphaned DGN line style components) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a file's own standard-named arrowhead blocks are kept) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
|
||||
items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
|
||||
@@ -138,6 +138,7 @@ our output in AutoCAD black-box.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |
|
||||
| 2026-10-17 | minor | DWG read: DGN line style components no line style definition reaches are left out before the DXF bridge, with a read note; an 83 MB site plan with 888,483 of them (1.2 GB of DXF, over the limit) now opens (#1482) |
|
||||
| 2026-10-12 | minor | PDF output: object transparency plotted as PDF alpha (`/ExtGState` `/ca` `/CA`) when the page setup's or the plot's "Plot transparency" is on; PNG plots blend it, SVG uses `opacity`; plots without it are unchanged byte for byte |
|
||||
| 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space |
|
||||
|
||||
Reference in New Issue
Block a user