From 5f80b3bcfbf3ed964d4f374b8e10b9e4c25bdd28 Mon Sep 17 00:00:00 2001 From: Li Yanxu Date: Fri, 10 Jul 2026 15:24:28 +0800 Subject: [PATCH 1/2] fix: add AEC_MODI and AEC_SYMB_SECTION as ProxyEntity in readUnlistedType AEC custom objects (TDbSymbModi, TDbSymbSection) from BIM applications like Revit/Tianzheng are stored in DWG with proxy graphics for fallback display in AutoCAD. ACadSharp previously read these as UnknownEntity, causing their proxy graphics to be lost. Route them through readProxyEntity() so the proxy geometry data is preserved in ProxyGeometries for downstream exporters. --- src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs index d2a91569e..e90202d2a 100644 --- a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs +++ b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs @@ -5577,6 +5577,12 @@ private CadTemplate readUnlistedType(short classNumber) case DxfFileToken.ObjectAecCleanupGroupDef: template = this.readAecCleanupGroup(); break; + case "AEC_MODI": + template = this.readProxyEntity(); + break; + case "AEC_SYMB_SECTION": + template = this.readProxyEntity(); + break; case "ACDBDICTIONARYWDFLT": template = this.readDictionaryWithDefault(); break; From 0289d75b25028367d93b06152e5e7bf7187f0e74 Mon Sep 17 00:00:00 2001 From: Li Yanxu Date: Sun, 30 Aug 2026 00:13:53 +0800 Subject: [PATCH 2/2] feat: read unlisted AEC objects as proxy entities/objects Replace the hardcoded AEC_MODI and AEC_SYMB_SECTION cases with a generic fallback that routes every unlisted AEC class through the proxy readers, based on the class naming: dxf name AEC* (covers AEC_/AECS_/AECB_), application name Aec* or cpp class name Aec*/TDb* (covers the Tianzheng/Revit TDb family). - objects stored in proxy format (e.g. saved without their application) are read as ProxyEntity or ProxyObject keeping the proxy graphics and the class definition, see #1148 - objects stored in their native format fall back to the unknown object readers, the proxy attempt runs on a copy of the readers so the object stream is not corrupted on failure - extend the DxfClass notification suppression to ProxyObject Implements the review comment of PR #1148 requesting a generic solution for the AEC objects that are not implemented yet instead of hardcoding AEC_MODI/AEC_SYMB_SECTION. --- .../IO/AecProxyFallbackTests.cs | 54 ++++++++++ .../DWG/DwgStreamReaders/DwgObjectReader.cs | 99 +++++++++++++++++-- src/ACadSharp/IO/Templates/CadTemplate.cs | 1 + 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 src/ACadSharp.Tests/IO/AecProxyFallbackTests.cs diff --git a/src/ACadSharp.Tests/IO/AecProxyFallbackTests.cs b/src/ACadSharp.Tests/IO/AecProxyFallbackTests.cs new file mode 100644 index 000000000..ceedff0f7 --- /dev/null +++ b/src/ACadSharp.Tests/IO/AecProxyFallbackTests.cs @@ -0,0 +1,54 @@ +using ACadSharp.Classes; +using ACadSharp.IO.DWG; +using Xunit; + +namespace ACadSharp.Tests.IO +{ + public class AecProxyFallbackTests + { + [Theory] + [InlineData("AEC_MODI", "TDbSymbModi", true)] + [InlineData("AEC_SYMB_SECTION", "TDbSymbSection", true)] + [InlineData("AEC_DBCONFIG", "TDbConfig", true)] + [InlineData("AEC_DOOR", "AecDbDoor", true)] + [InlineData("AECS_MEMBER", "AecsDbMember", true)] + [InlineData("AECB_PIPE", "AecbDbPipe", true)] + [InlineData("AEC_INTERFACESRECO", "AecInterfaceRecord", true)] + [InlineData("SOME_CUSTOM_ENTITY", "AecDbCustom", true)] + [InlineData("SOME_TIANZHENG_ENTITY", "TDbCustom", true)] + [InlineData("aec_wall", "AecDbWall", true)] + [InlineData("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", false)] + [InlineData("ACDBDETAILVIEWSTYLE", "AcDbDetailViewStyle", false)] + [InlineData("CELLSTYLEMAP", "AcDbCellStyleMap", false)] + [InlineData("LINE", "AcDbLine", false)] + public void IsAecClass_DetectsAecFamilies(string dxfName, string cppClassName, bool expected) + { + DxfClass dxfClass = new DxfClass + { + DxfName = dxfName, + CppClassName = cppClassName, + }; + + Assert.Equal(expected, DwgObjectReader.IsAecClass(dxfClass)); + } + + [Fact] + public void IsAecClass_DetectsAecApplicationName() + { + DxfClass dxfClass = new DxfClass + { + DxfName = "SOME_CUSTOM_ENTITY", + CppClassName = "SomeCustomObject", + ApplicationName = "AEC_KERNAL|missing interpreter", + }; + + Assert.True(DwgObjectReader.IsAecClass(dxfClass)); + } + + [Fact] + public void IsAecClass_NullClass_ReturnsFalse() + { + Assert.False(DwgObjectReader.IsAecClass(null)); + } + } +} diff --git a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs index e90202d2a..de62ea2e0 100644 --- a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs +++ b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs @@ -5577,12 +5577,6 @@ private CadTemplate readUnlistedType(short classNumber) case DxfFileToken.ObjectAecCleanupGroupDef: template = this.readAecCleanupGroup(); break; - case "AEC_MODI": - template = this.readProxyEntity(); - break; - case "AEC_SYMB_SECTION": - template = this.readProxyEntity(); - break; case "ACDBDICTIONARYWDFLT": template = this.readDictionaryWithDefault(); break; @@ -5793,6 +5787,11 @@ private CadTemplate readUnlistedType(short classNumber) break; } + if (template == null && IsAecClass(c)) + { + template = this.readAecProxy(c); + } + if (template == null && c.IsAnEntity) { template = this.readUnknownEntity(c); @@ -5812,6 +5811,94 @@ private CadTemplate readUnlistedType(short classNumber) return template; } + /// + /// Reads an unlisted AEC object as a proxy object keeping the proxy graphics and the class definition. + /// + /// + /// AEC objects saved without their application are stored in proxy format and can be parsed by the + /// proxy readers. If the object is stored in its native format the proxy readers fail, in that case + /// the object is read by the unknown object readers instead. + /// + private CadTemplate readAecProxy(DxfClass c) + { + //Read the object on a copy of the readers, if the object is stored in its native format the + //proxy readers fail and the original readers remain untouched for the unknown object readers + IDwgStreamReader objectReader = this._objectReader; + IDwgStreamReader textReader = this._textReader; + IDwgStreamReader handlesReader = this._handlesReader; + IDwgStreamReader mergedReaders = this._mergedReaders; + + this._objectReader = DwgStreamReaderBase.GetStreamHandler(this._version, HugeMemoryStream.Clone(this._memoryStream), this._reader.Encoding); + this._objectReader.SetPositionInBits(objectReader.PositionInBits()); + this._textReader = DwgStreamReaderBase.GetStreamHandler(this._version, HugeMemoryStream.Clone(this._memoryStream), this._reader.Encoding); + this._textReader.SetPositionInBits(textReader.PositionInBits()); + this._handlesReader = DwgStreamReaderBase.GetStreamHandler(this._version, HugeMemoryStream.Clone(this._memoryStream), this._reader.Encoding); + this._handlesReader.SetPositionInBits(handlesReader.PositionInBits()); + this._mergedReaders = new DwgMergedReader(this._objectReader, this._textReader, this._handlesReader); + + try + { + if (c.IsAnEntity) + { + CadTemplate template = this.readProxyEntity(); + ((ProxyEntity)template.CadObject).DxfClass = c; + this._builder.Notify($"Unlisted AEC entity with DXF name {c.DxfName} has been read as a ProxyEntity", NotificationType.Warning); + return template; + } + else + { + CadTemplate template = this.readProxyObject(); + ((ProxyObject)template.CadObject).DxfClass = c; + this._builder.Notify($"Unlisted AEC object with DXF name {c.DxfName} has been read as a ProxyObject", NotificationType.Warning); + return template; + } + } + catch (Exception) + { + //The object is stored in its native format, restore the original readers so the + //unknown object readers can read its common data + this._objectReader = objectReader; + this._textReader = textReader; + this._handlesReader = handlesReader; + this._mergedReaders = mergedReaders; + + if (!this._builder.Configuration.Failsafe) + throw; + + return null; + } + } + + /// + /// Checks if a dxf class belongs to an AEC (Architecture, Engineering and Construction) application + /// based on the naming of its dxf, application and cpp class names. + /// + /// + /// AEC class names are not limited to the AEC_* dxf name, structural members use AECS_* + /// and the MEP family uses AECB_*. The application name uses the Aec* prefix and the + /// cpp class name is used as a secondary signal for the Autodesk AecDb*/AecsDb* families + /// and for the Tianzheng/Revit TDb* family. + /// + internal static bool IsAecClass(DxfClass c) + { + if (c == null) + return false; + + if (c.DxfName?.StartsWith("AEC", StringComparison.OrdinalIgnoreCase) == true) + return true; + + if (c.ApplicationName?.StartsWith("AEC", StringComparison.OrdinalIgnoreCase) == true) + return true; + + if (c.CppClassName?.StartsWith("Aec", StringComparison.OrdinalIgnoreCase) == true) + return true; + + if (c.CppClassName?.StartsWith("TDb", StringComparison.OrdinalIgnoreCase) == true) + return true; + + return false; + } + #region Evaluation Graph, Enhanced Block etc. private Matrix4 read4x3Matrix() diff --git a/src/ACadSharp/IO/Templates/CadTemplate.cs b/src/ACadSharp/IO/Templates/CadTemplate.cs index 4f6b9209e..8a8c17c1c 100644 --- a/src/ACadSharp/IO/Templates/CadTemplate.cs +++ b/src/ACadSharp/IO/Templates/CadTemplate.cs @@ -49,6 +49,7 @@ public void Build(CadDocumentBuilder builder) switch (this.CadObject) { case ProxyEntity: + case ProxyObject: case UnknownEntity: case UnknownNonGraphicalObject: break;