Files
envi-code/AcDbX3dSolid-refactor-plan.md
T
company-desktop-computer e5e6fd7d6d docs: TransformBy/ToJson 必须随阶段三改,补 kLoft 丢数据后果
11.2 第 1 项从「是否一起改」改为已定。这两处都是 m_sScr 的读者,容器化
后不改不是导出数据不对,而是功能直接坏:

- TransformBy:5 个类型靠 StringToRegion(m_sScr) 取截面,取不到就
  bChanged=false、不 ReCreate。kExtrude2 留下参数与几何不一致;
  kRevolve/kRevolve2/kSweep 整段不执行
- kLoft:vec 为空使 strAll 为空,:5567 把 m_sScr 覆盖成空串,ReCreate
  里 CreateLoft 因空输入直接 return false,一次移动永久丢截面数据
- ToJson:三个分支全靠 m_sScr 解析 __sScr__,不改则导出几何为空

同时记录 m_sScr 的分布(66 处、24 个函数),说明阶段三的范围是所有
m_sScr 读写者,不是生成路径加少量读取点。
2026-10-22 14:09:44 +08:00

28 KiB
Raw Blame History

AcDbX3dSolid 重构计划

目标:解决 AcDbX3dSolid 用 CString 保存几何数据(m_sScr / m_sszPoint2ds)带来的体积翻倍、精度丢失、临时文件冲突和解析开销问题。

范围:SourceCode/Code2026/CoreLibrary/AcDbX3dSolid.cpp(5596 行)、inc/AcDbX3dSolid.h、CoreCommand/CoreTools.cpp。

原则:单向兼容——新版必须能读老图纸,老版不保证能显示新图的 region 类型几何(见 11.1);写入统一新格式;不改对象模型(AcDbX3dSolid 仍是可平凡复制的数据类)。


1. 现状

AcDbX3dSolid 不是 AcDbEntity,是"参数化几何单元",只存生成参数,按需重建 AcDb3dSolid。

两个 CString 承担了超出设计的职责:

字段 实际内容
m_sszPoint2ds 断面/路径点串、引导线、GSMarker 十进制整数
m_sScr ACIS SAT 全文(region 类,9 种类型)、管廊断面脚本(kEarthWorm / kSHEarthWorm)

region 类的数据流:

AcDbRegion* ──RegionToString()──► %TEMP%\Region1.sat ──读文件──► char* ──ANSI→UTF-16──► CString m_sScr
                                                                            │
                                                              dwgOutFields: writeString (2 字节/字符)

读取反向:CString → UTF-8 → fixACISFormat → 写 %TEMP%\Region2.sat → acisIn。

已知问题:

# 问题 位置
1 SAT 是 ASCII,却按 UTF-16 存 DWG,体积翻倍 :4783 writeString((LPCTSTR)m_sScr)
2 临时文件名写死(Region1.sat / Region2.sat / Region212.sat),多 CAD 实例并发会互相覆盖 CoreCommand/CoreTools.cpp
3 new char[dwFileLength] 缺 +1,无结尾零字节,CString 赋值时越界读;file.Read 返回值未检查 CoreTools::RegionToString
4 dwgInFields 末尾无条件 ReCreate(),开图时每个单元一次临时文件往返 :4753
5 分隔符 |、@、@region@ 无转义、无校验 多处
6 Clone() 内含 ReCreate(),绘制缓存建立时重复重建 :5593 + :295

2. 已否决的方案

方案 否决原因
存构建曲线 + AcDbRegion::createFromCurves 接口只拿得到 AcDbRegion*,可能是复杂布尔结果;AcDbRegion 无反向导出边界 API(dbregion.h 只有 getPerimeter/getArea/getPlane/getAreaProp/booleanOper);explode 得到的是 ACIS 求交后的边,是近似
自写 / 复用 AcDbDwgFiler 做内存序列化 R26 readString(ACHAR**) 已 final(arx/R26.0/inc/dbfiler.h:148),基类实现转发到 readString(AcString&);CMFCDbDwgFiler 该函数直接 return eInvalidInput,且 readAddress/writeAddress/seek 全空实现。R26 下字符串读取必然失败
在 AcDbX3dSolid 内持有 AcDbRegion 对象,循环调 dwgInFields/dwgOutFields ① std::vector<AcDbRegion> 无法实例化:AcDbObject(const AcDbObject&) = delete(dbobject.h:600);② dwgOutFields 不写对象头,但 AcDbEntity::dwgOutFields 仍写 layerId 等 id → 未入库对象写出悬空引用;③ Clone() 必须深拷贝 region,而 region 不能拷贝 → 只能再序列化一遍,绘制路径成本翻倍
隐藏 BlockTableRecord / 扩展字典存 region 实体 AcDbX3dSolid 非数据库对象,Clone() 是内存拷贝,AcDbObjectId 会被共享;改动面扩散到全部持有者
SAT 文本 + zlib 压缩 工程只有 inc/zlib.h 头,CoreLibrary/unzip.cpp 是 inflate 半边,无 deflate 实现,dep_lib 无 zlib 库;收益低于前三项

结论:采用 SAT 的 ASCII 字节流存进 m_pSatBuffer。

不改对象模型、不碰 filer、不加新成员、Clone() 仍是 memcpy。


3. 目标存储格式

3.1 为什么可以复用 m_pSatBuffer

m_pSatBuffer 的读写通道已经打通并持久化:

// dwgOutFields :4822-4829
if (AcDbX3dSolid::kCurrentVersionNumber > 1) {
    pFiler->writeUInt32(m_nSatLen);
    if (m_pSatBuffer != NULL)
        pFiler->writeBytes(m_pSatBuffer, m_nSatLen);
}

dwgInFields 有对称的 readUInt32 + readBytes。序列化层零改动,只需把内容从"SAT 文本的 UTF-16 拷贝"换成"SAT 的 ASCII 字节"。

3.2 多 region 容器

9 种 region 类型里,只有 7 种参与迁移:5 种单 region、1 种双 region(kConePipe)、1 种 N region(kLoft)。kIntersect(28) / kSliceSolid(29) 是遗留类型,全库没有产生路径,不参与迁移(见下)。容器只装 region:

// 16 字节头
struct RegionBlobHeader {
    Adesk::UInt32 magic;      // 'SRGN' = 0x4E475253
    Adesk::UInt16 version;    // 1
    Adesk::UInt16 reserved;   // 0
    Adesk::UInt32 nRegions;   // region 个数
    Adesk::UInt32 totalLen;   // 总字节数(含头),用于跳过
};

// 后接 nRegions 个:
//   Adesk::UInt32 len;
//   char          data[len];   // SAT 的 ASCII 字节,不含结尾 0

对应关系(按当前代码核对):

类型 m_sScr 实际内容 nRegions 迁移
kExtrude2(27) / kSlicePipe(26) / kRevolve(21) / kRevolve2(38) / kSweep(40) 单个 region 的 SAT 1 是
kConePipe(30) 外环SAT + | + 内环SAT 2 是
kLoft(41) N 个 region SAT,@region@ 连接 N 是
kIntersect(28) / kSliceSolid(29) regionSAT + | + 折线文本 — 否,遗留类型,m_sScr 保持原样

几点必须注意:

  • kIntersect / kSliceSolid 不参与迁移:不进容器、不停止写 m_sScr,读写路径保持原样。它们的老格式是 regionSAT|折线文本,第二个分量是 CoreTools::CurveToString 产出的折线(:2130/:2184),不是 region;读端用 StringToPolyline(:4403、:4441)。原文档把它们当成「外 + 内」两个 region 是错的
  • 切分统一取第一个 |,且 sScr2 = m_sScr.Mid(nPos) 保留了分隔符本身,靠 StringToRegion / StringToPolyline 内部找 ACIS 头容错
  • kSweep 的路径存在 m_sszPoint2ds(AcDbCurveToString,:5088/:5103),不在 m_sScr
  • 降级导出按这张表拼回:迁移的 7 种走容器,遗留的 2 种原样保留 m_sScr

magic 是必需的,不是可选项:m_pSatBuffer 是公共字节池,当前已有 4 种语义(kFenceExtrude 点数组、kFrom3JS SubDMesh、kLoftReducer 宽字符串、SAT 导入原始字节),必须自校验。

kIntersect / kSliceSolid 为什么排除在外(2026-10-22 核对):

  • 枚举只在 inc/AcDbX3dSolid.h:53-54 定义,其它模块零引用
  • CreateIntersect / CreateSliceSolid 只有三个出现点:定义(:2050、:2136)、头文件声明、ReCreate 的读分支调用(:4413、:4451)—— 是纯读路径,只为重建图纸里已有的对象
  • Create() 的 12 个 case 里没有它们;SetSolidData() 全库无调用者,内部也没有对应 case
  • 没有 (Primitive)28 / (Primitive)29 这类数字强转

结论:新图永远不会产生这两种对象,阶段三不必为它们设计容器格式。

3.3 编解码

// AcDbX3dSolid.cpp 匿名 namespace
bool PackRegions(const AcArray<AcDbRegion*>& regs, void*& pBuf, Adesk::UInt32& nLen);
bool UnpackRegions(const void* pBuf, Adesk::UInt32 nLen, AcArray<AcDbRegion*>& regs);

单 region 的 SAT 字节不需要新函数,直接复用已有的两个重载:

// 输出 SAT 的 ASCII 字节,返回长度,pszTxt 由调用方 delete[]
Adesk::UInt64 CoreTools::RegionToString(const AcDbRegion* pReg, char*& pszTxt);  // CoreTools.cpp:3114
// 从字节重建 region
AcDbRegion* CoreTools::StringToRegion(Adesk::UInt64 nLen, char* pszReg);         // CoreTools.cpp:3581

RegionToString(char*&) 内部已经用 MakeTempSatPath() + DeleteFile,StringToRegion 内部已经过 fixACISFormat,阶段一、二落地后这两条链路都是现成的。PackRegions 负责把拿到的字节拷进容器并 delete[]。


4. 改动清单

阶段一、二已合并;阶段三暂缓,取舍与前提见第 11 节。以下保留完整方案,供重启时参考。

阶段一:临时文件唯一化(纯 bug 修复)

位置:CoreCommand/CoreTools.cpp

函数 现状 改法
RegionToString Region1.sat 唯一名 + 用完 DeleteFile
StringToRegion(const CString&) :3477 Region2.sat 同上
StringToRegion(nLen, char*) :3525 Region212.sat 同上

唯一名用 GetTempFileName 或 %TEMP%\{GUID}.sat。

同时修:

char* pszTxt = new char[dwFileLength + 1];            // 补 +1
::memset(pszTxt, 0, dwFileLength + 1);
UINT nRead = file.Read(pszTxt, (UINT)dwFileLength);   // 检查短读
if (nRead != (UINT)dwFileLength) { delete[] pszTxt; return false; }

风险:低。收益:消除多实例并发时的几何错乱与越界读。


阶段二:懒重建

位置:CoreLibrary/AcDbX3dSolid.cpp / inc/AcDbX3dSolid.h

位置 现在 改成
dwgInFields 末尾 :4753 ReCreate(); m_bNeedCreate = true;
拷贝构造末尾 :295 ReCreate(); m_bNeedCreate = true;
inc/AcDbX3dSolid.h:255 AcDb3dSolid* m_pSolid; 加 mutable
GetDraw3dSolid() const :3852 — 入口调 EnsureCreated()
mutable bool m_bNeedCreate;    // 不序列化,默认 true
void EnsureCreated() { if (m_bNeedCreate) { m_bNeedCreate = false; ReCreate(); } }

必须一起改的入口(凡是读 m_pSolid 或依赖已建几何的):

  • GetInterface()(:3542 附近)
  • TransformBy(:5496 起)
  • GetDrawSubDMesh()(:3893)
  • ToJson
  • GetDraw2d()(:3933)

风险:中。漏一个入口会出现"几何没建出来"。收益:开图不再逐单元走临时文件;Clone() 里的 ReCreate() 也一并省掉。


阶段三:region 数据二进制化

4.3.1 写入点(RegionToString → PackRegions)

行号为当前 main(阶段一、二落地后),实施时以函数名定位。

行 类型 / 函数 现状
:1371 kExtrude2 RegionToString(pReg, m_sScr)
:1436、:1439 kConePipe 外/内 → sScr1/sScr2 → 拼 m_sScr
:2041 kSlicePipe → m_sScr
:2211 kRevolve → m_sScr
:2242 kRevolve2 → m_sScr
:5029 kLoft strAll(@region@ 拼接)→ m_sScr
:5087、:5101 kSweep → m_sScr
:4882、:4895 SetSolidData m_sScr = srcRegion
:5135、:5155、:5176、:5206 SetExturd2Data / SetRevolve2Data / SetSweepData / SetLoftData m_sScr = sReg
:5516、:5528、:5542、:5562、:5589 TransformBy 变换后重新导出

kIntersect(:2127) 与 kSliceSolid(:2181) 不迁移,写入保持现状(见 3.2)。

4.3.2 读取点(StringToRegion(m_sScr) → UnpackRegions)

行 类型 / 函数
:4366 kSlicePipe
:4378 kExtrude2
:4468 kRevolve
:4488、:4489 kConePipe
:4563 kRevolve2
:4587 kLoft(循环)
:4601 kSweep
:5321、:5345、:5370、:5453 ToJson
:5512、:5524、:5538、:5558、:5584 TransformBy

kIntersect(:4402) 与 kSliceSolid(:4440) 的读分支保留原样,不改成 UnpackRegions。

4.3.3 序列化

不改。m_pSatBuffer 的 writeUInt32 + writeBytes 已经在 :4822-4829,dwgInFields 有对称读。

只改一件事:迁移的 7 种 region 类型写入时不再写 m_sScr(保持空字符串)。kIntersect / kSliceSolid 例外,继续按老格式写;管廊脚本类型不受影响。

4.3.4 兼容判断

bool HasNewRegionData() const {
    if (!m_pSatBuffer || m_nSatLen < sizeof(RegionBlobHeader)) return false;
    return ((const RegionBlobHeader*)m_pSatBuffer)->magic == kRegionBlobMagic;
}

读取:

if (HasNewRegionData())
    UnpackRegions(m_pSatBuffer, m_nSatLen, regs);       // 新格式
else if (!m_sScr.IsEmpty())
    ParseLegacyRegions(m_sScr, regs);                   // 老图纸,沿用现有切分逻辑

写入:只写新格式。

不升 kCurrentVersionNumber。理由:字段都在,靠"哪个非空"即可判别;升版本会让老版本 CAD 读新图时走 eMakeMeProxy,新图在所有老版本上彻底不可编辑。代价是老版本看不到 region 类型的几何,这是已定的取舍,见 11.1。

4.3.5 kLoftReducer 纳入统一容器

CreateLoftReducer(:3015)当前已把多 region 串写进 m_pSatBuffer(:3118-3121),格式是 TCHAR 宽字符 + TCHAR(0x00c1) 分隔 —— 属于半迁移状态,体积没减半。

本次一并纳入统一容器:

  • 写入:PackRegions 替代 :3118-3121 的 TCHAR 拷贝
  • 读取:GetLoftReducerSides(:4309 附近)先验 magic;命中走 UnpackRegions,未命中回退到现有的 0x00c1 切分(老图纸)
  • kLoftReducer 的 m_sszPoint2ds 存"下口|上口"点串(:3090-3092),与 region 容器互不影响,保持不变

4.3.6 m_sScr 的去留

kEarthWorm(7) 与 kSHEarthWorm(23) 均已废弃,不再产生新数据,但存量图纸仍需可读。

本次保留 m_sScr 字段不删,仅停止写入:

  • 写入:region 类型不再写 m_sScr(保持空字符串)
  • 读取:kEarthWorm / kSHEarthWorm 的脚本分支保留,保证老图纸可读
  • 字段本身、dwgOutFields / dwgInFields 的 writeString / readString 调用全部保留

后续等存量图纸淘汰后,再单独清理字段和读写调用。


5. 公开接口处理

以下接口签名是 CString,外部有调用方(AecDbBDPipeBase.cpp 6 处、Pool3dCmd/DTYGDlg.cpp 1 处、Pool3dCmd/DTYG_MODIFY.cpp 8 处):

bool GetExturd2Data(CString& sReg, AcGePoint3d& ptFr, AcGePoint3d& ptTo);            // :5106
void SetExturd2Data(const CString& sReg, const AcGePoint3d&, const AcGePoint3d&);    // :5115
bool GetRevolve2Data(CString& sReg, AcGePoint3d&, AcGeVector3d&, double&, double&);  // :5126
void SetRevolve2Data(const CString& sReg, ...);                                      // :5137
bool GetSweepData(CString& sReg, AcDbCurve*& pCurve);                                // :5150
void SetSweepData(const CString& sReg, const AcDbCurve* pCurve);                     // :5158

保持签名不变。内部实现改为:

  • SetXxxData:把传入的文本按老分隔符解析成 region 数组 → PackRegions
  • GetXxxData:UnpackRegions → 拼回原有文本格式返回

CString 无法可靠承载二进制(含 \0 会截断),所以对外仍是"文本 SAT"语义,转换只发生在这一层,调用方无感。这些是对话框级低频调用,开销可接受。

SetSatBuffer / GetSatBuffer(:4971 / :4980)是公开裸接口,外部 Pool3dCmd/DTYGDlg.cpp:805/4371 在用。它们操作的是 SubDMesh 类数据,与 region 不冲突,靠 magic 隔离即可。


6. 风险清单

# 风险 影响 应对
1 m_pSatBuffer 语义冲突 把点数组当 region 容器解析,崩溃 强制 magic 校验,不匹配一律走老路径
2 漏改某个 ReCreate 入口 几何不显示 阶段二逐入口核对,用 EnsureCreated() 统一收口
3 老图纸读取回归 存量图纸打不开 保留 ParseLegacyRegions,不做自动迁移
4 kLoft 的 @region@ 分隔 多 region 顺序错乱 容器改成"数量 + 长度前缀",顺序显式
5 跨模块链接 CoreTools::RegionToBuffer 在 CoreCommand,CoreLibrary 是静态库 已有 CoreTools::RegionToString 先例,照现有模式
6 新旧格式并存 代码里长期保留两条读路径 可接受;等存量图纸淘汰后再清理

7. 验证清单

编译:仅 R190 + Debug,目标 AecRxBase、CoreLibrary_BAL_REMOTE/_TRIAL_YEAR、CoreCommand_BAL_REMOTE/_TRIAL_YEAR、CoreImplement。

功能(必须在 CAD 内实测):

  1. 新建 + 存盘 + 重开:迁移的 7 个 region 类型的几何与改前一致(对比 getArea、体积、getGeomExtents);kIntersect / kSliceSolid 不迁移,只需确认老图仍能打开
  2. 老图纸兼容:打开改动前保存的图,走 m_sScr 路径,几何正确
  3. 移动 / 旋转:kExtrude2、kRevolve、kRevolve2、kLoft、kSweep 变换后几何跟着动(TransformBy 的 5 处往返)
  4. 复制粘贴 / 撤销:验证 Clone() 路径(AecDbXDraw::add3DCache)不崩、不丢几何
  5. 对话框往返:DTYGDlg、DTYG_MODIFY 的 GetXxxData/SetXxxData 行为不变
  6. 并发:同时开两个 CAD 实例打开含 region 的图,几何无错乱(验证阶段一)

指标:

  • 开图耗时(阶段二)
  • 存盘后 DWG 体积(阶段三,预期 region 部分减半)
  • Clone() 耗时(阶段二后应显著下降)

8. 明确不做

项 原因
存构建曲线 + createFromCurves 拿不到原始曲线,布尔结果不可逆
自写 AcDbDwgFiler R26 readString(ACHAR**) 已 final
AcDbX3dSolid 持有 AcDbRegion 对象 拷贝构造 = delete,Clone() 退化
隐藏 BTR / 扩展字典 对象模型改动过大,AcDbObjectId 共享问题
zlib 压缩 工程无 deflate 实现,收益低于前三项
修 CMFCDbDwgFiler 的 R26 字符串问题 该类已不再使用
补 ToJson 的 kEarthWorm 分支 kEarthWorm 已废弃
删除 m_sScr 字段 kEarthWorm / kSHEarthWorm 已废弃,但存量图纸仍需可读;本次保留字段,后续单独清理
自动迁移老图纸 任何一次保存都会改写图纸数据,风险过高

9. 实施顺序

顺序 阶段 改动量 风险 收益
1 一:临时文件唯一化 小 低 消除并发错乱、越界读
2 二:懒重建 中 中 开图性能(最大)
3 三:region 二进制化 大 中高 DWG 体积减半、去编码转换

每个阶段独立提交、独立 PR,前一阶段验证通过后再进下一阶段。


10. 已确认事项

# 事项 结论
1 kLoftReducer(22) 是否纳入统一容器 纳入(4.3.5),老数据回退 0x00c1 切分
2 kSHEarthWorm(23) 是否仍在使用 已废弃,但 m_sScr 字段本次保留不删,仅停止写入(4.3.6)
3 公开 CString 接口 签名不变,内部转换,外部调用方零改动(第 5 节)

11. 阶段三暂缓(2026-10-22)

决定:暂不实施。 阶段一(临时文件唯一化、越界读)、阶段二(懒重建)已合并,主要痛点已经解决;阶段三剩下的收益只有「region 部分 DWG 体积减半」,代价是新图在老版本插件里 region 几何不可见。当前时机未到。

11.1 兼容方向(已定)

单向兼容:新版插件完整读取旧版图纸(m_sScr 老路径永久保留);旧版插件打开新图时,region 类型的 AcDbX3dSolid 几何不可见。

不可见不等于丢数据:m_pSatBuffer 老版本也会原样读写(版本 > 1 的通道一直都在),所以旧版打开后另存、再回新版打开,几何仍能恢复。非 region 类型(m_sszPoint2ds 点串、SubDMesh、管廊脚本)不受影响。

需要把图交给老版本用户时,用第 12 节的降级导出:存盘瞬间把容器翻成老格式的 m_sScr,老插件就能正常显示几何,当前文档本身不受影响。

选项 后果 结论
不升版本号,接受老插件缺几何 老插件能开图、参数都在,region 类型几何不显示;换新版可恢复 已定,选这个
升到 5,老插件显式 proxy 不会静默缺几何,但新图在所有老版本上彻底不可编辑,且不可回退 不选
保留双写(m_sScr 仍写老格式) 体积变成 2N + N,比现状更差 否决

11.2 重启前还要定的事

  1. 已定:TransformBy、ToJson 必须一起改(2026-10-22)。这两处都是 m_sScr 的读者,容器化后不改不是「导出数据不对」,而是功能直接坏:
    • TransformBy(:5488)5 个类型都靠 StringToRegion(m_sScr) 取截面,取不到就 bChanged = false,末尾不 ReCreate。kExtrude2 会留下「参数已变换、几何未变」的不一致;kRevolve / kRevolve2 / kSweep 整段分支不执行,完全不动
    • kLoft 更严重:vec 为空导致 strAll 为空,:5567 把 m_sScr 覆盖成空字符串,ReCreate 里 CreateLoft 因空输入直接 return false(:5006)——一次移动就永久丢掉截面数据
    • ToJson(:5211)三个分支(kSweep、单 region、多 region)全靠 m_sScr 解析出 __sScr__,不改则导出的 JSON 里 region 几何为空
    • 因此阶段三的范围是「所有 m_sScr 读写者」,不存在「只改生成路径、留着这两处」的做法。m_sScr 在 AcDbX3dSolid.cpp 共 66 处、24 个函数:ReCreate 18、TransformBy 10、ToJson 9、9 个 Create* 各 1、4 组 Get/Set 各 1、dwgIn/OutFields 各 1
  2. 老图纸里 region 类型的实际占比——占比低的话体积收益有限,可以永久搁置

11.3 实施时的技术前提(已核对)

  • kCurrentVersionNumber 当前是 4。m_pSatBuffer 的读写通道在 dwgOutFields:4822 / dwgInFields:4754,版本 > 1 即生效,阶段三不需要动序列化
  • 不需要新增 CoreTools::RegionToBuffer:RegionToString(const AcDbRegion*, char*&)(CoreTools.cpp:3114)已经输出 SAT 的 ASCII 字节和长度,内部用 MakeTempSatPath + DeleteFile
  • 读端 StringToRegion(Adesk::UInt64, char*)(CoreTools.cpp:3581)已经过 fixACISFormat
  • m_pSatBuffer 当前有 4 种语义:kFenceExtrude 点数组(:1514)、kFrom3JS SubDMesh(:1019/:1048)、kLoftReducer 宽字符串(:3132/:5405)、SAT 导入原始字节(:2872)。容器头的 magic 是必需的
  • 拷贝构造对 m_pSatBuffer 做了深拷贝(:274-280),Clone() 不会共享指针

11.4 当前状态

阶段 状态 PR
一:临时文件唯一化 已合并 #22、#26
二:懒重建 已合并 #25、#27
三:region 二进制化 暂缓 —
降级导出(阶段三配套) 方案已定,未实施 —

12. 降级导出(新格式 → 老格式)

12.1 结论

可行,而且不需要重建几何。容器里存的就是 acisOut 出来的 SAT ASCII 字节,老 m_sScr 里存的也是同一批字节;降级只是「解包容器 → 按老规则拼字符串」,纯内存操作,不调 acisIn、不落临时文件、不损失任何东西。

这让阶段三的兼容代价从「老版本永远看不到新图几何」变成「平时用新格式,交付给老版本用户前导一次」。

12.2 为什么在 dwgOutFields 里做

AcDbX3dSolid 的持有关系是分散的:AecDbX3dPart、AecDbX3dPartEntity,以及 AecDbHanger / AecDbRoof / AecDbStair 各自的 m_solids 缓存。从命令层遍历找不全所有实例,但存盘时每个实例一定会走 dwgOutFields。

所以在序列化层加一个进程级开关:开关打开时,dwgOutFields 把 region 容器翻译成老格式的 m_sScr,并抑制容器写出。

12.3 接口

// AcDbX3dSolid.h
static void SetLegacyOutput(bool b);
static bool IsLegacyOutput();

private:
bool MakeLegacyScr(CString& sScr) const;   // 解包容器,按 m_eType 拼回老格式
bool HasNewRegionData() const;             // 见 4.3.4

MakeLegacyScr 成功返回 true;容器损坏、解包不出 region 时返回 false,调用方退回写新格式(宁可不降级,也不写出空几何)。

12.4 dwgOutFields 的改动

只有两处::4797 写 m_sScr、:4822-4829 写容器。

CString sScrOut = m_sScr;
const void* pSatOut = m_pSatBuffer;
Adesk::UInt32 nSatOut = m_nSatLen;

if (IsLegacyOutput() && HasNewRegionData() && MakeLegacyScr(sScrOut))
{
    pSatOut = NULL;      // 容器不写出,老插件读到长度 0
    nSatOut = 0;
}

pFiler->writeString((LPCTSTR)sScrOut);
...
pFiler->writeUInt32(nSatOut);
if (pSatOut != NULL)
    pFiler->writeBytes(pSatOut, nSatOut);

关键点:

  • 不改对象状态。m_sScr / m_pSatBuffer 都不动,只换写出的内容,导出后当前图纸仍是新格式,可以继续编辑、继续存盘
  • 容器写 0 长度,老插件 readUInt32 读到 0 直接跳过,不会报错
  • 对象版本号仍写 4,不升

12.5 MakeLegacyScr 的拼法(按 3.2 的表)

类型 容器里 拼回 m_sScr
kExtrude2 / kSlicePipe / kRevolve / kRevolve2 / kSweep 1 个 region region 的 SAT 文本
kConePipe 2 个 region(外、内) 外环SAT + | + 内环SAT
kIntersect / kSliceSolid 不进容器 不涉及,m_sScr 原样写出
kLoft N 个 region N 段 SAT 用 @region@ 连接

kConePipe 的读端用 StringToRegion(sScr2)(:4489),sScr2 带着开头的 |,靠 StringToRegion 内部找 ACIS 头容错,所以拼回时照原样加 | 即可。

12.6 kIntersect / kSliceSolid 不参与迁移(已定)

这两种是遗留类型,全库没有产生路径(核对见 3.2),只可能出现在早期版本写出的图纸里。因此:

  • 阶段三不为它们设计容器格式,m_sScr 继续按老格式写
  • 降级导出对它们无事可做——本来就没进容器
  • 老图打开时仍走 ReCreate 的 StringToRegion + StringToPolyline 重建,读路径保留

之前设想的「折线后缀与 4.3.6 停写 m_sScr 冲突」因此不成立:停写范围只覆盖迁移的 7 种类型。

12.7 命令

void SaveLegacyDwg()
{
    AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
    // 目标文件:当前图名 + _legacy.dwg,或让用户选
    ScopedLegacyOutput guard;                      // 构造时置位,析构复位
    Acad::ErrorStatus es = pDb->saveAs(szTarget, false, AcDb::kDHL_CURRENT);
    if (es != Acad::eOk)
        acutPrintf(_T("\n导出失败,err=%d"), es);
}
  • saveAs(file, false, ...) 的第二个参数是 bBakAndRename,传 false 只写副本、不切换当前文档(dbmain.h:473)
  • 开关用 RAII 复位,命令异常、用户取消都不会留下「后续存盘全变老格式」的状态
  • 只处理当前数据库,外部参照里的 AcDbX3dSolid 不在范围内

12.8 一个必须说清的限制:只能兼容 kCurrentVersionNumber >= 4 的插件

dwgOutFields 写出的对象版本号固定是 4(:4776)。老插件的 dwgInFields 一旦发现 version > kCurrentVersionNumber 就返回 eMakeMeProxy(:4682),整个对象变代理,降级内容一点都读不到。

dwgInFields 里还留着 version > 2 && version < 4 的分支(:4759),说明线上确实存在版本号 3 的插件。这些插件靠降级导出救不回来——要兼容它们得额外写 v3 布局(kSweep 的路径要以 polyline 内联在对象字段之后),工作量另算,本次不做。

降级导出的目标客户是「版本号已经是 4、只是不认识容器」的插件。

12.9 验证清单

  • 迁移的 7 种类型各建一个样本,导出后装到老版本插件里打开,几何、面积、位置都正确
  • 老版本打开导出的图 → 另存 → 回新版打开,几何仍正确(回环)
  • 导出前后,当前文档自身存盘仍是新格式(开关已复位)
  • 容器被人为改坏(magic 对、region 段截断)时,MakeLegacyScr 返回 false,导出退回新格式并打日志,不产出空几何
  • 遗留的 kIntersect / kSliceSolid(若老图里有)打开、另存、再打开,截面仍能重建

12.10 成本与时机

项 量
开关 + dwgOutFields 分支 ~30 行
MakeLegacyScr 按类型拼回 ~100 行
导出命令 + 文件选择 ~40 行
主要风险 老格式规则核对不全,导出的图在老版本里读不出来

12.6 的结论已经定:遗留类型不参与迁移,阶段三不必为它们设计容器格式。与阶段三的先后关系:两者最好一起上——阶段三一旦合入、m_sScr 停写,没有降级导出的那段时间里,新图在老版本插件上就是缺几何的。