- 现状:m_sScr 用 CString 存 ACIS SAT,UTF-16 导致体积翻倍;临时文件名写死;开图逐单元走临时文件 - 否决方案:存构建曲线、自写 AcDbDwgFiler、持有 AcDbRegion 对象、隐藏 BTR、zlib 压缩 - 采用方案:SAT 的 ASCII 字节流存进 m_pSatBuffer(RegionBlobHeader + 长度前缀) - 三阶段:临时文件唯一化 -> 懒重建 -> region 数据二进制化 - 兼容策略:读兼容老图纸(m_sScr / 0x00c1 切分),写入统一新格式,不自动迁移 - 公开 CString 接口签名不变,内部转换 - kLoftReducer 纳入统一容器;kEarthWorm/kSHEarthWorm 已废弃但 m_sScr 字段暂不删除
16 KiB
AcDbX3dSolid 重构计划
目标:解决
AcDbX3dSolid用CString保存几何数据(m_sScr/m_sszPoint2ds)带来的体积翻倍、精度丢失、临时文件冲突和解析开销问题。范围:
SourceCode/Code2026/CoreLibrary/AcDbX3dSolid.cpp(5596 行)、inc/AcDbX3dSolid.h、CoreCommand/CoreTools.cpp。原则:老图纸必须能读;写入统一新格式;不改对象模型(
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 :4808-4814
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 类型里,5 种单 region,3 种双 region,1 种(kLoft)N 个 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
对应关系:
| 类型 | nRegions |
|---|---|
kExtrude2(27) / kSlicePipe(26) / kRevolve(21) / kRevolve2(38) / kSweep(40) |
1 |
kConePipe(30) / kIntersect(28) / kSliceSolid(29) |
2(外 + 内) |
kLoft(41) |
N |
magic 是必需的,不是可选项:m_pSatBuffer 是公共字节池,当前已有 4 种语义(kFenceExtrude 点数组、kFrom3JS SubDMesh、kLoftReducer 宽字符串、SAT 导入原始字节),必须自校验。
3.3 编解码
// AcDbX3dSolid.cpp 内部 static
static bool PackRegions(const AcArray<AcDbRegion*>& regs, void*& pBuf, Adesk::UInt32& nLen);
static bool UnpackRegions(const void* pBuf, Adesk::UInt32 nLen, AcArray<AcDbRegion*>& regs);
// 单 region 的 SAT 字节(CoreTools 新增,与 RegionToString 共用临时文件逻辑)
bool CoreTools::RegionToBuffer(const AcDbRegion* pReg, void*& pBuf, Adesk::UInt32& nLen);
UnpackRegions 内部用已存在的 CoreTools::StringToRegion(Adesk::UInt64 nLen, char* pszReg)(CoreTools.cpp:3525),读端无需新代码。
4. 改动清单
阶段一:临时文件唯一化(纯 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)
| 行 | 类型 | 现状 |
|---|---|---|
:1364 |
kExtrude2 |
RegionToString(pReg, m_sScr) |
:1429-1432 |
kConePipe |
外/内两个 → sScr1/sScr2 → 拼 m_sScr |
:2034 |
kSlicePipe |
RegionToString(pRegion, m_sScr) |
:2120 |
kIntersect |
→ sScr1 → m_sScr |
:2174 |
kSliceSolid |
→ sSrc1 → m_sScr |
:2204 |
kRevolve |
→ m_sScr |
:2235 |
kRevolve2 |
→ m_sScr |
:5020 |
kLoft |
strAll(@region@ 拼接)→ m_sScr |
:5073、:5087 |
kSweep |
→ m_sScr |
:4868、:4881 |
SetSolidData |
m_sScr = srcRegion |
:5121、:5141、:5162、:5192 |
SetExturd2Data / SetRevolve2Data / SetSweepData / SetLoftData(:5192 所属函数实施时核对) |
m_sScr = sReg |
4.3.2 读取点(StringToRegion(m_sScr) → UnpackRegions)
| 行 | 类型 |
|---|---|
:4364 |
kSlicePipe |
:4376 |
kExtrude2 |
:4400 |
kIntersect |
:4438 |
kSliceSolid |
:4466 |
kRevolve |
:4486-4487 |
kConePipe |
:4561 |
kRevolve2 |
:4585 |
kLoft |
:4599 |
kSweep |
:5307、:5331、:5356、:5439 |
ToJson |
:5500、:5512、:5526、:5542、:5571 |
TransformBy |
:5111、:5129、:5154 |
GetExturd2Data / GetRevolve2Data / GetSweepData |
4.3.3 序列化
不改。m_pSatBuffer 的 writeUInt32 + writeBytes 已经在 :4808-4814,dwgInFields 有对称读。
只改一件事:region 类型写入时不再写 m_sScr(保持空字符串),管廊脚本类型不受影响。
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。
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 内实测):
- 新建 + 存盘 + 重开:9 个 region 类型的几何与改前一致(对比
getArea、体积、getGeomExtents) - 老图纸兼容:打开改动前保存的图,走
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 | 二:懒重建 | 中 | 中 | 开图性能(最大) |
| 3 | 三:region 二进制化 | 大 | 中高 | DWG 体积减半、去编码转换 |
每个阶段独立提交、独立 PR,前一阶段验证通过后再进下一阶段。
10. 已确认事项
| # | 事项 | 结论 |
|---|---|---|
| 1 | kLoftReducer(22) 是否纳入统一容器 |
纳入(4.3.5),老数据回退 0x00c1 切分 |
| 2 | kSHEarthWorm(23) 是否仍在使用 |
已废弃,但 m_sScr 字段本次保留不删,仅停止写入(4.3.6) |
| 3 | 公开 CString 接口 |
签名不变,内部转换,外部调用方零改动(第 5 节) |