35 KiB
AcDbX3dSolid 重构计划
目标:解决
AcDbX3dSolid用CString保存几何数据(m_sScr/m_sszPoint2ds)带来的体积翻倍、精度丢失、临时文件冲突和解析开销问题。范围:
SourceCode/Code2026/CoreLibrary/AcDbX3dSolid.cpp(5596 行)、inc/AcDbX3dSolid.h、CoreCommand/CoreTools.cpp。状态(2026-10-22):阶段一、二已合并;阶段三(region 二进制化)决定不做,存储格式保持现状,新老插件双向可读。理由见第 11 节,后续方向见第 13 节。
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. 目标存储格式
阶段三已取消(第 11 节),以下为存档方案。
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)ToJsonGetDraw2d()(: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 节。
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 数组 →PackRegionsGetXxxData: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 内实测):
- 新建 + 存盘 + 重开:迁移的 7 个 region 类型的几何与改前一致(对比
getArea、体积、getGeomExtents);kIntersect/kSliceSolid不迁移,只需确认老图仍能打开 - 老图纸兼容:打开改动前保存的图,走
m_sScr路径,几何正确 - 移动 / 旋转:
kExtrude2、kRevolve、kRevolve2、kLoft、kSweep变换后几何跟着动(TransformBy的 5 处往返) - 复制粘贴 / 撤销:验证
Clone()路径(AecDbXDraw::add3DCache)不崩、不丢几何 - 对话框往返:
DTYGDlg、DTYG_MODIFY的GetXxxData/SetXxxData行为不变 - 并发:同时开两个 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 | 二:懒重建 | 中 | 中 | 开图性能(实际最大瓶颈在 X3dPart::Create,见 13) |
| 3 | 三:region 二进制化 | 大 | 中高 | DWG 体积减半、去编码转换(已取消) |
每个阶段独立提交、独立 PR,前一阶段验证通过后再进下一阶段。
10. 已确认事项
| # | 事项 | 结论 |
|---|---|---|
| 1 | kLoftReducer(22) 是否纳入统一容器 |
原定纳入(4.3.5),随阶段三取消 |
| 2 | kSHEarthWorm(23) 是否仍在使用 |
已废弃,但 m_sScr 字段本次保留不删,仅停止写入(4.3.6) |
| 3 | 公开 CString 接口 |
签名不变,内部转换,外部调用方零改动(第 5 节) |
11. 阶段三取消(2026-10-22)
决定:不实施。 两条理由:
- 阶段三的主要收益是图纸体积(region 部分约减半),而当前图纸大小在可接受范围内,这个收益不值一次格式变更
- 改动会改变存储格式:新图里的 region 类型在老插件上读不出几何,要么升
kCurrentVersionNumber变成 proxy,要么额外做降级导出兜底,两头都要付代价,性价比不足
阶段一、二已经解决了并发临时文件错乱、越界读、开图时的重复重建,剩下的痛点不足以支撑一次格式变更。开图速度的问题另有更直接的着手点,见第 13 节。
11.1 取消后的兼容状态
存储格式保持现状:m_sScr 继续写 UTF-16 的 SAT 文本,m_pSatBuffer 的语义也不变。新老插件双向可读,不存在单向兼容问题,也不需要第 12 节的降级导出。
存档:当初评估过三个兼容选项——不升版本(老插件缺几何,换新版可恢复)、升到 5(老插件显式 proxy)、双写(体积更差)——都随阶段三取消而不适用。
11.2 重启的前提
- 图纸体积重新成为瓶颈——阶段三的收益只有这一项
- 能接受格式变更,或愿意同时做降级导出(第 12 节)
TransformBy、ToJson与 4 组 Get/Set 必须一起改,核对结果见下
m_sScr 在 AcDbX3dSolid.cpp 共 66 处、24 个函数:ReCreate 18、TransformBy 10、ToJson 9、9 个 Create* 各 1、4 组 Get/Set 各 1、dwgIn/OutFields 各 1。范围是「所有 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 几何为空- 4 组 Get/Set(
:5120~:5209)直接读写m_sScr,外部 15 处调用方(AecDbBDPipeBase.cpp6、DTYGDlg.cpp1、DTYG_MODIFY.cpp8)依赖它们
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)、kFrom3JSSubDMesh(:1019/:1048)、kLoftReducer宽字符串(:3132/:5405)、SAT 导入原始字节(:2872)。容器头的 magic 是必需的- 拷贝构造对
m_pSatBuffer做了深拷贝(:274-280),Clone()不会共享指针
11.4 当前状态
| 阶段 | 状态 | PR |
|---|---|---|
| 一:临时文件唯一化 | 已合并 | #22、#26 |
| 二:懒重建 | 已合并 | #25、#27 |
| 三:region 二进制化 | 取消(2026-10-22) | — |
| 降级导出(阶段三配套) | 取消,方案存档于第 12 节 | — |
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 停写,没有降级导出的那段时间里,新图在老版本插件上就是缺几何的。
13. 开图速度优化方案(2026-10-22)
13.1 现状与成本模型(改造前)
AecDbX3dPartEntity::dwgInFields:1824在开图时无条件调用m_pPart->Create()。全库扫描dwgInFields,立即重建的只有这一处AecDbX3dPart::Create()(:786)先删掉m_pSolid,再对每个 unite 成员调GetDraw3dSolid()(EnsureCreated+clone+ 旋转),逐个booleanOper(kBoolUnite),再跑 subtract 列表,最后整体旋转/平移、设颜色、把m_mapXData写成BIMDATAXData- 单个 part 的成本约 N×(acisIn + clone) + (N−1)×布尔。N 大、布尔对象复杂时是非线性增长,这正是「复杂几何由 boolean 得到」的场景
Create()没有「已是最新」判断,每次调用都全量重建;AddX3dSolid(:151)只 append、不置脏,所以调用方只能靠再调一次Create()保证几何同步
13.2 方案 A:懒加载标志 + 按需重建(已实现,PR #43)
不改存储格式。分两步:A1 引入懒加载标志,A2 开图不建几何。
A1 m_bNeedCreate + EnsureCreated()
| 位置 | 改动 |
|---|---|
inc/AecDbX3dPart.h:144 |
增加 mutable bool m_bNeedCreate;。mutable 是为了让 const 读入口(CreateDraw3dSolid、Draw3d、getOsnapPoints、GetForHouseSubtract)能触发重建 |
inc/AecDbX3dPart.h:74 |
新增 bool EnsureCreated() const; |
inc/AecDbX3dPart.h:121 |
新增 protected: bool Rebuild();(无条件重建布尔结果) |
构造 :51 / 拷贝构造 :64 |
置 true |
Create() :802 |
保持强制重建语义,直接 return Rebuild(); |
Rebuild() :809 |
开头先 m_bNeedCreate = false; 再 DELETE_PTR(m_pSolid) 重建,写法同 AcDbX3dSolid::EnsureCreated(:4620),避免构建过程中再次触发 EnsureCreated 造成递归 |
EnsureCreated() :929 |
if (!m_bNeedCreate && m_pSolid != NULL) return true; 否则 const_cast 调 Rebuild() |
| 置脏接口(15 处) | AddX3dSolid:155、SetOrgPoint:175、RemoveAll3dSolidRec:474、RemoveAt:511、SetAxis:590、SetHide:617、PeelOffTheSkin:633、SetActive:650、AddMove:656、AddMove(matrix):668、ResetMove:685、AddRotate:697、ResetRotate:721、SetBoolOperFor:783、CopyFrom:1300 |
SetHideShow() 不单独置脏——它内部转调 SetHide(),已覆盖。
Create() 为什么不做幂等早退:外部有 10 处是「先 GetX3dSolid(0) 拿到 x3dsolid 直接改(如 AecDbBDPipeBase.cpp:941 调 TransformBy),再调 Create()」。这类改动部件无法感知,m_bNeedCreate 仍是 false,若 Create() 早退就会返回旧几何。所以显式 Create() 一律重建,只有 EnsureCreated() 条件重建。
现状里 AddMove、AddRotate 等已经 DELETE_PTR(m_pSolid) 做失效,改成置脏后语义一致,额外好处是能区分「已失效但还没人要」和「已重建」。
A2 开图不建几何
AecDbX3dPartEntity::dwgInFields:1823去掉m_pPart->Create(),由AecDbX3dPart::dwgInFields结束时(:1233)置脏- 读入口统一先
EnsureCreated():
| 入口 | 位置 |
|---|---|
GetForHouseSubtract() |
AecDbX3dPart.cpp:939 |
CreateDraw3dSolid() |
:978 |
Draw3d() |
:1009 |
getOsnapPoints() |
:1145 |
AecDbX3dPartEntity::Draw3d() |
AecDbX3dPartEntity.cpp:1755,经 CreateDraw3dSolid 已覆盖,确认即可 |
AecDbBDPipeBase.cpp:329 那种「CreateDraw3dSolid() 返回空就 Create() 再试」的兜底可以删掉,EnsureCreated 已经包含在 CreateDraw3dSolid 里。
收益边界
- 关闭/冻结图层、视口外、XREF 附着、审核、发布等不绘制的场景:完全省掉
- 全图都在视口内:开图时间下降,首次 regen 补回来,总时间基本持平
风险
Create()有副作用:整体变换、setColorIndex、m_mapXData.Write(m_pSolid, "BIMDATA")(:903-924)。延迟后这些也延迟,凡是在建好之前读 solid 的代码都必须经过EnsureCreated- unite 列表为空时
Create()返回 false 且m_pSolid == NULL;DTYG_TYPE且Hide的分支直接return true且不建体(:792-799)。EnsureCreated的判定同时看m_bNeedCreate和m_pSolid != NULL,两个都为假才早退,不会因为m_pSolid == NULL反复重试 Create()非 const,EnsureCreated需要const_cast,因此标志本身必须mutableCopyFrom:1321-1337里对每个 x3dsolid 仍调了ReCreate()。AcDbX3dSolid已有m_bNeedCreate,这段可以删掉,属可选清理,本次未动
13.3 方案 B:缓存布尔结果(改格式,暂不推荐)
把 part 最终的 solid 用 acisOut 存成 SAT,开图时一次 acisIn 代替 N 次建体 + N 次布尔。这是唯一真正减少计算量的办法,但两条落点都有代价:
| 落点 | 兼容性 | 问题 |
|---|---|---|
AecDbX3dPart 的 dwg 字段 |
需要升版本号,老插件整块变 proxy | 与阶段三取消的理由相同,不接受 |
| 实体扩展字典(xrecord 分块存 SAT) | 老插件忽略未知 xrecord,仍按参数重建 | 保存/打开后写缓存、失效判定、体积膨胀都要额外处理 |
体积上,布尔结果的 SAT 通常比输入参数之和大一个量级,复杂 part 可能到几百 KB,会打破「图纸大小可接受」这个前提。
结论:A 已落地(13.2),量出开图耗时里布尔占多少,再决定 B 值不值。
13.4 方案 C:降低单次重建成本(可选)
- 合并前先按 bbox 剔除被完全包含的成员,少跑几次布尔
GetDraw3dSolid()每个成员 clone 一次完整 solid,N 个成员就是 N 次深拷贝;只参与一次布尔的成员可以改成「取原生指针直接参与运算」,但要逐个确认所有权(booleanOper会消耗传入对象)AecDbECFitting::DrawUserInf3d(AecDbECFitting.cpp:4001)每次绘制都新建临时AecDbX3dPart并Create(),和持久化对象无关,属于另一个热点
13.5 度量与验收
- 埋点:
AecDbX3dPart::Create()的调用次数与累计耗时、开图总时长、首次 regen 时长 - 场景:全图可见 / 一半图层冻结 / 缩放到局部 / 作为 XREF 附着
- 判据:开图时长下降;首次 regen 的增量不超过开图省下的量;移动、改参数等交互不退化
13.6 进度与后续
A1(、m_bNeedCreate+EnsureCreated())A2(:已实现,PR #43dwgInFields去掉Create())- 待做:按 13.5 度量真实图纸的开图耗时,确认收益后再决定 B / C