DXF: proxy objects and entities keep their class through a save (#1812) (#1858)

* dxf: proxy objects and entities keep their class through a save (#1812)

A proxy (ACAD_PROXY_OBJECT, ACAD_PROXY_ENTITY) names its class by number
in group 91: 500 plus the class's place in the CLASSES section (DXF
Reference). Saving rewrites CLASSES (CADCraft's own classes, then the kept
ones), but kept proxies were written with their read number, which then
named another class: an R14 file's visual styles came back as SCALE,
TABLESTYLE or MLEADERSTYLE, and numbers past the last class stayed.

The reader now keeps the file's class order (Foreign::class_order) when
the file has proxies, and the CLASS records its proxies name. The writer
renumbers each kept proxy to its class's place in the written CLASSES and
drops a proxy whose number names no class (with what it owns and its
dictionary entry). The DWG bridge writes through the same path.

Test proxies_keep_their_class_when_classes_are_rewritten (a file with
VISUALSTYLE and WIDGETX first in CLASSES, proxies of both and of a number
past the last class, and a proxy entity; also through a second save).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP

* docs: the proxy class fix is for DXF save (#1812)

From the local CodeRabbit review: DWG save drops proxy entities (acadrust)
and proxy objects through DWG weren't checked, so the progress log says DXF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP

* dxf: a proxy's class number is found outside 102 groups (#1812)

From the PR review: an application's 102 {NAME ... 102 } group before a
proxy's own groups can hold a group 91, which was read as the class number
and overwritten on save. class_group finds the first 91 outside 102 groups
(before xdata), for reading and renumbering alike (test
proxies_keep_their_class_when_classes_are_rewritten, now with a {MYAPP
group holding a 91 that stays as it was).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP

---------

Co-authored-by: Tyrel Haveman <1434399+tyrel@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Tyrel Haveman
2026-10-24 01:55:07 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Tyrel Haveman Brandon Thomas
parent fde62a39ab
commit 73e7486dc9
5 changed files with 238 additions and 7 deletions
+3 -1
View File
@@ -2,7 +2,7 @@
**Stage: pre-alpha** · next: alpha, ~2 points (38% → 40% ready for real work) and ~125–190 Opus 5.5 hours away (the four failing core workflows of the [alpha gate](docs/roadmap.md#alpha-gate))
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (HATCHEDIT boundary add/remove progress) · **Target:** Autodesk AutoCAD 2027
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** trivial (progress log: proxy objects keep their class through a save, #1812; previously minor: HATCHEDIT boundary add/remove progress) · **Target:** Autodesk AutoCAD 2027
CADCraft targets full parity with AutoCAD (2D drafting first, then annotation, layouts and
plotting, DWG, parametrics and 3D), plus what AutoCAD doesn't have: agent control over MCP, a
@@ -119,6 +119,7 @@ Ranked; detail and the full beta checklist in [docs/roadmap.md](docs/roadmap.md)
| Date | Entry |
|---|---|
| 2026-10-21 | DXF save keeps each proxy object's and proxy entity's class: its class number (group 91) is renumbered to the rewritten CLASSES instead of naming another class, so AutoCAD's visual styles in an R14 file no longer come back as SCALE or MLEADERSTYLE (#1812) |
| 2026-10-14 | HATCHEDIT adds boundaries from internal points or closed objects and removes selected loops in the dialog, command line and JSON command; associative links and undo are covered by tests. |
| 2026-10-12 | TRIM: a pick after a closed polyline's last cut keeps only the complementary run instead of an extra circuit of the outline (#726) |
| 2026-10-12 | HATCHEDIT refuses an explicit bad `scale` or `angle` before it edits anything, so a rejected request leaves the drawing, undo and redo as they were (#724) |
@@ -135,6 +136,7 @@ Ranked; detail and the full beta checklist in [docs/roadmap.md](docs/roadmap.md)
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | trivial | Progress log: proxy objects keep their class through a save (#1812) |
| 2026-10-14 | minor | Progress log: HATCHEDIT boundary add/remove |
| 2026-10-12 | trivial | Progress log: closed-polyline TRIM (#726) |
| 2026-10-12 | trivial | Progress log: HATCHEDIT numeric validation (#724) |
+5
View File
@@ -66,6 +66,10 @@ pub struct Foreign {
pub records: BTreeMap<ForeignKey, ForeignExtra>,
/// The roles of the file handles the kept data refers to, for objects CADCraft rewrites.
pub targets: BTreeMap<Handle, ForeignKey>,
/// The upper-case names of the file's classes in its CLASSES order, kept when the file has
/// proxy objects or entities: a proxy's class number (group 91) is 500 plus its class's place
/// here, and saving renumbers it to the written CLASSES (#1812).
pub class_order: Vec<String>,
/// The file's data store section (ACDSDATA, AutoCAD 2013 and later), as read: its groups
/// after the section name. It holds the ACIS geometry of 3D solids, regions, bodies and
/// surfaces, which from 2013 on those entities don't carry themselves: each `ACDSRECORD`
@@ -81,6 +85,7 @@ impl Foreign {
&& self.records.is_empty()
&& self.classes.is_empty()
&& self.targets.is_empty()
&& self.class_order.is_empty()
&& self.data_store.is_empty()
}
+92 -3
View File
@@ -455,6 +455,25 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
EntityKind::Unknown(u) => Some(up(&u.dxf_type)),
_ => None,
}));
// A proxy's class is the one its group 91 numbers in the file's CLASSES: keep it too, and the
// file's class order, by which saving renumbers the proxy.
let class_order: Vec<String> =
classes.iter().filter(|(k, _, _)| k == "CLASS").take(MAX_OBJECTS).map(|(_, _, t)| up(&val(t, 1).unwrap_or_default())).collect();
let mut proxies = false;
let proxy_tags = f.objects.iter().filter(|o| is_proxy(&o.kind)).map(|o| &o.tags);
let proxy_tags = proxy_tags.chain(all_entities(d).filter_map(|e| match &e.kind {
EntityKind::Unknown(u) if is_proxy(&u.dxf_type) => Some(&u.tags),
_ => None,
}));
for tags in proxy_tags {
proxies = true;
if let Some(name) = class_index(class_number(tags.iter().map(|r| (r.code, r.value.as_str())))).and_then(|i| class_order.get(i)) {
types.insert(name.clone());
}
}
if proxies {
f.class_order = class_order;
}
for (kind, _, t) in &classes {
if kind == "CLASS" && val(t, 1).is_some_and(|n| types.contains(&up(&n))) && f.classes.len() < MAX_OBJECTS {
f.classes.push(t.iter().map(raw).collect());
@@ -492,6 +511,45 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
}
}
/// Proxy records (DXF Reference, ACAD_PROXY_OBJECT and ACAD_PROXY_ENTITY): another application's
/// objects and entities, whose class is named by number in group 91.
fn is_proxy(kind: &str) -> bool {
matches!(up(kind).as_str(), "ACAD_PROXY_OBJECT" | "ACAD_PROXY_ENTITY")
}
/// Where a proxy's class number is: its first group 91 outside the `102 {NAME` … `102 }` groups
/// (an application's can hold a 91 of its own) and before any xdata.
fn class_group<S: AsRef<str>>(groups: impl IntoIterator<Item = (i32, S)>) -> Option<usize> {
let mut in_group = false;
for (i, (code, value)) in groups.into_iter().enumerate() {
match code {
1000.. => return None,
102 => in_group = value.as_ref().trim().starts_with('{'),
91 if !in_group => return Some(i),
_ => {}
}
}
None
}
/// A proxy's class number (see [`class_group`]).
fn class_number<S: AsRef<str>>(groups: impl IntoIterator<Item = (i32, S)> + Clone) -> Option<i64> {
let i = class_group(groups.clone())?;
groups.into_iter().nth(i).and_then(|(_, v)| v.as_ref().trim().parse().ok())
}
/// The place in CLASSES a class number names (500 is the first class).
fn class_index(number: Option<i64>) -> Option<usize> {
usize::try_from(number?.checked_sub(500)?).ok()
}
/// Set a proxy's class number (see [`class_group`]).
fn set_class_number(tags: &mut [Tag], n: i64) {
if let Some(t) = class_group(tags.iter().map(|t| (t.code, t.str()))).and_then(|i| tags.get_mut(i)) {
*t = Tag::i(91, n);
}
}
/// Most groups of a data store section kept (a damaged file's can be endless).
const MAX_DATA_STORE_GROUPS: usize = 1 << 26;
@@ -775,8 +833,28 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
roles.entry(k).or_insert(h);
}
let mut written: HashSet<String> = recs.iter().filter_map(|r| head(body(r)).handle).collect();
// The written CLASSES as numbered (500 for the first): the writer's own, then the kept ones it
// doesn't write (appended below, in this order). A kept proxy's group 91 is renumbered to its
// class's place here; a proxy whose class isn't written is dropped (#1812).
let class_names: HashSet<String> = recs.iter().filter(|r| r.kind == "CLASS").filter_map(|r| val(body(r), 1)).map(|n| up(&n)).collect();
let appended = f.classes.iter().filter_map(|c| c.iter().find(|t| t.code == 1)).map(|n| up(&n.value)).filter(|n| !class_names.contains(n));
let mut numbers: HashMap<String, i64> = HashMap::new();
for (i, n) in recs.iter().filter(|r| r.kind == "CLASS").filter_map(|r| val(body(r), 1)).map(|n| up(&n)).chain(appended).enumerate() {
numbers.entry(n).or_insert(500_i64.saturating_add(i64::try_from(i).unwrap_or(i64::MAX)));
}
let renumber = |old: Option<i64>| class_index(old).and_then(|i| f.class_order.get(i)).and_then(|n| numbers.get(n).copied());
// Entities of types CADCraft doesn't model, placed among the written ones.
let planned = plan_unknown(d, &recs, tags, &roles);
let mut planned = plan_unknown(d, &recs, tags, &roles);
for list in planned.values_mut() {
list.retain_mut(|(_, t)| {
if !t.first().is_some_and(|z| is_proxy(&z.str())) {
return true;
}
let Some(n) = renumber(class_number(t.iter().map(|x| (x.code, x.str())))) else { return false };
set_class_number(t, n);
true
});
}
for list in planned.values() {
for (_, t) in list {
written.extend(head(t.get(1..).unwrap_or(&[])).handle);
@@ -813,6 +891,9 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
.filter_map(|o| Some((o.handle.hex(), head(&o.tags.iter().take_while(|t| t.code < 1000).map(tag).collect::<Vec<_>>()).owner?)))
.collect();
let mut alive_memo: HashMap<String, bool> = HashMap::new();
for o in f.objects.iter().filter(|o| is_proxy(&o.kind) && renumber(class_number(o.tags.iter().map(|r| (r.code, r.value.as_str())))).is_none()) {
alive_memo.insert(o.handle.hex(), false);
}
for o in &f.objects {
let mut path: Vec<String> = Vec::new();
let mut cur = o.handle.hex();
@@ -850,7 +931,6 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
}
let entities: HashSet<String> = all_entities(d).map(|e| e.handle.hex()).filter(|h| written.contains(h)).collect();
let map = Map { f, alive, raw: objs.keys().cloned().collect(), roles, entities };
let class_names: HashSet<String> = recs.iter().filter(|r| r.kind == "CLASS").filter_map(|r| val(body(r), 1)).map(|n| up(&n)).collect();
let mut out: Vec<Tag> = Vec::with_capacity(tags.len() + f.objects.iter().map(|o| o.tags.len() + 1).sum::<usize>());
let mut block = String::new();
for r in &recs {
@@ -866,7 +946,16 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
}
if r.kind == "ENDSEC" && r.sec == "OBJECTS" {
for o in f.objects.iter().filter(|o| map.alive.contains_key(&o.handle.hex())) {
map.object(o, &mut out);
let number = is_proxy(&o.kind).then(|| renumber(class_number(o.tags.iter().map(|r| (r.code, r.value.as_str()))))).flatten();
match number {
Some(n) => {
let mut tags: Vec<Tag> = o.tags.iter().map(tag).collect();
set_class_number(&mut tags, n);
let o = ForeignObject { tags: tags.iter().map(raw).collect(), ..o.clone() };
map.object(&o, &mut out);
}
None => map.object(o, &mut out),
}
}
}
for (sec, t) in planned.get(&r.start).into_iter().flatten() {
+134
View File
@@ -2607,3 +2607,137 @@ fn drawing_without_insunits_stays_unitless() {
assert_eq!(insunits(&write_dxf(&Drawing::new_metric())), Some("4".into()));
assert_eq!(insunits(&write_dxf(&Drawing::new_imperial())), Some("1".into()));
}
/// DXF text as (code, value) pairs.
fn dxf_pairs(text: &str) -> Vec<(String, String)> {
let lines: Vec<&str> = text.lines().collect();
lines.chunks(2).filter(|c| c.len() == 2).map(|c| (c[0].trim().to_string(), c[1].trim().to_string())).collect()
}
/// The records of type `kind` in DXF pairs, each as its groups after the type.
fn dxf_records<'a>(pairs: &'a [(String, String)], kind: &str) -> Vec<&'a [(String, String)]> {
let mut out = Vec::new();
for (i, p) in pairs.iter().enumerate() {
if p.0 == "0" && p.1 == kind {
let rest = &pairs[i + 1..];
let end = rest.iter().position(|q| q.0 == "0").unwrap_or(rest.len());
out.push(&rest[..end]);
}
}
out
}
fn group<'a>(rec: &'a [(String, String)], code: &str) -> Option<&'a str> {
rec.iter().find(|p| p.0 == code).map(|p| p.1.as_str())
}
/// #1812: a file whose CLASSES start with VISUALSTYLE (500) and WIDGETX (501), with proxy objects
/// of both classes and of a number past its last class, and a proxy entity of WIDGETX.
fn proxy_file() -> Vec<u8> {
let text = String::from_utf8(write(&Drawing::new_metric(), "p.dxf").unwrap()).unwrap();
let mut pairs = dxf_pairs(&text);
let p = |c: &str, v: &str| (c.to_string(), v.to_string());
let class = |name: &str, cpp: &str| {
vec![p("0", "CLASS"), p("1", name), p("2", cpp), p("3", "ObjectDBX Classes"), p("90", "4095"), p("280", "1"), p("281", "0")]
};
let classes = pairs.iter().position(|q| q.0 == "2" && q.1 == "CLASSES").unwrap() + 1;
let mut injected = class("VISUALSTYLE", "AcDbVisualStyle");
injected.extend(class("WIDGETX", "AcDbWidgetX"));
pairs.splice(classes..classes, injected);
let objects = pairs.iter().position(|q| q.0 == "2" && q.1 == "OBJECTS").unwrap();
let nod = objects + 1;
let nod_handle = pairs[nod + 1].1.clone();
let after = nod + pairs[nod..].iter().position(|q| q.0 == "281").unwrap() + 1;
pairs.splice(after..after, [p("3", "ACAD_VISUALSTYLE"), p("350", "AAA0")]);
let proxy = |h: &str, class: &str, data: &str| {
vec![
p("0", "ACAD_PROXY_OBJECT"),
p("5", h),
// An application's 102 group before the proxy's own groups holds a 91 of its own.
p("102", "{MYAPP"),
p("91", "7"),
p("102", "}"),
p("330", "AAA0"),
p("100", "AcDbProxyObject"),
p("90", "499"),
p("91", class),
p("95", "0"),
p("70", "0"),
p("92", "8"),
p("310", data),
p("94", "0"),
]
};
let mut objs = vec![
p("0", "DICTIONARY"),
p("5", "AAA0"),
p("330", &nod_handle),
p("100", "AcDbDictionary"),
p("281", "1"),
p("3", "Realistic"),
p("350", "AAA1"),
p("3", "Widget"),
p("350", "AAA2"),
p("3", "Stray"),
p("350", "AAA3"),
];
objs.extend(proxy("AAA1", "500", "AA"));
objs.extend(proxy("AAA2", "501", "BB"));
objs.extend(proxy("AAA3", "537", "CC"));
let end = objects + pairs[objects..].iter().position(|q| q.0 == "0" && q.1 == "ENDSEC").unwrap();
pairs.splice(end..end, objs);
let ms = {
let recs = dxf_records(&pairs, "BLOCK_RECORD");
group(recs.iter().find(|r| group(r, "2") == Some("*Model_Space")).unwrap(), "5").unwrap().to_string()
};
let ents = pairs.iter().position(|q| q.0 == "2" && q.1 == "ENTITIES").unwrap() + 1;
pairs.splice(
ents..ents,
[
p("0", "ACAD_PROXY_ENTITY"),
p("5", "AAA4"),
p("330", &ms),
p("100", "AcDbEntity"),
p("8", "0"),
p("100", "AcDbProxyEntity"),
p("90", "498"),
p("91", "501"),
p("92", "0"),
p("93", "0"),
p("94", "0"),
p("95", "0"),
p("70", "0"),
],
);
pairs.iter().map(|(c, v)| format!("{c}\n{v}\n")).collect::<String>().into_bytes()
}
#[test]
fn proxies_keep_their_class_when_classes_are_rewritten() {
let d = read(&proxy_file(), "p.dxf").unwrap();
let text = String::from_utf8(write(&d, "out.dxf").unwrap()).unwrap();
let pairs = dxf_pairs(&text);
let classes: Vec<String> = dxf_records(&pairs, "CLASS").iter().map(|r| group(r, "1").unwrap().to_string()).collect();
let class_of = |n: &str| classes.get(n.parse::<usize>().unwrap() - 500).cloned();
// The proxy's own 91 (after its subclass marker); the application group's stays as it was.
let own_91 = |r: &[(String, String)]| r.iter().skip_while(|q| q.1 != "AcDbProxyObject").find(|q| q.0 == "91").map(|q| q.1.clone()).unwrap();
let objects: Vec<(String, Option<String>)> =
dxf_records(&pairs, "ACAD_PROXY_OBJECT").iter().map(|r| (group(r, "310").unwrap().to_string(), class_of(&own_91(r)))).collect();
assert!(dxf_records(&pairs, "ACAD_PROXY_OBJECT").iter().all(|r| group(r, "91") == Some("7")), "the application's 91 changed");
assert_eq!(
objects,
[("AA".to_string(), Some("VISUALSTYLE".to_string())), ("BB".to_string(), Some("WIDGETX".to_string()))],
"classes {classes:?}"
);
let entities: Vec<Option<String>> = dxf_records(&pairs, "ACAD_PROXY_ENTITY").iter().map(|r| class_of(group(r, "91").unwrap())).collect();
assert_eq!(entities, [Some("WIDGETX".to_string())]);
// The proxy whose number named no class is gone, with its dictionary entry.
assert!(!text.contains("\nStray\n"));
// And it stays so through another save.
let again = String::from_utf8(write(&read(text.as_bytes(), "out.dxf").unwrap(), "again.dxf").unwrap()).unwrap();
let pairs = dxf_pairs(&again);
let classes: Vec<String> = dxf_records(&pairs, "CLASS").iter().map(|r| group(r, "1").unwrap().to_string()).collect();
let names: Vec<String> =
dxf_records(&pairs, "ACAD_PROXY_OBJECT").iter().map(|r| classes[own_91(r).parse::<usize>().unwrap() - 500].clone()).collect();
assert_eq!(names, ["VISUALSTYLE", "WIDGETX"]);
}
+4 -3
View File
@@ -1,6 +1,6 @@
# File-format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (a diameter dimension's `*D` text keeps the sign as `%%c`) · **Target:** Autodesk AutoCAD 2027
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (proxy objects and entities keep their class through a save, #1812; previously minor: DWG read leaves out orphaned DGN line style components) · **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
@@ -89,13 +89,13 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG
| WIPEOUT | yes | yes (#147, fixes #79) | |
| MLINE (+ MLINESTYLE), HELIX, TOLERANCE | yes (#510, #420) | yes | |
| REGION | other programs' REGIONs (ACIS data) kept as Unknown | our regions are written as closed polylines tagged with CADCraft xdata (#510; ACIS has no public spec) | |
| 3DSOLID, BODY, SURFACE, MESH, SHAPE, UNDERLAY, OLE2FRAME, LIGHT, CAMERA, SECTION, POINTCLOUD, proxy entities | kept as Unknown (raw tags) | written back as read, with their CLASS records (#528); **dropped by DWG save** (acadrust) | only their layer follows edits |
| 3DSOLID, BODY, SURFACE, MESH, SHAPE, UNDERLAY, OLE2FRAME, LIGHT, CAMERA, SECTION, POINTCLOUD, proxy entities | kept as Unknown (raw tags) | written back as read, with their CLASS records (#528); a proxy entity's class number (group 91) is renumbered to its class's place in the written CLASSES (#1812); **dropped by DWG save** (acadrust) | only their layer follows edits |
| Header variables | all read | 36 written | AutoCAD writes about 280 |
| Tables: LAYER (+ transparency, description), LTYPE, STYLE, DIMSTYLE, BLOCK_RECORD | yes | yes | |
| Tables: VPORT, VIEW, UCS, APPID | yes | yes: saved viewport configurations (#504), named views, UCSs | UCS elevation lost |
| Objects: LAYOUT, DICTIONARY, XRECORD, TABLESTYLE, DIMASSOC | yes | yes | |
| Objects: GROUP, MLINESTYLE, MLEADERSTYLE, IMAGEDEF, SCALE | yes | yes (groups #417, scale list #468) | |
| Objects: VISUALSTYLE, PLOTSETTINGS, materials, field objects and other unmodelled objects | kept raw (#528) | written back as read | not editable |
| Objects: VISUALSTYLE, PLOTSETTINGS, materials, field objects and other unmodelled objects | kept raw (#528) | written back as read; a proxy object (ACAD_PROXY_OBJECT) keeps its class: its group 91 is renumbered to the written CLASSES, which keep the class it names, and one whose number names no class is dropped (#1812) | not editable |
| Foreign xdata, extension dictionaries, reactors, unmodelled objects, colour books | kept raw (#528); an entity's xdata is part of the entity, so COPY, MIRROR, ARRAY and paste keep it on the copies (EXPLODE pieces don't) | written back, handles remapped, references to deleted objects dropped | through DWG: xdata on table records and blocks, entity colour books and reactors, custom-class objects lost (acadrust) |
| Objects: dynamic block data (ACAD_EVALUATION_GRAPH, BLOCK…PARAMETER, BLOCK…ACTION, grips) | read into a model (`dynblock`, `blocks.dynamic`): parameters, visibility states, value sets, stretch and lookup actions; references: their dynamic block, values and custom properties (`blocks.reference`, Properties ▸ Custom, read-only); kept raw (#528) | written back as read; a changed visibility state is written as AutoCAD writes it (a new anonymous block with its `AcDbBlockRepBTag`, the objects' `AcDbBlockRepETag`, the reference's representation data) | visibility states (and lookups that set only them) can be changed (`blocks.reference.set`, Properties ▸ Custom); distances, positions and the actions they drive not yet; DWG open and save keep it too (checked with an AutoCAD dynamic block) |
| Handles and owners | entity handles kept; others regenerated | consistent | stable across CADCraft re-saves (#495); other programs' table and object handles renumbered once |
@@ -142,6 +142,7 @@ our output in AutoCAD black-box.
| 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) |
| 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-20 | minor | DIMENSION: the `*D` block's MTEXT, and the MTEXT EXPLODE leaves, hold the diameter sign as `%%c` instead of the decoded U+2300, which no SHX font has, so other programs plotted diameter dimensions in an SHX style without the sign (#1645) |
| 2026-10-21 | minor | Proxy objects and entities keep their class through a save: group 91 renumbered to the written CLASSES (the class it names is kept), a proxy whose number names no class dropped (#1812) |
| 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 |