diff --git a/.agents/features/cad-exchange.md b/.agents/features/cad-exchange.md index 0260b4e..c68aa51 100644 --- a/.agents/features/cad-exchange.md +++ b/.agents/features/cad-exchange.md @@ -28,7 +28,13 @@ covers: src/Import/CadExchange.*, src/Import/AcisReader.*, src/Serialization/Dwg - 3DSOLID / REGION / BODY / 各类曲面(`Dwg::IsAcisEntity`):`Importer::ConvertAcis` 经 `Dwg::DecodeAcisData` 取出 ACIS(R2013 起查 `Dwg::ReadAcisDataStore`),`Import::ReadAcis` 读成 B-rep,用 `SetCadImportSolidKernel` 设的内核 `FromBrep` 建成 `Solid3DEntity`;没有内核或建不出时只显示边线框,结束时按类型 `LOG_WARN`。格式与限制见 `docs/Solid/PLAN.md` 第 16 节。导出:`Solid3DEntity` 写成多面网格(只有边线、没有网格的跳过并提示);原图未改动的实体在同格式同版本另存时原样写回 - 不支持的类型跳过并按类型统计,导入结束时 `LOG_WARN`(读入 `m_skipped`;导出时不能写的实体经 `Skip` 同样统计)。改映射前先用 skill `drawing-debug` 看原图里有哪些类型 - OLE2FRAME(嵌入的 Word / Excel 等)只导入显示:MiniDWG 原样保留,`Dwg::DecodeOle2Frame` 解出数据,`src/Core/Ole/OleObject` 解析 AutoCAD 头与 OLE 复合文档里的展示图(WMF / EMF / DIB),显示为 `OleFrameEntity`;图元文件经 `AppPlatform::RasterizeOle` 光栅化(桌面版 GDI,网页版只画边框) -- 其他软件的自定义实体(`SWR_*`)没有专门适配:带代理图形的经 `Tch::DecodeProxyGraphics` 按代理图形显示(图层 / 线型序号与文字字体经 `Importer::ProxyContext` 映射,格式与支持的记录见 `docs/evidence/Tch-proxy-graphics.md`),结束时按类型 `LOG_WARN` "显示为普通几何";`SWR_BLOCK_INSERT` 的代理为空,按块参照前缀解(`Tch::DecodeBlockInsert`)。代理图形里不支持或数据不合理的记录只跳过那一条(`TchProxyGraphics::Skipped`),记录头损坏、有裁剪 / 变换矩阵记录或一个图元都没有时整个对象不显示,看 `m_tchFailures` 打出的原因;`MINICAD_PROXYEXT=SWR_*` 列出各对象用到的记录类型 +- 其他软件的自定义实体(`SWR_*`)没有专门适配:带代理图形的经 `Proxy::Decode`(`src/Import/ProxyGraphics.h`,命名空间 `MiniCAD::Proxy`)按代理图形显示(图层 / 线型序号与文字字体经 `Proxy::ProxyGraphicsContext` 映射,格式与支持的记录见 `docs/evidence/Tch-proxy-graphics.md`),结束时按类型 `LOG_WARN` "显示为普通几何";`SWR_BLOCK_INSERT` 的代理为空,按块参照前缀解(`Tch::DecodeBlockInsert`)。`MINICAD_PROXYEXT=SWR_*` 列出各对象用到的记录类型 + - 代理图形解码器是**厂商无关**的:只解记录流,不看对象类名。天正、斯维尔、浩辰、建筑结构设计工具(`ASDT_*`)等写进来的都是同一套记录格式,因此走同一个函数 + - **容错按「能解多少算多少」**:不认识的记录、数值不合理的记录只**跳过该条**(`ProxyGraphics::Skipped` 计数),其它已识别的记录照常输出,不再整份放弃;坐标超出 `|v| < 1e12` 的图元直接丢弃(`Discarded` 计数),防止少量脏数据把包围盒撑到 1e70 量级、导致"全部显示"时整张图缩成一个点。一条都解不出时才返回 false,看 `m_tchFailures` 打出的原因(含跳过 / 丢弃条数) + - 支持记录类型:`1`(包围盒)`2/3`(圆)`4/5`(弧)`6/7/32`(折线 / 多边形 / 带法向)`9`(shell)`10/11/36/38`(文字)`14/16/18/19/20/22/23/24/25`(颜色 / 图层 / 线型 / 标记 / 填充 / 线宽 / 线型比例 / 厚度)`33`(内嵌 LWPOLYLINE,位字段编码,payload 前 4 字节是长度前缀) + - **位流的位序是 MSB-first**(先取字节最高位);用 LSB-first 解 `33` 会得到 `flags=0x1FE4` 这类垃圾值,进而算出天文数字坐标。`DD`(带默认值的 double)是**字节差分编码**(`00` 用默认值、`01` 覆盖低 4 字节、`10` 覆盖偏移 4-5 加低 4 字节、`11` 完整 8 字节),不是"沿用旧值" + - `22` 颜色记录的 4 字节按 **[method, r, g, b]** 存放,不是小端 u32:实测句柄 `368F5` 的 `c2 00 00 00` = 真彩 RGB(0,0,0) 黑框、`c3 00 00 07` = ACI 7 白字。按 `value >> 24` 取 method 会得到 0,两条都落进 ByLayer 兜底,黑框就画成了背景色(浅色主题下是白块)盖住文字 + - `fill`(记录 `20`,`1` = 总是填充、`2` = 从不)作用在闭合图元上时,**既出实心填充、也出轮廓线**。只出填充不加轮廓,会让文字的背景框渲染成一个没有边框的纯色块 - 天正对象见 [tianzheng](tianzheng.md),外部参照见 [xref](xref.md),图纸空间视口见 [layout-viewport](layout-viewport.md) ## 以原图为底另存(`CadSource`) diff --git a/.agents/features/tch.md b/.agents/features/tch.md index c423f76..51c08d1 100644 --- a/.agents/features/tch.md +++ b/.agents/features/tch.md @@ -26,7 +26,8 @@ covers: src/Import/Tch*, tests/Core/Tch*, tests/Core/TianzhengWallTests.cpp | `TchText.h` | TCH文字的样式规则(文字样式上 `_TCH` 扩展数据给出西文字高与宽度系数) | | `TchShape.h`、`TchPathArray.h` | TCH矩形;线图案(沿路径重复排列图案块) | | `TchPipe.h`、`TchElec.h`、`TchHvac.h` | 给排水管线;电气桥架与配件、折断线;暖通风管、管件(法兰、变径、弯头、三通、四通、乙字弯)与风管标注 | -| `TchBlockInsert.h`、`TchProxyGraphics.h` | TCH块参照的标准几何前缀(`SWR_BLOCK_INSERT` 布局相同);代理图形记录(也用于其他带代理图形的自定义实体,见 `docs/evidence/Tch-proxy-graphics.md`;TCH另存为代理实体的图纸里图层、线型序号多为 -1,按对象自身的处理;不支持或数据不合理的记录只跳过那一条,裁剪 / 变换矩阵记录整个放弃) | +| `TchBlockInsert.h` | TCH块参照的标准几何前缀(`SWR_BLOCK_INSERT` 布局相同) | +| `TchProxyGraphics.h` | 天正这一侧的代理图形入口:`Tch::DecodeProxyGraphics` 是通用解码器 `Proxy::Decode`(`src/Import/ProxyGraphics.h`)的薄封装,只做类型别名与转发。代理图形本身与厂商无关(也用于其他带代理图形的自定义实体,见 `docs/evidence/Tch-proxy-graphics.md`;TCH另存为代理实体的图纸里图层、线型序号多为 -1,按对象自身的处理;不支持或数据不合理的记录只跳过那一条,裁剪 / 变换矩阵记录整个放弃) | ## 约束 @@ -36,9 +37,14 @@ covers: src/Import/Tch*, tests/Core/Tch*, tests/Core/TianzhengWallTests.cpp - **新版(T30,TCH基类版本 9~14)**:各类的版本号与明文版本的对应写在 `Read*Fields` / `Decode*Cipher` 上方的注释里。自带 RC 版本与密钥、第二个密钥为 0 的子层用 `Tch::Detail::CipherLayer`(表格子对象、屋顶基类、管线接口、标注样式、管件基类的列表)。TCH基类里的属性包值与文字方向记在 `Tch::Detail::CipherReader::FieldStrings` / `Tch::Detail::CipherReader::FieldNumbers` / `Tch::Detail::CipherReader::Angle`(立管、多管标注要用)。机电:管线节点基类没有加密层(`Tch::Detail::ReadCipherPipeNode`);属性表移进了TCH基类(`Tch::Detail::CipherReader::XData`),管线基类版本 4 在末尾另存 Start、End(`Tch::Detail::ReadCipherPipeBase`) - **DWG2004**:没有单独的文字流,GUID、属性名与密文都是主数据流里的 GBK 文字;密文字符在 GBK 里有的存为双字节,没有的写成 `\U+XXXX`,`Tch::Detail::CipherReader` 取各字符的低字节解密,与 2007 起 UTF-16 的做法相同。实物图纸(中望看图另存)里坐标标注读出的格式 48、方向 3 超出已核对的范围,仍跳过。明文机电对象里只有管线、管件、管线文字(`Tch::DecodePipe`、`Tch::DecodePipeFitting`、`Tch::DecodePipeText`,`Tch::Detail::CheckPayload` 的 `inlineText`)按内嵌字符串读 2004 格式,其余给排水类仍要求 2007 起 - **暖通**:风管与管件的几何都在属性表里(`Tch::ReadCableTrayMap` / `Tch::ReadCableFittingMap`,明文管件的属性表后是边界列表(每条带若干段)与连接数,与风管相同)。管件按属性表的键分类型(`Tch::DecodeHvacFitting`):三通 I4 为支管样式(1 直、3 弧形、5 Y 形、7 Y 形而对侧为直边),四通有 Pt4 / Pt5,乙字弯有 R28、R34 而无 Vt6(R2 为偏移,竖向偏移画直管加 Z 形符号),分节弯头的节数 I5 ≥ 2 时取 I5、否则 I4 − 1。风管标注 `TCH_HVACDIMENSION` 是TCH基类 + BS 版本 1 + 属性表(`Tch::DecodeHvacDimension`):S1 为“|”分隔的显示文字,I4 = 0 时文字居中在 Pt6,I4 = 4 时引线到 Pt7 再画两行两列的表格;天正按字形实际宽度定列宽,这里是估算(误差约 1 个字宽)。以上对照 2004 格式图纸的天正分解核对;分解结果会被重叠对象裁掉一部分,比对时只看“参照有而 MiniCAD 没有”的部分 -- **TCH基类版本7**(ZW看图另存为2018格式):`ReadTchBase` 在版本 5 的字段后读属性表(键在文字流、值在主数据流,`Tch::Detail::SkipXDataMap`),两个句柄个数分别多存了 5、3;之后接旧版明文的各类字段(柱 6、门窗 5 等)。属性表要从头读文字流,所以 `ReadTchBase` 要传 `data`,只适用于对象自身的基类(组合门窗的子门窗不传,版本 7 按不支持)。墙版本 10(`Tch::Detail::DecodeWallFields`):受保护几何同标记 7 先互换半字节再异或密钥,墙高在左右宽之前,结尾标记后多 13 个 0 位(含义未核对,只接受不超过 16 位且全为 0)。坐标标注版本 6(格式 48、方向 3)仍跳过 +- **TCH基类版本6~8(天正 T20,t20V2~V7 的图纸)**:`ReadTchBase` 在版本 5 的字段后读属性表(键在文字流、值在主数据流,`Tch::Detail::SkipXDataMap`),之后接旧版明文的各类字段;要传 `data`。三者的主数据流除首字节版本号外布局相同,差别是:版本 **6 的句柄个数不减偏量**、7/8 各减 5/3;版本 **8 在属性表后多一个内联 GUID 变长字符串**(`ReadVariableText`),跳过它各类字段才对得上。组合门窗的子门窗、表格 `DecodeSheet` 等也要给 `ReadTchBase` 传 `data`。墙受保护几何的类版本随基类变:6→9、7→10、8→11。三者解密 / 字段顺序已按 t20V2~V7 的实物图纸核对:**版本 9 只异或密钥、不换半字节,墙高在左右宽之后,且部分墙收尾标记后多 ≤16 个全零位**;**10/11 先互换半字节再异或,墙高在左右宽之前**(10 的尾部 13 个 0 位照旧)。版本 11(base 8)在受保护几何之后、门窗段之前多 4 个字节标记(实测 76 36 76 76 一类),跳过它门窗前缀才读得到 0。墙 `DecodeWallFields` 的 `t20Base` 按 `Main[0]` 在 6~8 判断。各类的字段版本随 T20 升级:`TCH_COORD` 版本 6 在 `LineLength` 后追加「字节 + 二维点 + 内联文字」(与 `DecodeCoordCipher` 尾部一致);`TCH_RECTSTAIR` 版本 5/6 末尾多一个 `BitShort`(与 `DecodeRectStairCipher` 的 `f.Short()` 一致);`TCH_NORTHTHUMB` 部分 T20 版本主数据流末尾补 ≤34 个全零位,按全零尾部接受。`TCH_TEXT` / `TCH_MTEXT` 加密版(`DecodeTextCipher`)的类版本 3(T20 V8)句柄流只有 TextStyle 一个、版本 4 起末尾另有两个句柄,按类版本区分读取;V8 的文字因此能全部解析。索引符号 `TCH_INDEXPOINTER`:AC1018 的文字内联在主数据流里,字段顺序与加密版 `DecodeIndexPointerCipher` 一致——4 段文字(编号 / 图号 / 上旁注 / 下旁注)后按类版本分支:**版本 1/2 即结束**(版本 2 多 2 字节),**版本 3/4** 再接两张字符串表、2 个 BD + 字节、多段线、收尾 BD 与内联文字;以前把两段文字当列表计数、又把余下文字放到单独文字流里,主数据流少读约 330 位。坐标标注 `TCH_COORD` 的类字段按版本递增:1 只有点 + 值 4 个 RD,2 补引线点 2 个 RD,3 再补一个字节,4 起才有 BD `TextGap`,5 补 `LineLength`,6 再追加「字节 + 二维点 + 内联文字」;旧版(基类 2~4)因此能全部解析。**版本 1 没有引线点字段**,`BuildCoord` 按天正的默认规则补出:从标注点沿 `(-4.2894*字高缩放, +4.5069*字高缩放)`(`Side` 决定左右镜像)引到水平线起点——这组倍率取自 t8/t10/t20V5 里存了引线点的同位置标注(三者值一致),补出的斜引线与文字位置与那些版本逐项相同。旧版(基类 2~4)的箭头 `TCH_ARROW` 与矩形楼梯 `TCH_RECTSTAIR` 也已按类版本核对:箭头类版本 2 在 `ArrowLength` 后即结束、3 多一个 Head 字节、4/5 才接完整尾部;楼梯类版本 1 尾部是「1 字节 + 1 个 BD + 2 位」且句柄流更短、3 读到「版本 ≥ 3」的 2 BD + 字节为止、4 起才读完整字段。旧版墙(基类 2/3/4,t5/t6/t7)没有受保护的 84 字节块,几何是主数据流里的**明文位级 BD 字段**:`ReadTchBase` 后 +8 位读 7 个 BD(起点 XYZ、终点 XYZ、曲线参数),此后相对 +12 / +78 / +144 位是三个明文 RD(左宽、右宽、墙高,实测 120 / 120 / 3000),三个版本的相对偏移一致;`DecodeOldWall` 按这些偏移读并用 `CheckWall` 数值校验兜底。旧版标注 `TCH_DIMENSION`(区别于 `TCH_DIMENSION2`)与后者字段序列相同、只是类版本号为 3,`DecodeDimension` 放行该 DxfName 与版本 1~3 即可复用(`Exact()` 校验通过,实测位置 / 角度 / 跨数对齐) +- **老虎窗 `TCH_DORMER`**(`TchDormer.h`):代理图形是空的(只有 8 字节头、0 条记录),几何必须从私有数据解;两个家族字段语义一致但编码不同。明文(TCH基类 3~8,t6~t20V7):`ReadTchBase` + version(2) + `BS`(1) + 10 个 BD 参数 + 6 个 BD(插入点 X/Y/Z,然后**屋面法向** X/Y/Z)。加密(基类 9~14,原始图 / t20V8 / V9):`CipherReader::ReadBase` + version(3) + `ReadKey` + 11 个 `Double`(首值 1,其余 10 个参数)+ `P3`(插入点,按 Key 重排为 X/Y/Z)+ `P3`(屋面法向)。各版本这 10 个参数实测完全相同(200, 200, 0.5, 450, 200, 1500, 180, 1800, 1300, 1400),插入点 X=61689.23 / Y=29421.88 / Z=744.23、法向 (-0.7656, 0, 0.6433)(模长 1)也一致。**平面朝向取法向在 XY 的方位角 `atan2(NY, NX)`**:实测该值(180°)让老虎窗背靠最近的竖直墙(靠墙角距墙 66、背离侧 1466),与图纸布局自洽;早前把法向的 X 分量(-0.7656)当旋转角用,方向差约 136°,已修正。`BuildDormer` 按「+u 坡向 / +v 墙面方向」画矩形窗洞 + 中脊线 + 两条坡屋面线(伸出量按坡度 `Params[2]`),参数几何含义仍未与 CAD 逐项核对 +- **房间 `TCH_SPACE`**(`TchSpace.h`):代理图形为空(8 字节头、0 条记录),几何是私有数据里的轮廓。加密对象(基类 9~14):`ReadBase` + `version` + `ReadKey` 之后有一个 `BS` 顶点数,然后是顶点数组——每个顶点是两个明文 RD(X、Y),组内共 130 位(两个 RD 加 2 位)。顶点数组结束位恰好落在主数据流末尾,据此在 `afterKey` 之后扫描定位数组起点(要求数组末尾对齐 `MainBits`),8 个顶点的房间轮廓(实测 `33559.67/24669.33` 起)画成闭合多段线。其余字段(房间名等)未核对,只画轮廓 +- **曲面网格 `TCH_CURVEMESH2`**(`TchCurveMesh.h`):代理图形为空。两个家族——加密(基类 9~14,原始图)与明文(基类 2~8,t7);顶点都是明文 BD 三点组(X、Y、Z)。实现用「找最长的连续合理三点组」定位顶点数组(扫描窗口 640 位),再按顺序连成折线显示;X 或 Y 允许为 0(网格边界顶点) +- **多版本核对**:`tools/tch_versions_check.ps1` 用 `MiniCADDwgInspect` 批量扫描一组不同天正版本另存的同一张图(`-Dir` 传目录,本机图纸不入库),统计各文件「显示为普通几何 / 轮廓」(解析成功)与「无法恢复显示」的类型数,低于阈值报错。测试 exe 里直接 `ImportCad` 这些图会因栈溢出崩溃,用独立的 `MiniCADDwgInspect` 进程核对 - **新版未核对**(只有合成样例,没有实物图纸对照):屋顶、曲线楼梯、剖切线、双跑楼梯、阳台;自动扶梯(第三方查看器画两道、MiniCAD 一道);设备 / 井 / 水泵的图块、多管标注、版本 4 的桥架与风管、桥架配件与风管管件(井标注能显示井的标高,说明井按新版布局读对了)。未支持:带形窗(与转角窗同为 `TCH_CORNER_WINDOW`);修订标记另存的多段线不画 - **剖切符号**:旧样式 `Tch::BuildSection`,新国标样式 `Tch::BuildSectionNewStyle`;样式开关在 `Tch::Detail::CipherReader::XData` 里,旧版明文对象一律旧样式 +- **半径 / 直径标注 `TCH_RADIUSDIM`**(`Tch::BuildRadiusDim`):天正用同一个 `TDbRadiusDim` 类承载半径与直径两种标注,靠 `Flags` 的 `0x10` 位区分。**直径**(置位)按天正默认样式画:一条直径线贯穿圆心、两端各一个箭头指向圆周内壁、文字(`⌀` + 两倍半径,如 `⌀1500`)水平放在圆心上方的线中央;**半径**(未置位)则是箭头尖在圆周、沿半径方向引线、文字写 `R` + 半径。直径线的方向取自存盘的 `Center→Tip` 方向(实测与水平差 0.41°,属数据本身,不强行拉平)。以前一律按半径画并写 `R`,直径标注会被画成半径 - **柱、幕墙**:柱 `Tch::Detail::DecodeColumnCipher`(版本 7~10,版本 ≥ 9 的出图比例取对象自己的);矩形 `Tch::Detail::DecodeRectCipher`(版本 3);幕墙在 `Tch::Detail::DecodeWallCipher` 的墙字段之后读幕墙版本 3,分格列表是否存在看墙标志 0x100 / 0x10000(旧版明文同样,样例两位都有,没有实物核对) - **影响显示的开关**(新旧版同读):折断线按 `TchRupture::Count` 等分、0 不画;扶手 `TchHandRail::Align` 为 1 / 2 时路径是一侧边线,`TchHandRail::Draw2d` 为假不画;阳台 `TchBalcony::NoRailEdges` 置位的段(靠墙)不画,内缘在该段不偏移(`Tch::Detail::OffsetPathEdges`);转角窗两段各按 `TchCornerWindow::Thickness` / `TchCornerWindow::Thickness2` 向自身方向右侧偏移;自动扶梯与多跑梯标志位 10 清零时不画箭头(`TchAutoStair::Flags`);设备 Y 比例、阀门 Y 比例与象限(1 X 向镜像、2 两向、3 Y 向镜像),Y 比例近 0 时取 X 比例。读了仍没用的:修订版次文字、柱标注文字与保温层、双线管法兰、弧形平台与外侧栏杆、幕墙分格线、设备系统图块、转角窗文字转角 - **对称轴**:`Tch::BuildSymmetry`,两端各两条横线,距端点 Gap、Gap + Spacing(乘比例) diff --git a/src/Core/Entity/EntityColor.hpp b/src/Core/Entity/EntityColor.hpp index 9ad2152..b02ef97 100644 --- a/src/Core/Entity/EntityColor.hpp +++ b/src/Core/Entity/EntityColor.hpp @@ -111,8 +111,9 @@ namespace MiniCAD if (Method != ColorMethod::ByRgb) return Rgba; auto pure = [](const Math::Color4& c, double v) { - auto near = [v](double x) { return x > v - 0.004 && x < v + 0.004; }; - return near(c.r) && near(c.g) && near(c.b); + // 变量不能叫 near:Windows 头文件里有同名宏,包含它之后会被展开成语法错误 + auto isClose = [v](double x) { return x > v - 0.004 && x < v + 0.004; }; + return isClose(c.r) && isClose(c.g) && isClose(c.b); }; if (pure(Rgba, 1.0)) return { 1.0 - kTrueColorInset, 1.0 - kTrueColorInset, 1.0 - kTrueColorInset, Rgba.a }; if (pure(Rgba, 0.0)) return { kTrueColorInset, kTrueColorInset, kTrueColorInset, Rgba.a }; diff --git a/src/Import/CadExchange.cpp b/src/Import/CadExchange.cpp index 7bb5a32..735de9c 100644 --- a/src/Import/CadExchange.cpp +++ b/src/Import/CadExchange.cpp @@ -13,6 +13,9 @@ #include "Import/TchAnnotation.h" #include "Import/TchSheet.h" #include "Import/TchShape.h" +#include "Import/TchDormer.h" +#include "Import/TchSpace.h" +#include "Import/TchCurveMesh.h" #include "Import/TchMultiStair.h" #include "Import/TchSymbol.h" #include "Import/TchPipe.h" @@ -1799,6 +1802,24 @@ namespace MiniCAD ++m_tchConverted[unknown->Raw.DxfName]; return MakeTchGraphics(src, Tch::BuildRect(tchRect), TextStyleTable::StandardID); } + Tch::TchDormer dormer; + if (Tch::DecodeDormer(*unknown, dormer, &nativeReason)) + { + ++m_tchConverted[unknown->Raw.DxfName]; + return MakeTchGraphics(src, Tch::BuildDormer(dormer), TextStyleTable::StandardID); + } + Tch::TchSpace tchSpace; + if (Tch::DecodeSpace(*unknown, tchSpace, &nativeReason)) + { + ++m_tchConverted[unknown->Raw.DxfName]; + return MakeTchGraphics(src, Tch::BuildSpace(tchSpace), TextStyleTable::StandardID); + } + Tch::TchCurveMesh curveMesh; + if (Tch::DecodeCurveMesh(*unknown, curveMesh, &nativeReason)) + { + ++m_tchConverted[unknown->Raw.DxfName]; + return MakeTchGraphics(src, Tch::BuildCurveMesh(curveMesh), TextStyleTable::StandardID); + } Tch::TchSlab slab; if (Tch::DecodeSlab(*unknown, slab, &nativeReason)) { diff --git a/src/Import/ProxyGraphics.h b/src/Import/ProxyGraphics.h new file mode 100644 index 0000000..601ae34 --- /dev/null +++ b/src/Import/ProxyGraphics.h @@ -0,0 +1,751 @@ +#pragma once +// 代理图形(proxy graphics)通用解码器。 +// +// 自定义实体在 DWG 里会附带一段预先算好的二维矢量快照(代理图形),供没有安装 +// 原插件的程序也能把对象画出来。这段数据与厂商无关——天正(TCH_*)、斯维尔 +// (SWR_*)、浩辰、建筑结构设计工具(ASDT_*)等写进来的格式是同一套记录流, +// 区别只在各自用到了哪些记录类型、以及少数记录的取值约定。 +// +// 本文件只做「把记录流还原成基础图元」,不含任何厂商分支;天正私有的主数据流 +// (墙体、门窗、楼梯等语义)另见 `Tch*.h`。 +// +// 记录格式(小端): +// u32 total_size (== 整段长度) +// u32 record_count +// record_count × { +// u32 record_size (字节数,含这 8 字节头) +// u32 record_type +// u8[record_size-8] data +// } +// 字符串为 NUL 结尾(8 位或 UTF-16LE),按 4 字节对齐。 +// +// 容错原则「能解多少算多少」: +// · 未知记录类型 → 跳过该条,继续解后面的记录(不再整份放弃) +// · 单条记录数据不合理 → 跳过该条,继续 +// · 坐标超出合理范围 → 丢弃该图元(防止少量脏数据把整张图的包围盒撑爆) +// · 一条都解不出 → 返回 false,由调用方决定怎么提示 +// 返回值只表示「有没有解出可显示的图元」,具体跳过情况记在 Skipped 里。 + +#include "Database/UnknownObjects.h" +#include "Codec/CodePage.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MiniCAD::Proxy +{ + struct ProxyGraphics + { + std::vector> Entities; + // 因为不认识 / 数据不合理而跳过的记录条数。几何仍照常输出这些除外。 + std::size_t Skipped = 0; + // 因为坐标超出合理范围而丢弃的图元个数(正常图纸应为 0)。 + std::size_t Discarded = 0; + }; + + // 记录里的图层 / 线型是表内序号,文字只给字体文件名:由调用方按图纸提供映射 + struct ProxyGraphicsContext + { + std::vector Layers; // 图层表的表项顺序 + std::vector LineTypes; // 线型表的表项顺序 + MiniDWG::Handle ByLayerLineType = MiniDWG::kNullHandle; + MiniDWG::Handle ByBlockLineType = MiniDWG::kNullHandle; + std::function StyleForFont; + MiniDWG::Codec::CodePage CodePage = MiniDWG::Codec::CodePage::Gbk; + }; + + namespace Detail + { + // 坐标合理上限。代理图形偶尔有解错位产生的天文数字,一旦进入场景会把包围盒 + // 撑到 1e70 量级,视图「全部显示」时整张图缩成一个点(真实事故)。1e12 远大于 + // 任何实际工程图纸(即使以毫米为单位也够用),却足以挡住脏数据。 + inline constexpr double kMaxCoordinate = 1e12; + inline bool Sane(double v) { return std::isfinite(v) && std::abs(v) < kMaxCoordinate; } + inline bool Sane(const MiniDWG::XYZ& p) { return Sane(p.X) && Sane(p.Y) && Sane(p.Z); } + + struct Reader + { + std::span Bytes; + std::size_t Position = 0; + bool Good = true; + std::uint32_t Long() + { + if (Bytes.size() - Position < 4) { Good = false; return 0; } + std::uint32_t value = 0; + for (int i = 0; i < 4; ++i) value |= std::uint32_t(Bytes[Position++]) << (8 * i); + return value; + } + double Double() + { + if (Bytes.size() - Position < 8) { Good = false; return 0; } + std::uint64_t bits = 0; + for (int i = 0; i < 8; ++i) bits |= std::uint64_t(Bytes[Position++]) << (8 * i); + double value; + std::memcpy(&value, &bits, sizeof(value)); + if (!std::isfinite(value)) Good = false; + return value; + } + MiniDWG::XYZ Point() { const double x = Double(), y = Double(), z = Double(); return {x, y, z}; } + void Align() { Position = std::min(Bytes.size(), (Position + 3) & ~std::size_t(3)); } + std::string PaddedString() + { + const std::size_t start = Position; + while (Position < Bytes.size() && Bytes[Position] != 0) ++Position; + if (Position >= Bytes.size()) { Good = false; return {}; } + std::string value(reinterpret_cast(Bytes.data() + start), Position - start); + ++Position; + Align(); + return value; + } + std::string PaddedUnicodeString() + { + std::string value; + for (;;) + { + if (Bytes.size() - Position < 2) { Good = false; return {}; } + char32_t unit = Bytes[Position] | (char32_t(Bytes[Position + 1]) << 8); + Position += 2; + if (unit == 0) break; + if (unit >= 0xd800 && unit < 0xdc00 && Bytes.size() - Position >= 2) + { + const char32_t low = Bytes[Position] | (char32_t(Bytes[Position + 1]) << 8); + if (low >= 0xdc00 && low < 0xe000) + { + Position += 2; + unit = 0x10000 + ((unit - 0xd800) << 10) + (low - 0xdc00); + } + } + MiniDWG::Codec::AppendUtf8(value, unit); + } + Align(); + return value; + } + bool Done() const { return Good && Position == Bytes.size(); } + }; + + // DWG 位流读取(type 33 内嵌的 LWPOLYLINE 用位字段编码,与其它记录不同)。 + // + // ⚠ 位序是 **MSB-first**:一个字节里先取最高位。这与直觉相反,但 ODA 的位流 + // 规范如此(读 2 位即 `last_byte >> 6`)。用 LSB-first 解出来的 flags 会是 + // 0x1FE4 这类垃圾值。 + // + // 另外 raw 类型(RS/RL/RD)读取时若当前不在字节边界,需要把跨字节的位拼起来, + // 见 Byte() 的实现。 + struct BitReader + { + std::span Bytes; + std::size_t Bit = 0; // 已消费的位 + bool Good = true; + + bool Has(std::size_t bits) const + { + const std::size_t total = Bytes.size() * 8; + return Bit <= total && total - Bit >= bits; + } + // n 位(n<=8),先取高位 + std::uint8_t Bits(int count) + { + std::uint32_t value = 0; + for (int i = 0; i < count; ++i) + { + if (!Has(1)) { Good = false; return static_cast(value); } + const std::uint8_t bit = (Bytes[Bit >> 3] >> (7 - (Bit & 7))) & 1; + value = (value << 1) | bit; + ++Bit; + } + return static_cast(value); + } + std::uint8_t Byte() + { + if ((Bit & 7) == 0) + { + if (!Has(8)) { Good = false; return 0; } + const std::uint8_t value = Bytes[Bit >> 3]; + Bit += 8; + return value; + } + return Bits(8); // 跨字节:逐位拼 + } + std::uint16_t RawShort() + { + const std::uint16_t lo = Byte(), hi = Byte(); + return static_cast(lo | (hi << 8)); + } + std::int32_t RawLong() + { + std::uint32_t value = 0; + for (int i = 0; i < 4; ++i) value |= std::uint32_t(Byte()) << (8 * i); + return static_cast(value); + } + double RawDouble() + { + std::uint64_t bits = 0; + for (int i = 0; i < 8; ++i) bits |= std::uint64_t(Byte()) << (8 * i); + double value = 0; + if (Good) std::memcpy(&value, &bits, sizeof(value)); + return value; + } + // BS:2 位标记,0 = 16 位原码,1 = 8 位无符号,2 = 0,3 = 256 + std::int16_t BitShort() + { + switch (Bits(2)) + { + case 0: return static_cast(RawShort()); + case 1: return Byte(); + case 2: return 0; + default: return 256; + } + } + // BL:2 位标记,0 = 32 位原码,1 = 8 位无符号,2 = 0,3 未用(按 0 处理) + std::int32_t BitLong() + { + switch (Bits(2)) + { + case 0: return RawLong(); + case 1: return Byte(); + case 2: return 0; + default: return 0; + } + } + // BD:2 位标记,0 = 原码 double,1 = 1.0,2/3 = 0.0 + double BitDouble() + { + switch (Bits(2)) + { + case 0: return RawDouble(); + case 1: return 1.0; + default: return 0.0; + } + } + // DD:相对上一个值的**字节差分**编码(不是"沿用") + // 00 = 用默认值;01 = 4 字节覆盖低 4 字节; + // 10 = 6 字节(2 覆盖偏移 4-5、4 覆盖低 4);11 = 完整 RD + double BitDoubleWithDefault(double fallback) + { + std::uint8_t arr[8]; + std::memcpy(arr, &fallback, sizeof(arr)); + switch (Bits(2)) + { + case 0: return fallback; + case 1: + for (int i = 0; i < 4; ++i) arr[i] = Byte(); + break; + case 2: + arr[4] = Byte(); arr[5] = Byte(); + for (int i = 0; i < 4; ++i) arr[i] = Byte(); + break; + default: + return RawDouble(); + } + double value = 0; + std::memcpy(&value, arr, sizeof(value)); + return value; + } + }; + + inline bool AxisZ(const MiniDWG::XYZ& normal) + { + return std::abs(normal.X) < 1e-9 && std::abs(normal.Y) < 1e-9 && std::abs(normal.Z - 1) < 1e-9; + } + inline void CopyAppearance(const MiniDWG::Entity& source, MiniDWG::Entity& destination) + { + destination.BookColorHandle = source.BookColorHandle; + destination.Color = source.Color; + destination.IsInvisible = source.IsInvisible; + destination.LayerHandle = source.LayerHandle; + destination.LineTypeHandle = source.LineTypeHandle; + destination.LineTypeScale = source.LineTypeScale; + destination.LineWeight = source.LineWeight; + destination.MaterialHandle = source.MaterialHandle; + destination.Transparency = source.Transparency; + } + inline bool ArcThrough(const MiniDWG::XYZ& a, const MiniDWG::XYZ& b, const MiniDWG::XYZ& c, MiniDWG::XYZ& center, double& radius) + { + const double d = 2 * (a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)); + const double scale = std::max({std::hypot(b.X - a.X, b.Y - a.Y), std::hypot(c.X - a.X, c.Y - a.Y), 1e-300}); + if (std::abs(d) < 1e-12 * scale * scale) return false; + const double a2 = a.X * a.X + a.Y * a.Y, b2 = b.X * b.X + b.Y * b.Y, c2 = c.X * c.X + c.Y * c.Y; + center = {(a2 * (b.Y - c.Y) + b2 * (c.Y - a.Y) + c2 * (a.Y - b.Y)) / d, + (a2 * (c.X - b.X) + b2 * (a.X - c.X) + c2 * (b.X - a.X)) / d, a.Z}; + radius = std::hypot(a.X - center.X, a.Y - center.Y); + return std::isfinite(radius) && radius > 0; + } + inline std::unique_ptr SolidHatch(const std::vector>& loops) + { + auto hatch = std::make_unique(); + hatch->IsSolid = true; + hatch->Pattern.Name = "SOLID"; + hatch->PatternType = MiniDWG::HatchPatternType::SolidFill; + hatch->Elevation = loops.front().front().Z; + for (const auto& loop : loops) + { + MiniDWG::HatchBoundaryPath path; + path.Flags = MiniDWG::BoundaryPathFlags::Polyline; + auto edge = std::make_unique(); + edge->IsClosed = true; + for (const auto& p : loop) edge->Vertices.push_back({p.X, p.Y, 0}); + path.Edges.push_back(std::move(edge)); + hatch->Paths.push_back(std::move(path)); + } + return hatch; + } + } + + // 把一段代理图形记录流解成可显示的图元。返回是否解出了至少一个图元; + // 跳过的记录与丢弃的图元数量记在 output 的 Skipped / Discarded 里。 + inline bool Decode(const MiniDWG::UnknownEntity& source, ProxyGraphics& output, + std::string* diagnostic = nullptr, const ProxyGraphicsContext* context = nullptr); + + inline bool Decode(const MiniDWG::UnknownEntity& source, ProxyGraphics& output, + std::string* diagnostic, const ProxyGraphicsContext* context) + { + // 本文件不 using MiniDWG:调用方(如 CadExchange.cpp)也会引入 + // MiniCAD 自己的 Circle/Arc/TextEntity 等实体,只用 using 会被这些同名类型遮蔽, + // 导致 emit() 收到错误的 unique_ptr。所有类型一律写全限定名。 + output.Entities.clear(); + output.Skipped = 0; + output.Discarded = 0; + if (diagnostic) diagnostic->clear(); + auto fail = [&](const std::string& reason) + { + if (diagnostic) *diagnostic = reason; + return false; + }; + const auto& bytes = source.ProxyGraphics; + if (bytes.size() < 8) return fail("proxy header is shorter than 8 bytes"); + if (bytes.size() > 32 * 1024 * 1024) return fail("proxy exceeds 32 MiB compatibility limit"); + ProxyGraphics parsed; + MiniDWG::Color color = source.Color; + MiniDWG::Handle layer = source.LayerHandle, lineType = source.LineTypeHandle; + double lineTypeScale = source.LineTypeScale; + MiniDWG::LineWeightType lineWeight = source.LineWeight; + bool fill = false; + if (!std::isfinite(lineTypeScale) || lineTypeScale <= 0) return fail("invalid inherited linetype scale"); + + std::size_t offset = 8, records = 0, totalVertices = 0; + // 解出一条图元:外观属性随当前状态;坐标由各分支在生成时校验(Detail::Sane) + auto emit = [&](std::unique_ptr primitive) -> bool + { + Detail::CopyAppearance(source, *primitive); + primitive->Color = color; + primitive->LayerHandle = layer; + primitive->LineTypeHandle = lineType; + primitive->LineTypeScale = lineTypeScale; + primitive->LineWeight = lineWeight; + parsed.Entities.push_back(std::move(primitive)); + return true; + }; + // 单条记录解析失败时调用:记账后跳过,不放弃整份 + auto skip = [&]() { ++parsed.Skipped; }; + + while (offset < bytes.size()) + { + if (++records > 100000) { skip(); break; } + if (bytes.size() - offset < 8) { skip(); break; } + Detail::Reader header{std::span(bytes).subspan(offset, 8)}; + const auto size = header.Long(), type = header.Long(); + if (size < 8 || size > bytes.size() - offset) { skip(); break; } + const std::span payload = std::span(bytes).subspan(offset + 8, size - 8); + const bool advanced = [&]() -> bool + { + Detail::Reader r{payload}; + bool trailing = false; + switch (type) + { + case 1: // 包围盒:不产生图元 + if (r.Bytes.size() != 48) return false; + r.Point(); r.Point(); + break; + case 2: + case 4: + { + if (r.Bytes.size() != (type == 2 ? 56u : 88u) && !(type == 4 && r.Bytes.size() == 92)) + return false; + const auto center = r.Point(); + const double radius = r.Double(); + const auto normal = r.Point(); + if (!r.Good || radius <= 0 || !Detail::AxisZ(normal)) return false; + if (!Detail::Sane(center) || !Detail::Sane(radius)) { ++parsed.Discarded; break; } + if (type == 2) + { + auto circle = std::make_unique(); + circle->Center = center; circle->Radius = radius; circle->Normal = normal; + emit(std::move(circle)); + } + else + { + const auto start = r.Point(); + const double sweep = r.Double(); + if (!r.Good || std::abs(start.Z) > 1e-9 || std::hypot(start.X, start.Y) < 1e-12 || + sweep <= 0 || sweep >= 2 * MiniDWG::kPi) + return false; + if (r.Bytes.size() == 92 && r.Long() != 0) return false; + auto arc = std::make_unique(); + arc->Center = center; arc->Radius = radius; arc->Normal = normal; + arc->StartAngle = std::atan2(start.Y, start.X); + arc->EndAngle = arc->StartAngle + sweep; + emit(std::move(arc)); + } + break; + } + case 3: + case 5: + { + if (r.Bytes.size() != 72 && r.Bytes.size() != 76) return false; + const auto p1 = r.Point(), p2 = r.Point(), p3 = r.Point(); + if (r.Bytes.size() == 76 && r.Long() != 0) return false; + MiniDWG::XYZ center; double radius = 0; + if (!r.Good || std::abs(p1.Z - p2.Z) > 1e-9 || std::abs(p1.Z - p3.Z) > 1e-9 || + !Detail::ArcThrough(p1, p2, p3, center, radius)) + return false; + if (!Detail::Sane(center) || !Detail::Sane(radius)) { ++parsed.Discarded; break; } + if (type == 3) + { + auto circle = std::make_unique(); + circle->Center = center; circle->Radius = radius; + emit(std::move(circle)); + break; + } + const double a1 = std::atan2(p1.Y - center.Y, p1.X - center.X); + const double a3 = std::atan2(p3.Y - center.Y, p3.X - center.X); + const double cross = (p2.X - p1.X) * (p3.Y - p1.Y) - (p2.Y - p1.Y) * (p3.X - p1.X); + auto arc = std::make_unique(); + arc->Center = center; arc->Radius = radius; + arc->StartAngle = cross > 0 ? a1 : a3; + arc->EndAngle = cross > 0 ? a3 : a1; + if (arc->EndAngle <= arc->StartAngle) arc->EndAngle += 2 * MiniDWG::kPi; + emit(std::move(arc)); + break; + } + case 6: + case 7: + case 32: + { + if (r.Bytes.size() < 4) return false; + const auto count = r.Long(); + const std::size_t normalBytes = type == 32 ? 24 : 0; + if (count < (type == 7 ? 3u : 2u) || count > 1000000 || + count > (r.Bytes.size() - 4) / 24 || + r.Bytes.size() != 4 + std::size_t(count) * 24 + normalBytes || + totalVertices + count > 1000000) + return false; + totalVertices += count; + std::vector points; + points.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) + { + points.push_back(r.Point()); + if (!r.Good) return false; + if (std::abs(points.back().Z - points.front().Z) > 1e-9) return false; + } + if (type == 32 && !Detail::AxisZ(r.Point())) return false; + for (const auto& q : points) + if (!Detail::Sane(q)) { ++parsed.Discarded; return true; } + if (type == 7 && fill) + { + emit(Detail::SolidHatch({points})); + break; + } + auto polyline = std::make_unique(); + polyline->Vertices.reserve(count); + for (const auto& p : points) + { + MiniDWG::LwPolylineVertex vertex; vertex.Location = {p.X, p.Y}; + polyline->Vertices.push_back(vertex); + } + polyline->Elevation = points.front().Z; + if (type == 7) polyline->Flags = MiniDWG::LwPolylineFlags::Closed; + emit(std::move(polyline)); + break; + } + case 9: + { + const auto count = r.Long(); + if (!r.Good || count < 3 || count > (r.Bytes.size() - 4) / 24 || totalVertices + count > 1000000) + return false; + totalVertices += count; + std::vector vertices(count); + for (auto& v : vertices) v = r.Point(); + for (const auto& q : vertices) + if (!Detail::Sane(q)) { ++parsed.Discarded; return true; } + const auto entries = r.Long(); + if (!r.Good) return false; + if (entries > (r.Bytes.size() - r.Position) / 4) return false; + std::vector>> faces; // 面(外环 + 洞) + for (std::uint32_t read = 0; read < entries;) + { + const auto edges = static_cast(r.Long()); + const std::uint32_t n = static_cast(edges < 0 ? -std::int64_t(edges) : edges); + if (!r.Good || n < 3 || n > entries - read - 1) return false; + read += 1 + n; + std::vector loop; + for (std::uint32_t i = 0; i < n; ++i) + { + const auto index = r.Long(); + if (!r.Good || index >= count) return false; + loop.push_back(vertices[index]); + if (std::abs(loop.back().Z - loop.front().Z) > 1e-9) return false; + } + if (edges < 0 && !faces.empty()) faces.back().push_back(std::move(loop)); + else faces.push_back({std::move(loop)}); + } + for (auto& face : faces) + { + // fill 为真时**既出实心填充、也出边框轮廓**。 + // 以前这里 fill 时 `continue` 只出填充、不加轮廓线,于是文字类的 + // 背景框(`SWR_TEXT`,实测 2200×650)渲染成一个没有边框的纯白色块, + // 把画在上面的文字整个盖住——看起来就是"显示成了白色块状"。 + if (fill) emit(Detail::SolidHatch(face)); + for (const auto& loop : face) + { + auto polyline = std::make_unique(); + for (const auto& p : loop) + { + MiniDWG::LwPolylineVertex vertex; vertex.Location = {p.X, p.Y}; + polyline->Vertices.push_back(vertex); + } + polyline->Elevation = loop.front().Z; + polyline->Flags = MiniDWG::LwPolylineFlags::Closed; + emit(std::move(polyline)); + } + } + trailing = true; // 之后的边 / 面属性不影响二维显示 + break; + } + case 10: + case 11: + case 36: + case 38: + { + const bool unicode = type >= 36; + const auto position = r.Point(), normal = r.Point(), direction = r.Point(); + double height = 0, width = 1; + std::string value, font, bigFont; + if (type == 10 || type == 36) + { + height = r.Double(); width = r.Double(); r.Double(); + value = unicode ? r.PaddedUnicodeString() : r.PaddedString(); + } + else + { + // type 11 / 38 的布局: + // [72] 字符串(NUL 结尾、补到 4 字节对齐) + // 然后 8 字节「字符串长度 + raw 标志」,接着 height / widthFactor / oblique。 + value = unicode ? r.PaddedUnicodeString() : r.PaddedString(); + r.Long(); r.Long(); + height = r.Double(); width = r.Double(); r.Double(); r.Double(); + for (int i = 0; i < 5; ++i) r.Long(); + font = unicode ? r.PaddedUnicodeString() : r.PaddedString(); + bigFont = unicode ? r.PaddedUnicodeString() : r.PaddedString(); + } + if (!r.Good || !Detail::AxisZ(normal) || !(height > 0) || + std::hypot(direction.X, direction.Y) < 1e-12) + return false; + if (!Detail::Sane(position) || !Detail::Sane(height)) { ++parsed.Discarded; break; } + auto text = std::make_unique(); + text->InsertPoint = position; + text->Height = height; + text->WidthFactor = width > 0 ? width : 1; + text->Rotation = std::atan2(direction.Y, direction.X); + text->Value = MiniDWG::Codec::DecodeUnicodeEscapes(unicode ? value + : MiniDWG::Codec::ToUtf8(value, context ? context->CodePage : MiniDWG::Codec::CodePage::Gbk)); + if (context && context->StyleForFont) + text->StyleHandle = context->StyleForFont(font, bigFont); + trailing = true; // 有的写法在末尾多补对齐字节 + if (!text->Value.empty()) emit(std::move(text)); + break; + } + case 14: + { + if (r.Bytes.size() != 4) return false; + const auto aci = r.Long(); + if (aci > 256) return false; + color = MiniDWG::Color(static_cast(aci)); + break; + } + case 16: + { + if (r.Bytes.size() != 4) return false; + const auto index = r.Long(); + if (index == 0xFFFFFFFFu) layer = source.LayerHandle; + else if (!context || index >= context->Layers.size()) return false; + else layer = context->Layers[index]; + break; + } + case 18: + { + if (r.Bytes.size() != 4) return false; + const auto index = r.Long(); + if (index == 0xFFFFFFFFu) lineType = source.LineTypeHandle; + else if (!context) return false; + else if (index == 32767) lineType = context->ByLayerLineType; + else if (index == 32766) lineType = context->ByBlockLineType; + else if (index < context->LineTypes.size()) lineType = context->LineTypes[index]; + else return false; + break; + } + case 22: + { + if (r.Bytes.size() != 4) return false; + // 颜色记录的 4 字节有两种写法,都要认: + // a) 字节序 [method, r, g, b]:实测句柄 368F5 的 + // `c2 00 00 00` = 真彩 RGB(0,0,0)、`c3 00 00 07` = ACI 7; + // b) 小端 u32 且 method 在高字节(如 0xC2FF8000)。 + // 先看低字节是不是合法 method,不是再看高字节。以前只认 b),a) 形态下 + // `>> 24` 恒为 0,颜色全部落到 ByLayer 兜底。 + const std::uint32_t raw = r.Long(); + const auto lo = static_cast(raw & 0xFFu); + if (lo == 0xc0 || lo == 0xc1 || lo == 0xc2 || lo == 0xc3) + { + const auto c1 = static_cast((raw >> 8) & 0xFFu); + const auto c2 = static_cast((raw >> 16) & 0xFFu); + const auto c3 = static_cast((raw >> 24) & 0xFFu); + if (lo == 0xc0) color = MiniDWG::Color::ByLayer(); + else if (lo == 0xc1) color = MiniDWG::Color::ByBlock(); + else if (lo == 0xc2) + color = MiniDWG::Color::FromTrueColor((std::uint32_t(c3) << 16) | (std::uint32_t(c2) << 8) | c1); + else color = MiniDWG::Color(static_cast(c3)); + break; + } + const auto method = raw >> 24; + if (method == 0xc0) color = MiniDWG::Color::ByLayer(); + else if (method == 0xc1) color = MiniDWG::Color::ByBlock(); + else if (method == 0xc2) color = MiniDWG::Color::FromTrueColor(raw); + else if (method == 0xc3 && (raw & 0xffffff) <= 255) color = MiniDWG::Color(static_cast(raw & 255)); + else return false; + break; + } + case 19: + if (r.Bytes.size() != 4) return false; + r.Long(); + break; + case 20: + if (r.Bytes.size() != 4) return false; + fill = r.Long() != 0; + break; + case 23: + { + if (r.Bytes.size() != 4) return false; + const auto weight = static_cast(r.Long()); + switch (weight) + { + case -3: case -2: case -1: case 0: case 5: case 9: case 13: case 15: case 18: + case 20: case 25: case 30: case 35: case 40: case 50: case 53: case 60: + case 70: case 80: case 90: case 100: case 106: case 120: case 140: case 158: case 200: case 211: + lineWeight = static_cast(weight); + break; + default: return false; + } + break; + } + case 24: + if (r.Bytes.size() != 8) return false; + lineTypeScale = r.Double(); + if (!r.Good || lineTypeScale <= 0) return false; + break; + case 25: + // 厚度:只接受 0(非零厚度本预览不建模,跳过该条状态记录即可) + if (r.Bytes.size() != 8) return false; + r.Double(); + break; + case 33: + { + // LWPOLYLINE:顶点与凸度按**位字段**编码,payload 前 4 字节是长度前缀, + // 其后才是位流。 + // 实测句柄 1863E6(标高标注的实心倒三角箭头): + // flags=0x0000、7 个顶点,解出 (-148976.3,43026.2)…(-149216.3,43026.2), + // 与同对象 type 6 折线的坐标同一区域。 + if (r.Bytes.size() < 4) return false; + const auto dataBytes = static_cast(r.Long()); + if (!r.Good || dataBytes < 4 || dataBytes > r.Bytes.size() - 4) return false; + Detail::BitReader b{r.Bytes.subspan(4, dataBytes)}; + const auto flags = static_cast(b.BitShort()); + double constantWidth = 0.0, elevation = 0.0, thickness = 0.0; + MiniDWG::XYZ extrusion{0, 0, 1}; + if (flags & 0x4) constantWidth = b.BitDouble(); + if (flags & 0x8) elevation = b.BitDouble(); + if (flags & 0x2) thickness = b.BitDouble(); + if (flags & 0x1) + { + extrusion.X = b.BitDouble(); + extrusion.Y = b.BitDouble(); + extrusion.Z = b.BitDouble(); + } + // 顶点数上限按实际字节数推导,不用魔数 + const std::int32_t maxPoints = static_cast(b.Bytes.size() / 8); + const auto pointCount = b.BitLong(); + const auto bulgeCount = (flags & 0x10) ? b.BitLong() : 0; + if (!b.Good || pointCount < 2 || pointCount > maxPoints || + bulgeCount < 0 || bulgeCount > pointCount || + totalVertices + std::size_t(pointCount) > 1000000) + return false; + totalVertices += std::size_t(pointCount); + std::vector points; + points.reserve(std::size_t(pointCount)); + MiniDWG::XYZ p{b.RawDouble(), b.RawDouble(), elevation}; + points.push_back(p); + for (std::int32_t i = 1; i < pointCount; ++i) + { + const double x = b.BitDoubleWithDefault(p.X); + const double y = b.BitDoubleWithDefault(p.Y); + p = {x, y, elevation}; + points.push_back(p); + } + std::vector bulges(points.size(), 0.0); + for (std::int32_t i = 0; i < bulgeCount; ++i) bulges[std::size_t(i)] = b.BitDouble(); + if (!b.Good || !Detail::AxisZ(extrusion)) return false; + for (const auto& q : points) + if (!Detail::Sane(q)) return false; + const bool closed = (flags & 0x200) != 0; + if (fill && closed) + { + emit(Detail::SolidHatch({points})); + break; + } + auto polyline = std::make_unique(); + polyline->Vertices.reserve(points.size()); + for (std::size_t i = 0; i < points.size(); ++i) + { + MiniDWG::LwPolylineVertex vertex; + vertex.Location = {points[i].X, points[i].Y}; + vertex.Bulge = bulges[i]; + polyline->Vertices.push_back(vertex); + } + polyline->Elevation = elevation; + polyline->ConstantWidth = constantWidth; + polyline->Thickness = thickness; + if (closed) polyline->Flags = MiniDWG::LwPolylineFlags::Closed; + trailing = true; + emit(std::move(polyline)); + break; + } + default: + return false; // 不认识的记录:交给外层跳过 + } + if (trailing ? !r.Good : !r.Done()) return false; + return true; + }(); + if (!advanced) ++parsed.Skipped; + offset += size; + } + // 统计信息即使在没有可显示图元时也要带回去,调用方才能判断「是没解出几何」 + // 还是「几何都被丢弃了」 + const std::size_t skippedCount = parsed.Skipped; + const std::size_t discardedCount = parsed.Discarded; + if (parsed.Entities.empty()) + { + output.Skipped = skippedCount; + output.Discarded = discardedCount; + return fail("proxy contains no supported drawable geometry (skipped " + + std::to_string(skippedCount) + " records, discarded " + + std::to_string(discardedCount) + " primitives)"); + } + output = std::move(parsed); + return true; + } +} diff --git a/src/Import/TchAnnotation.h b/src/Import/TchAnnotation.h index 2e90f20..209ccbe 100644 --- a/src/Import/TchAnnotation.h +++ b/src/Import/TchAnnotation.h @@ -593,7 +593,7 @@ namespace MiniCAD::Tch auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; if (entity.Raw.DxfName != "TCH_NORTHTHUMB") return false; if (Detail::IsCipherObject(data)) return Detail::DecodeNorthThumbCipher(entity, north, diagnostic); - if (data.Version < CadVersion::AC1021 || data.Main.size() < (data.MainBits + 7) / 8) return fail("unsupported DWG version for north thumb"); + if (data.Version < CadVersion::AC1018 || data.Main.size() < (data.MainBits + 7) / 8) return fail("unsupported DWG version for north thumb"); DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); TchBase base; if (!ReadTchBase(r, base, &data)) return fail("unsupported Tianzheng base layout"); @@ -611,7 +611,16 @@ namespace MiniCAD::Tch n.Color = r.ReadBitShort(); r.ReadRawDouble(); r.ReadRawDouble(); } - if (r.Failed() || r.PositionInBits() != data.MainBits) return fail("native north thumb fields do not consume the payload exactly"); + // 部分 T20 版本(V2~V7 / t10)在末尾补了若干 0 位(≤ 34 位),与 t8 的布局相同, + // 只是主数据流多了一段全零填充。允许这段尾部,其余字段已全部读出。 + if (r.Failed()) return fail("native north thumb fields are incomplete"); + if (r.PositionInBits() != data.MainBits) + { + const auto start = r.PositionInBits(); + if (data.MainBits < start || data.MainBits - start > 64) return fail("native north thumb fields do not consume the payload exactly"); + for (auto i = start; i < data.MainBits; ++i) + if (r.ReadBit()) return fail("native north thumb trailing bits are not zero"); + } if (const char* why = Detail::CheckNorthThumb(n)) return fail(why); if (version > 1) { diff --git a/src/Import/TchCurveMesh.h b/src/Import/TchCurveMesh.h new file mode 100644 index 0000000..2f08942 --- /dev/null +++ b/src/Import/TchCurveMesh.h @@ -0,0 +1,109 @@ +#pragma once +#include "Import/TchShape.h" +#include +#include + +namespace MiniCAD::Tch +{ + // 曲面网格(TCH_CURVEMESH2):代理图形为空,几何在私有数据里。 + // 两个家族:加密(基类 9~14,原始图)与明文(基类 2~8,t7 等)。 + // 两者顶点都是明文 BD 三点组(X, Y, Z),从一个位起点起连续合理到固定末尾; + // 这里用「找最长的连续合理三点组」定位顶点数组,再按顺序连成折线显示。 + struct TchCurveMesh + { + std::vector Points; + int Version = 0; + }; + + namespace Detail + { + inline bool CheckCurveMesh(const TchCurveMesh& m) + { + if (m.Points.size() < 3 || m.Points.size() > 100000) return false; + for (const auto& p : m.Points) + if (!std::isfinite(p.X) || !std::isfinite(p.Y) || !std::isfinite(p.Z) || + std::abs(p.X) > 1e9 || std::abs(p.Y) > 1e9 || std::abs(p.Z) > 1e9) return false; + return true; + } + + // 从 scanFrom 起,找最长的连续合理 (X,Y,Z) BD 三点组序列 + inline std::vector ScanMeshVertices(const MiniDWG::RawDwgData& data, + unsigned long long scanFrom, + unsigned long long scanWindow) + { + std::vector best; + for (unsigned long long t = scanFrom; t < scanFrom + scanWindow && t < data.MainBits; ++t) + { + MiniDWG::DwgBitReader r(data.Main, data.Version, MiniDWG::Codec::CodePage::Gbk); + r.SetPositionInBits(t); + std::vector pts; + while (r.PositionInBits() + 6 <= data.MainBits) + { + const auto before = r.PositionInBits(); + const double x = r.ReadBitDouble(); + const double y = r.ReadBitDouble(); + const double z = r.ReadBitDouble(); + if (r.Failed()) break; + const bool ok = std::isfinite(x) && std::isfinite(y) && std::isfinite(z) && + std::abs(x) < 1e6 && std::abs(y) < 1e6 && std::abs(z) < 1e6 && + (std::abs(x) > 1000 || std::abs(y) > 1000); + if (!ok) + { + r.SetPositionInBits(before); + break; + } + pts.push_back({x, y, z}); + } + if (pts.size() > best.size()) best = std::move(pts); + } + return best; + } + } + + inline bool DecodeCurveMesh(const MiniDWG::UnknownEntity& entity, TchCurveMesh& mesh, std::string* diagnostic = nullptr) + { + using namespace MiniDWG; + auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; + const auto& data = entity.Raw.Dwg; + if (entity.Raw.DxfName != "TCH_CURVEMESH2") return false; + if (data.Main.empty() || data.Main.size() < (data.MainBits + 7) / 8) + return fail("unsupported TCH_CURVEMESH2 layout"); + TchCurveMesh m; + unsigned long long scanFrom = 0; + if (Detail::IsCipherObject(data)) + { + Detail::CipherReader f(entity); + if (!f.ReadBase()) return fail("unsupported Tianzheng curve mesh base layout"); + m.Version = f.Main.ReadByte(); + f.ReadKey(); + scanFrom = f.Main.PositionInBits(); + } + else + { + if (!Detail::SupportedVersion(data)) return fail("unsupported DWG version for curve mesh"); + DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); + TchBase base; + if (!ReadTchBase(r, base, &data)) return fail("unsupported Tianzheng curve mesh base layout"); + m.Version = r.ReadByte(); + scanFrom = r.PositionInBits(); + } + if (scanFrom + 192 > data.MainBits) return fail("curve mesh payload is incomplete"); + auto pts = Detail::ScanMeshVertices(data, scanFrom, 640); + if (pts.size() < 3) return fail("native curve mesh vertices are not recognized"); + m.Points = std::move(pts); + if (!Detail::CheckCurveMesh(m)) return fail("invalid native curve mesh"); + mesh = std::move(m); + if (diagnostic) diagnostic->clear(); + return true; + } + + // 曲面网格的平面显示:把顶点按顺序连成折线。 + inline TchGraphics BuildCurveMesh(const TchCurveMesh& m) + { + TchGraphics g; + TchGraphics::Path p; + for (const auto& pt : m.Points) p.Points.push_back({pt.X, pt.Y}); + if (p.Points.size() >= 2) g.Paths.push_back(std::move(p)); + return g; + } +} diff --git a/src/Import/TchDimension.h b/src/Import/TchDimension.h index 7898bb0..dedcfee 100644 --- a/src/Import/TchDimension.h +++ b/src/Import/TchDimension.h @@ -116,14 +116,19 @@ namespace MiniCAD::Tch { using namespace MiniDWG; const auto& data = entity.Raw.Dwg; - if (entity.Raw.DxfName != "TCH_DIMENSION2") return false; + if (entity.Raw.DxfName != "TCH_DIMENSION2" && entity.Raw.DxfName != "TCH_DIMENSION") return false; if (Detail::IsCipherObject(data)) return Detail::DecodeDimensionCipher(entity, dimension, diagnostic); auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; if (!Detail::SupportedVersion(data)) return fail("unsupported DWG version for native dimension"); DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); TchBase base; if (!ReadTchBase(r, base, &data)) return fail("unsupported Tianzheng base layout"); - if (r.ReadByte() != 1) return fail("unsupported native dimension version"); + // TCH_DIMENSION2 只认类版本 1(版本 2 的字段布局未核对,不能猜); + // 老类 TCH_DIMENSION(t5/t6)字段序列相同、类版本号为 3。 + const bool oldClass = entity.Raw.DxfName == "TCH_DIMENSION"; + const int dimVersion = r.ReadByte(); + if (oldClass ? (dimVersion != 3) : (dimVersion != 1)) + return fail("unsupported native dimension version"); Detail::TextStream text(data, r); for (int i = 0; i < base.TextCount; ++i) text.Next(); @@ -583,13 +588,44 @@ namespace MiniCAD::Tch const P tip{d.TipX, d.TipY}; auto at = [&](double t, double o) { return P{tip.X + ux * t - uy * o, tip.Y + uy * t + ux * o}; }; const double arrow = std::min(2 * style.ArrowSize * s, length); + + // 标志 0x10:直径标注(天正同一个 TDbRadiusDim 类承载直径与半径两种)。 + // 直径的画法与半径不同:一条直径线贯穿圆心,两端各一个箭头指向圆周内壁, + // 文字水平放在圆心上方的线中央(天正默认样式,见 .agents/features/tch.md)。 + const bool isDiameter = (d.Flags & 0x10) != 0; + const double height = style.TextHeight * 0.85 * s; + if (isDiameter) + { + const P cx{d.Center.X, d.Center.Y}; + // 以圆心为基准沿 Center→Tip 方向定位:t 为沿直径方向的距离,o 为垂直偏移 + auto along = [&](double t, double o) { return P{cx.X + ux * t - uy * o, cx.Y + uy * t + ux * o}; }; + const P end1 = along(radius, 0), end2 = along(-radius, 0); + const double rArrow = std::min(2 * style.ArrowSize * s, radius); + // 直径线:两端各缩进一个箭头长 + if (2 * radius > 2 * rArrow) g.Lines.push_back({along(-radius + rArrow, 0), along(radius - rArrow, 0)}); + if (rArrow > 0) + { + g.Arrows.push_back({along(radius - rArrow, 0.165 * rArrow), along(radius - rArrow, -0.165 * rArrow), end1}); + g.Arrows.push_back({along(-radius + rArrow, 0.165 * rArrow), along(-radius + rArrow, -0.165 * rArrow), end2}); + } + std::string value = (d.Flags & 4) ? d.TextOverride + : std::string("\xE2\x8C\x80") + Detail::FormatNumber(2 * radius, style.Decimals); + if (d.HasTextPosition) + g.Texts.push_back({{d.TextX, d.TextY}, height, 0, std::move(value), 5, 0, style.TextColor}); + else + { + // 文字水平放在圆心上方的直径线中央(天正默认样式) + const double lift = height / 2 + style.Gap * s; + g.Texts.push_back({{cx.X, cx.Y + lift}, height, 0, std::move(value), 5, 0, style.TextColor}); + } + return g; + } + if (length > arrow) g.Lines.push_back({at(arrow, 0), at(length, 0)}); if (arrow > 0) g.Arrows.push_back({at(arrow, 0.165 * arrow), at(arrow, -0.165 * arrow), tip}); - std::string value = (d.Flags & 4) ? d.TextOverride : "R" + Detail::FormatNumber(radius, style.Decimals); double rotation = std::atan2(uy, ux); const bool flipped = Detail::Upright(rotation); - const double height = style.TextHeight * 0.85 * s; if (d.HasTextPosition) g.Texts.push_back({{d.TextX, d.TextY}, height, rotation, std::move(value), 5, 0, style.TextColor}); else diff --git a/src/Import/TchDormer.h b/src/Import/TchDormer.h new file mode 100644 index 0000000..f77aad0 --- /dev/null +++ b/src/Import/TchDormer.h @@ -0,0 +1,130 @@ +#pragma once +#include "Import/TchShape.h" +#include +#include + +namespace MiniCAD::Tch +{ + // 老虎窗(TCH_DORMER):代理图形是空的(只有 8 字节头),几何必须从私有数据解。 + // 明文(基类 3~8)与 T30 加密(基类 9~14)两个家族的字段语义一致但编码不同: + // 明文:ReadTchBase + version(2) + BS(1) + 10 个 BD 参数 + 6 个 BD + // (插入点 X/Y/Z,然后屋面法向 X/Y/Z) + // 加密:ReadBase + version(3) + ReadKey + 11 个 Double(第 1 个=1,其余 10 个参数) + // + P3(插入点,按 Key 重排为 X/Y/Z)+ P3(屋面法向) + // 各版本(t6~t20V9)这 10 个参数实测完全相同: + // 200, 200, 0.5, 450, 200, 1500, 180, 1800, 1300, 1400; + // 插入点 X=61689.23, Y=29421.88, Z=744.23;屋面法向 (-0.7656, 0, 0.6433),模长 1。 + // 平面朝向取法向在 XY 平面的方位角 atan2(nrm.Y, nrm.X)——实测该值(180°)让老虎窗 + // 背靠最近的竖直墙(靠墙角距墙 134,背离侧 1666),与图纸布局自洽; + // 以前把法向的 X 分量(-0.7656)当旋转角用,方向差约 136°。 + struct TchDormer + { + double X = 0, Y = 0, Rotation = 0; + double Z = 0; // 插入点标高 + double NX = 0, NY = 0, NZ = 1; // 屋面法向(单位向量) + double Width = 0, Depth = 0; // 取自主尺寸参数(1800 / 1400) + double Params[10] = {}; // 10 个原始参数,保留供后续几何校准 + int Version = 0; + }; + + namespace Detail + { + inline bool CheckDormer(const TchDormer& d) + { + const std::array values{d.X, d.Y, d.Z, d.NX, d.NY, d.NZ, d.Rotation, d.Width, d.Depth}; + for (double v : values) + if (!std::isfinite(v) || std::abs(v) > 1e9) return false; + for (double v : d.Params) + if (!std::isfinite(v) || std::abs(v) > 1e9) return false; + return d.Width > 0 && d.Depth > 0; + } + + inline void FinishDormer(TchDormer& d) + { + d.Width = d.Params[7]; // 1800 + d.Depth = d.Params[9]; // 1400 + // 平面朝向取屋面法向在 XY 平面的方位角;法向在 XY 上没有分量时退回 0 + const double len = std::hypot(d.NX, d.NY); + d.Rotation = len > 1e-9 ? std::atan2(d.NY, d.NX) : 0.0; + } + } + + inline bool DecodeDormer(const MiniDWG::UnknownEntity& entity, TchDormer& dormer, std::string* diagnostic = nullptr) + { + using namespace MiniDWG; + auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; + const auto& data = entity.Raw.Dwg; + if (entity.Raw.DxfName != "TCH_DORMER") return false; + if (data.Main.empty() || data.Main.size() < (data.MainBits + 7) / 8) return fail("dormer payload is incomplete"); + if (Detail::IsCipherObject(data)) + { + Detail::CipherReader f(entity); + if (!f.ReadBase()) return fail("unsupported Tianzheng dormer base layout"); + TchDormer d; + d.Version = f.Main.ReadByte(); + f.ReadKey(); + const double flag = f.Double(); // 1(与明文的 BS 对应) + if (!std::isfinite(flag) || flag < 0 || flag > 8) return fail("unsupported native dormer version"); + for (int i = 0; i < 10 && f.Ok(); ++i) d.Params[i] = f.Double(); + const auto pos = f.Point3(); // 插入点 X / Y / Z + const auto nrm = f.Point3(); // 屋面法向(单位向量) + d.X = pos.X; d.Y = pos.Y; d.Z = pos.Z; + d.NX = nrm.X; d.NY = nrm.Y; d.NZ = nrm.Z; + if (!f.Ok()) return fail("native dormer fields are incomplete"); + Detail::FinishDormer(d); + if (!Detail::CheckDormer(d)) return fail("invalid native dormer settings"); + dormer = std::move(d); + if (diagnostic) diagnostic->clear(); + return true; + } + if (!Detail::SupportedVersion(data)) return fail("unsupported DWG version for native dormer"); + DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); + TchBase base; + if (!ReadTchBase(r, base, &data)) return fail("unsupported Tianzheng dormer base layout"); + TchDormer d; + d.Version = r.ReadByte(); + const int flag = r.ReadBitShort(); // 1 + if (flag < 0 || flag > 8) return fail("unsupported native dormer version"); + for (int i = 0; i < 10 && !r.Failed(); ++i) d.Params[i] = r.ReadBitDouble(); + d.X = r.ReadBitDouble(); d.Y = r.ReadBitDouble(); d.Z = r.ReadBitDouble(); + d.NX = r.ReadBitDouble(); d.NY = r.ReadBitDouble(); d.NZ = r.ReadBitDouble(); + if (r.Failed()) return fail("native dormer fields are incomplete"); + Detail::FinishDormer(d); + if (!Detail::CheckDormer(d)) return fail("invalid native dormer settings"); + dormer = std::move(d); + if (diagnostic) diagnostic->clear(); + return true; + } + + // 老虎窗的平面示意:局部坐标以插入点为原点,+u 为坡向(法向在 XY 的方位), + // +v 为墙面方向。窗洞是矩形(宽 Width 沿 v、进深 Depth 沿 u),在背离墙的一侧 + // 伸出屋面板(由坡度 Params[2] 与屋面高 Params[5] 决定伸出量),两侧坡线收成脊。 + // 参数化近似;位置、朝向、尺寸都来自实测数据。 + inline TchGraphics BuildDormer(const TchDormer& d) + { + TchGraphics g; + const double hw = d.Width / 2; // 沿 v(墙面方向)的半宽 + const double hd = d.Depth / 2; // 沿 u(坡向)的半深 + auto at = [&](double u, double v) { return Detail::Local(d.X, d.Y, d.Rotation, u, v); }; + // 窗洞矩形 + TchGraphics::Path outline; + for (auto [u, v] : {std::pair{-hd, -hw}, {hd, -hw}, {hd, hw}, {-hd, hw}, {-hd, -hw}}) + outline.Points.push_back({at(u, v)}); + g.Paths.push_back(std::move(outline)); + // 伸出量:坡度(Params[2])越大伸出越远;没有可用坡度时按半个进深 + const double slope = d.Params[2] > 0.01 && d.Params[2] < 10.0 ? d.Params[2] : 0.5; + const double reach = hd * (1.0 + slope); + auto line = [&](double u0, double v0, double u1, double v1) { + TchGraphics::Path p; + p.Points.push_back({at(u0, v0)}); + p.Points.push_back({at(u1, v1)}); + g.Paths.push_back(std::move(p)); + }; + // 中脊线(沿坡向) + line(-hd, 0, reach, 0); + // 两条坡屋面线:从窗洞两前角收向脊端 + line(hd, -hw, reach, 0); + line(hd, hw, reach, 0); + return g; + } +} diff --git a/src/Import/TchOpening.h b/src/Import/TchOpening.h index fa910fa..dc6e3fa 100644 --- a/src/Import/TchOpening.h +++ b/src/Import/TchOpening.h @@ -94,27 +94,34 @@ namespace MiniCAD::Tch } } - // 版本 1~5 为旧版明文;版本 7(中望看图另存的 2018 格式)在版本 5 之后加了属性表,句柄个数分别多存了 5、3,要传 data + // 版本 1~5 为旧版明文;版本 6~8(天正 T20 系列,按 t20V2~V7 的图纸核对)在 + // 版本 5 之后加了属性表,要传 data。三者骨架相同,差别见 bias 与版本 8 的 GUID。 inline bool ReadTchBase(MiniDWG::DwgBitReader& r, TchBase& base, const MiniDWG::RawDwgData* data = nullptr) { base = {}; base.Version = r.ReadByte(); - if (base.Version < 1 || base.Version > 7 || base.Version == 6 || (base.Version == 7 && !data)) + const bool t20 = base.Version >= 6 && base.Version <= 8; + if (base.Version < 1 || base.Version > 8 || (t20 && !data)) return false; if (base.Version > 1) base.Scale = r.ReadBitDouble(); if (base.Version > 2) r.ReadBitDouble(); if (base.Version > 3) { const int flags = r.ReadByte(); - const int bias = base.Version == 7 ? 5 : 0; + // 句柄个数的偏移:版本 6 不减、7/8 各减 5(T20 系列,按 t20V2~V7 核对)。 + // 版本 6 若也减 5 会得到负数句柄数,柱 / 墙等带句柄的类整个解码失败。 + const int bias = base.Version >= 7 ? 5 : 0; if (flags & 1) base.HandleCount += r.ReadByte() - bias; if (flags & 2) base.HandleCount += r.ReadByte() - (bias ? 3 : 0); if (base.HandleCount < 0) return false; } if (base.Version > 4 && !Detail::SkipPropertyBag(r, base.TextCount, base.HandleCount)) return false; - if (base.Version == 7 && !Detail::SkipXDataMap(r, *data, base)) + if (t20 && !Detail::SkipXDataMap(r, *data, base)) return false; + // 版本 8(T20 V7)在属性表之后多存一个 GUID 字符串(内联的变长文本) + if (base.Version == 8) + r.ReadVariableText(); return !r.Failed(); } @@ -296,7 +303,7 @@ namespace MiniCAD::Tch for (const char* c = kPrefix; *c; ++c) if (r.ReadBit() != (*c == '1')) return fail("unsupported group opening part header"); TchBase partBase; - if (!ReadTchBase(r, partBase)) return fail("unsupported group opening part base layout"); + if (!ReadTchBase(r, partBase, &data)) return fail("unsupported group opening part base layout"); for (int k = 0; k < partBase.TextCount; ++k) text.Next(); TchOpening part; int partKind = 0; diff --git a/src/Import/TchProxyGraphics.h b/src/Import/TchProxyGraphics.h index 2a7a277..f0b0a59 100644 --- a/src/Import/TchProxyGraphics.h +++ b/src/Import/TchProxyGraphics.h @@ -1,559 +1,36 @@ #pragma once -#include "Database/UnknownObjects.h" -#include "Codec/CodePage.h" -#include "Dwg/Read/DwgBitReader.h" -#include -#include -#include -#include -#include +// 天正(TCH_*)自定义实体的解码入口。 +// +// 代理图形本身与厂商无关,通用解码器在 `Import/ProxyGraphics.h`(命名空间 +// MiniCAD::Proxy)。本文件只保留天正这一侧的入口,做两件事: +// 1. 把通用的 ProxyGraphics 结果转成 TchProxyGraphics(历史类型名,供调用方沿用); +// 2. 记录天正特有的取值约定。 +// +// 天正私有主数据流(墙体、门窗、楼梯、标注等语义)不在这里,见同目录的 +// `TchWall.h`、`TchOpening.h`、`TchShape.h` 等。 +// +// 历史说明:这个文件以前同时承担「通用代理图形解码器」的职责,2026-11 把它拆成 +// 了 ProxyGraphics.h;现在 Tch::DecodeProxyGraphics 只是通用解码器的薄封装。 + +#include "Import/ProxyGraphics.h" #include -#include -#include namespace MiniCAD::Tch { - struct TchProxyGraphics - { - std::vector> Entities; - std::size_t Skipped = 0; // 跳过的记录(不支持的类型、数据不合理的记录) - }; - - // 记录里的图层 / 线型是表内序号,文字只给字体文件名:由调用方按图纸提供映射 - struct ProxyGraphicsContext - { - std::vector Layers; // 图层表的表项顺序 - std::vector LineTypes; // 线型表的表项顺序 - MiniDWG::Handle ByLayerLineType = MiniDWG::kNullHandle; - MiniDWG::Handle ByBlockLineType = MiniDWG::kNullHandle; - std::function StyleForFont; - MiniDWG::Codec::CodePage CodePage = MiniDWG::Codec::CodePage::Gbk; - }; + // 天正对象解出的代理图形图元(历史类型名,等价于 Proxy::ProxyGraphics 的实体表) + using TchProxyGraphics = Proxy::ProxyGraphics; - namespace ProxyDetail - { - struct Reader - { - std::span Bytes; - std::size_t Position = 0; - bool Good = true; - std::uint32_t Long() - { - if (Bytes.size() - Position < 4) { Good = false; return 0; } - std::uint32_t value = 0; - for (int i = 0; i < 4; ++i) value |= std::uint32_t(Bytes[Position++]) << (8 * i); - return value; - } - double Double() - { - if (Bytes.size() - Position < 8) { Good = false; return 0; } - std::uint64_t bits = 0; - for (int i = 0; i < 8; ++i) bits |= std::uint64_t(Bytes[Position++]) << (8 * i); - double value; - std::memcpy(&value, &bits, sizeof(value)); - if (!std::isfinite(value)) Good = false; - return value; - } - MiniDWG::XYZ Point() { const double x = Double(), y = Double(), z = Double(); return {x, y, z}; } - void Align() { Position = std::min(Bytes.size(), (Position + 3) & ~std::size_t(3)); } - std::string PaddedString() - { - const std::size_t start = Position; - while (Position < Bytes.size() && Bytes[Position] != 0) ++Position; - if (Position >= Bytes.size()) { Good = false; return {}; } - std::string value(reinterpret_cast(Bytes.data() + start), Position - start); - ++Position; - Align(); - return value; - } - std::string PaddedUnicodeString() - { - std::string value; - for (;;) - { - if (Bytes.size() - Position < 2) { Good = false; return {}; } - char32_t unit = Bytes[Position] | (char32_t(Bytes[Position + 1]) << 8); - Position += 2; - if (unit == 0) break; - if (unit >= 0xd800 && unit < 0xdc00 && Bytes.size() - Position >= 2) - { - const char32_t low = Bytes[Position] | (char32_t(Bytes[Position + 1]) << 8); - if (low >= 0xdc00 && low < 0xe000) - { - Position += 2; - unit = 0x10000 + ((unit - 0xd800) << 10) + (low - 0xdc00); - } - } - MiniDWG::Codec::AppendUtf8(value, unit); - } - Align(); - return value; - } - bool Done() const { return Good && Position == Bytes.size(); } - }; - - inline bool AxisZ(const MiniDWG::XYZ& normal) - { - return std::abs(normal.X) < 1e-9 && std::abs(normal.Y) < 1e-9 && std::abs(normal.Z - 1) < 1e-9; - } - // 坐标量级上限:超出的记录当作坏数据跳过,免得一条坏记录把整张图的范围撑到无穷大 - inline bool Sane(double v) { return std::isfinite(v) && std::abs(v) < 1e12; } - inline bool Sane(const MiniDWG::XYZ& p) { return Sane(p.X) && Sane(p.Y) && Sane(p.Z); } - - // 记录 22 的 4 字节:通常是小端 uint32、颜色方法在最高字节([b, g, r, method]); - // 也有写成 [method, r, g, b] 的。先按前者认,最高字节不是颜色方法时再按后者 - inline bool RawColor(std::uint32_t value, MiniDWG::Color& color) - { - auto isMethod = [](std::uint32_t m) { return m >= 0xc0 && m <= 0xc3; }; - std::uint32_t method = value >> 24, rgb = value & 0xffffff; - if (!isMethod(method) && isMethod(value & 0xff)) - { - method = value & 0xff; - rgb = ((value >> 8) & 0xff) << 16 | ((value >> 16) & 0xff) << 8 | (value >> 24); - } - if (method == 0xc0) color = MiniDWG::Color::ByLayer(); - else if (method == 0xc1) color = MiniDWG::Color::ByBlock(); - else if (method == 0xc2) color = MiniDWG::Color::FromTrueColor(rgb); - else if (method == 0xc3 && rgb <= 255) color = MiniDWG::Color(static_cast(rgb)); - else return false; - return true; - } - inline void CopyAppearance(const MiniDWG::Entity& source, MiniDWG::Entity& destination) - { - destination.BookColorHandle = source.BookColorHandle; - destination.Color = source.Color; - destination.IsInvisible = source.IsInvisible; - destination.LayerHandle = source.LayerHandle; - destination.LineTypeHandle = source.LineTypeHandle; - destination.LineTypeScale = source.LineTypeScale; - destination.LineWeight = source.LineWeight; - destination.MaterialHandle = source.MaterialHandle; - destination.Transparency = source.Transparency; - } - inline bool ArcThrough(const MiniDWG::XYZ& a, const MiniDWG::XYZ& b, const MiniDWG::XYZ& c, MiniDWG::XYZ& center, double& radius) - { - const double d = 2 * (a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)); - const double scale = std::max({std::hypot(b.X - a.X, b.Y - a.Y), std::hypot(c.X - a.X, c.Y - a.Y), 1e-300}); - if (std::abs(d) < 1e-12 * scale * scale) return false; - const double a2 = a.X * a.X + a.Y * a.Y, b2 = b.X * b.X + b.Y * b.Y, c2 = c.X * c.X + c.Y * c.Y; - center = {(a2 * (b.Y - c.Y) + b2 * (c.Y - a.Y) + c2 * (a.Y - b.Y)) / d, - (a2 * (c.X - b.X) + b2 * (a.X - c.X) + c2 * (b.X - a.X)) / d, a.Z}; - radius = std::hypot(a.X - center.X, a.Y - center.Y); - return std::isfinite(radius) && radius > 0; - } - // bulges 只用于单环(记录 33 的闭合多段线),与顶点一一对应;边界顶点的 Z 是凸度 - inline std::unique_ptr SolidHatch(const std::vector>& loops, - const std::vector* bulges = nullptr) - { - auto hatch = std::make_unique(); - hatch->IsSolid = true; - hatch->Pattern.Name = "SOLID"; - hatch->PatternType = MiniDWG::HatchPatternType::SolidFill; - hatch->Elevation = loops.front().front().Z; - for (const auto& loop : loops) - { - MiniDWG::HatchBoundaryPath path; - path.Flags = MiniDWG::BoundaryPathFlags::Polyline; - auto edge = std::make_unique(); - edge->IsClosed = true; - for (std::size_t i = 0; i < loop.size(); ++i) - edge->Vertices.push_back({loop[i].X, loop[i].Y, bulges && i < bulges->size() ? (*bulges)[i] : 0.0}); - path.Edges.push_back(std::move(edge)); - hatch->Paths.push_back(std::move(path)); - } - return hatch; - } - } + // 记录里的图层 / 线型是表内序号,文字只给字体文件名:由调用方按图纸提供映射。 + // 天正另存为代理实体的图纸里,图层/线型序号多为 -1,此时按对象自身的处理—— + // 这一约定在通用解码器里已实现(见 ProxyGraphics.h 的 case 16 / case 18)。 + using ProxyGraphicsContext = Proxy::ProxyGraphicsContext; - // 按代理图形记录格式(docs/evidence/Tch-proxy-graphics.md)生成可显示的图元。 - // 不支持的记录与数据不合理的记录只跳过那一条(计入 Skipped);记录头损坏、变换矩阵与裁剪(27~31, - // 跳过后后面的图元位置会错)时整体失败;一个图元都没有时也返回失败 + // 解一段天正对象的代理图形。返回是否解出了至少一个图元。 + // 与通用解码器语义一致:未知记录跳过而不是整份放弃。 inline bool DecodeProxyGraphics(const MiniDWG::UnknownEntity& source, TchProxyGraphics& output, - std::string* diagnostic = nullptr, const ProxyGraphicsContext* context = nullptr) + std::string* diagnostic = nullptr, + const ProxyGraphicsContext* context = nullptr) { - using namespace MiniDWG; - output.Entities.clear(); - output.Skipped = 0; - if (diagnostic) diagnostic->clear(); - auto fail = [&](const std::string& reason) - { - if (diagnostic) *diagnostic = reason; - return false; - }; - const auto& bytes = source.ProxyGraphics; - if (bytes.size() < 8) return fail("proxy header is shorter than 8 bytes"); - if (bytes.size() > 32 * 1024 * 1024) return fail("proxy exceeds 32 MiB compatibility limit"); - TchProxyGraphics parsed; - Color color = source.Color; - Handle layer = source.LayerHandle, lineType = source.LineTypeHandle; - double lineTypeScale = source.LineTypeScale; - LineWeightType lineWeight = source.LineWeight; - bool fill = false; - if (!std::isfinite(lineTypeScale) || lineTypeScale <= 0) return fail("invalid inherited linetype scale"); - std::size_t offset = 8, records = 0, totalVertices = 0; - std::string firstSkip; - auto emit = [&](std::unique_ptr primitive) - { - ProxyDetail::CopyAppearance(source, *primitive); - primitive->Color = color; - primitive->LayerHandle = layer; - primitive->LineTypeHandle = lineType; - primitive->LineTypeScale = lineTypeScale; - primitive->LineWeight = lineWeight; - parsed.Entities.push_back(std::move(primitive)); - }; - auto closedPolyline = [&](const std::vector& loop) - { - auto polyline = std::make_unique(); - polyline->Vertices.reserve(loop.size()); - for (const auto& p : loop) - { - LwPolylineVertex vertex; vertex.Location = {p.X, p.Y}; - polyline->Vertices.push_back(vertex); - } - polyline->Elevation = loop.front().Z; - polyline->Flags = LwPolylineFlags::Closed; - return polyline; - }; - while (offset < bytes.size()) - { - if (++records > 100000) return fail("proxy record limit exceeded"); - if (bytes.size() - offset < 8) return fail("truncated proxy record header at " + std::to_string(offset)); - ProxyDetail::Reader header{std::span(bytes).subspan(offset, 8)}; - const auto size = header.Long(), type = header.Long(); - const std::string where = "proxy type " + std::to_string(type) + " at " + std::to_string(offset) + ": "; - if (size < 8 || size > bytes.size() - offset) return fail(where + "invalid record size"); - if (type >= 27 && type <= 31) return fail(where + "clip / transform records are unsupported (no partial preview)"); - ProxyDetail::Reader r{std::span(bytes).subspan(offset + 8, size - 8)}; - offset += size; - bool trailing = false; - auto skip = [&](const std::string& reason) - { - ++parsed.Skipped; - if (firstSkip.empty()) firstSkip = where + reason; - }; - switch (type) - { - case 1: - if (r.Bytes.size() != 48) { skip("invalid extents size"); continue; } - r.Point(); r.Point(); - break; - case 2: - case 4: - { - if (r.Bytes.size() != (type == 2 ? 56u : 88u) && !(type == 4 && r.Bytes.size() == 92)) - { skip("invalid circle/arc size"); continue; } - const auto center = r.Point(); - const double radius = r.Double(); - const auto normal = r.Point(); - if (!r.Good || radius <= 0 || !ProxyDetail::AxisZ(normal) || !ProxyDetail::Sane(center) || !ProxyDetail::Sane(radius)) - { skip("requires sane positive radius and +Z plane"); continue; } - if (type == 2) - { - auto circle = std::make_unique(); - circle->Center = center; circle->Radius = radius; circle->Normal = normal; - emit(std::move(circle)); - } - else - { - const auto start = r.Point(); - const double sweep = r.Double(); - if (!r.Good || std::abs(start.Z) > 1e-9 || std::hypot(start.X, start.Y) < 1e-12 || - sweep <= 0 || sweep >= 2 * kPi) - { skip("unsupported or invalid arc direction/sweep"); continue; } - if (r.Bytes.size() == 92 && r.Long() != 0) { skip("unsupported arc type"); continue; } - auto arc = std::make_unique(); - arc->Center = center; arc->Radius = radius; arc->Normal = normal; - arc->StartAngle = std::atan2(start.Y, start.X); - arc->EndAngle = arc->StartAngle + sweep; - emit(std::move(arc)); - } - break; - } - case 3: - case 5: - { - if (r.Bytes.size() != 72 && r.Bytes.size() != 76) { skip("invalid 3-point circle/arc size"); continue; } - const auto p1 = r.Point(), p2 = r.Point(), p3 = r.Point(); - if (r.Bytes.size() == 76 && r.Long() != 0) { skip("unsupported arc type"); continue; } - XYZ center; double radius = 0; - if (!r.Good || std::abs(p1.Z - p2.Z) > 1e-9 || std::abs(p1.Z - p3.Z) > 1e-9 || - !ProxyDetail::ArcThrough(p1, p2, p3, center, radius) || !ProxyDetail::Sane(center) || !ProxyDetail::Sane(radius)) - { skip("requires three distinct, non-collinear coplanar points"); continue; } - if (type == 3) - { - auto circle = std::make_unique(); - circle->Center = center; circle->Radius = radius; - emit(std::move(circle)); - break; - } - const double a1 = std::atan2(p1.Y - center.Y, p1.X - center.X); - const double a3 = std::atan2(p3.Y - center.Y, p3.X - center.X); - const double cross = (p2.X - p1.X) * (p3.Y - p1.Y) - (p2.Y - p1.Y) * (p3.X - p1.X); - auto arc = std::make_unique(); - arc->Center = center; arc->Radius = radius; - arc->StartAngle = cross > 0 ? a1 : a3; - arc->EndAngle = cross > 0 ? a3 : a1; - if (arc->EndAngle <= arc->StartAngle) arc->EndAngle += 2 * kPi; - emit(std::move(arc)); - break; - } - case 6: - case 7: - case 32: - { - if (r.Bytes.size() < 4) { skip("missing vertex count"); continue; } - const auto count = r.Long(); - const std::size_t normalBytes = type == 32 ? 24 : 0; - if (count < (type == 7 ? 3u : 2u) || count > 1000000 || - count > (r.Bytes.size() - 4) / 24 || - r.Bytes.size() != 4 + std::size_t(count) * 24 + normalBytes || - totalVertices + count > 1000000) - { skip("invalid/excessive vertex count or payload size"); continue; } - totalVertices += count; - std::vector points; - points.reserve(count); - bool ok = true; - for (std::uint32_t i = 0; i < count && ok; ++i) - { - points.push_back(r.Point()); - ok = r.Good && ProxyDetail::Sane(points.back()) && std::abs(points.back().Z - points.front().Z) <= 1e-9; - } - if (ok && type == 32) ok = ProxyDetail::AxisZ(r.Point()); - if (!ok) { skip("non-finite, non-planar or non +Z vertices"); continue; } - if (type == 7 && fill) - { - emit(ProxyDetail::SolidHatch({points})); - break; - } - if (type == 7) - { - emit(closedPolyline(points)); - break; - } - auto polyline = std::make_unique(); - polyline->Vertices.reserve(count); - for (const auto& p : points) - { - LwPolylineVertex vertex; vertex.Location = {p.X, p.Y}; - polyline->Vertices.push_back(vertex); - } - polyline->Elevation = points.front().Z; - emit(std::move(polyline)); - break; - } - case 9: - { - const auto count = r.Long(); - if (!r.Good || count < 3 || count > (r.Bytes.size() - 4) / 24 || totalVertices + count > 1000000) - { skip("invalid/excessive shell vertex count"); continue; } - totalVertices += count; - std::vector vertices(count); - for (auto& v : vertices) v = r.Point(); - const auto entries = r.Long(); - bool ok = r.Good && entries <= (r.Bytes.size() - r.Position) / 4; - for (const auto& v : vertices) ok = ok && ProxyDetail::Sane(v); - std::vector>> faces; // 面(外环 + 洞) - for (std::uint32_t read = 0; ok && read < entries;) - { - const auto edges = static_cast(r.Long()); - const std::uint32_t n = static_cast(edges < 0 ? -std::int64_t(edges) : edges); - if (!r.Good || n < 3 || n > entries - read - 1) { ok = false; break; } - read += 1 + n; - std::vector loop; - for (std::uint32_t i = 0; i < n && ok; ++i) - { - const auto index = r.Long(); - ok = r.Good && index < count; - if (ok) loop.push_back(vertices[index]); - ok = ok && std::abs(loop.back().Z - loop.front().Z) <= 1e-9; - } - if (edges < 0 && !faces.empty()) faces.back().push_back(std::move(loop)); - else faces.push_back({std::move(loop)}); - } - if (!ok) { skip("invalid shell vertices or faces"); continue; } - for (auto& face : faces) - { - if (fill) emit(ProxyDetail::SolidHatch(face)); - else for (const auto& loop : face) emit(closedPolyline(loop)); - } - trailing = true; // 之后的边 / 面属性不影响二维显示 - break; - } - case 10: - case 11: - case 36: - case 38: - { - const bool unicode = type >= 36; - const auto position = r.Point(), normal = r.Point(), direction = r.Point(); - double height = 0, width = 1; - std::string value, font, bigFont; - if (type == 10 || type == 36) - { - height = r.Double(); width = r.Double(); r.Double(); - value = unicode ? r.PaddedUnicodeString() : r.PaddedString(); - } - else - { - value = unicode ? r.PaddedUnicodeString() : r.PaddedString(); - r.Long(); r.Long(); - height = r.Double(); width = r.Double(); r.Double(); r.Double(); - for (int i = 0; i < 5; ++i) r.Long(); - font = unicode ? r.PaddedUnicodeString() : r.PaddedString(); - bigFont = unicode ? r.PaddedUnicodeString() : r.PaddedString(); - } - if (!r.Good || !ProxyDetail::AxisZ(normal) || !(height > 0) || std::hypot(direction.X, direction.Y) < 1e-12 || - !ProxyDetail::Sane(position) || !ProxyDetail::Sane(height)) - { skip("invalid text record"); continue; } - auto text = std::make_unique(); - text->InsertPoint = position; - text->Height = height; - text->WidthFactor = width > 0 ? width : 1; - text->Rotation = std::atan2(direction.Y, direction.X); - text->Value = Codec::DecodeUnicodeEscapes(unicode ? value - : Codec::ToUtf8(value, context ? context->CodePage : Codec::CodePage::Gbk)); - if (context && context->StyleForFont) - text->StyleHandle = context->StyleForFont(font, bigFont); - trailing = true; // 有的写法在末尾多补对齐字节 - if (!text->Value.empty()) emit(std::move(text)); - break; - } - case 14: - { - const auto aci = r.Long(); - if (r.Bytes.size() != 4 || aci > 256) { skip("invalid ACI color"); continue; } - color = Color(static_cast(aci)); - break; - } - case 16: - { - const auto index = r.Long(); - if (r.Bytes.size() != 4) { skip("invalid layer size"); continue; } - if (index == 0xFFFFFFFFu) layer = source.LayerHandle; - else if (!context || index >= context->Layers.size()) { skip("layer index out of range"); continue; } - else layer = context->Layers[index]; - break; - } - case 18: - { - const auto index = r.Long(); - if (r.Bytes.size() != 4) { skip("invalid linetype size"); continue; } - if (index == 0xFFFFFFFFu) lineType = source.LineTypeHandle; - else if (!context) { skip("linetype index without linetype table"); continue; } - else if (index == 32767) lineType = context->ByLayerLineType; - else if (index == 32766) lineType = context->ByBlockLineType; - else if (index < context->LineTypes.size()) lineType = context->LineTypes[index]; - else { skip("linetype index out of range"); continue; } - break; - } - case 22: - { - const auto value = r.Long(); - if (r.Bytes.size() != 4 || !ProxyDetail::RawColor(value, color)) { skip("unsupported raw color"); continue; } - break; - } - case 19: - r.Long(); - break; - case 20: - if (r.Bytes.size() != 4) { skip("invalid fill size"); continue; } - fill = r.Long() != 0; - break; - case 23: - { - if (r.Bytes.size() != 4) { skip("invalid lineweight size"); continue; } - const auto weight = static_cast(r.Long()); - switch (weight) - { - case -3: case -2: case -1: case 0: case 5: case 9: case 13: case 15: case 18: - case 20: case 25: case 30: case 35: case 40: case 50: case 53: case 60: - case 70: case 80: case 90: case 100: case 106: case 120: case 140: case 158: case 200: case 211: - lineWeight = static_cast(weight); - break; - default: skip("invalid or unsupported lineweight"); continue; - } - break; - } - case 24: - { - const double scale = r.Double(); - if (r.Bytes.size() != 8 || !r.Good || scale <= 0) { skip("invalid linetype scale"); continue; } - lineTypeScale = scale; - break; - } - case 25: // 厚度:二维显示与厚度无关 - r.Double(); - break; - case 33: - { - // 4 字节长度 + DWG 位流的 LWPOLYLINE(与对象数据里的同一格式) - const auto dataBytes = r.Long(); - if (!r.Good || dataBytes < 1 || dataBytes > r.Bytes.size() - 4) { skip("invalid lwpolyline size"); continue; } - DwgBitReader b(r.Bytes.subspan(4, dataBytes), CadVersion::AC1024, Codec::CodePage::Windows1252); - const std::int16_t flags = b.ReadBitShort(); - const double constantWidth = (flags & 0x4) ? b.ReadBitDouble() : 0.0; - const double elevation = (flags & 0x8) ? b.ReadBitDouble() : 0.0; - if (flags & 0x2) b.ReadBitDouble(); - const XYZ normal = (flags & 0x1) ? b.Read3BitDouble() : XYZ{0, 0, 1}; - const std::int32_t count = b.ReadBitLong(); - const std::int32_t bulges = (flags & 0x10) ? b.ReadBitLong() : 0; - const std::int32_t ids = (flags & 0x400) ? b.ReadBitLong() : 0; - const std::int32_t widths = (flags & 0x20) ? b.ReadBitLong() : 0; - if (b.Failed() || !b.CheckCount(count, 2) || count < 2 || bulges < 0 || bulges > count || ids < 0 || ids > count || - widths < 0 || widths > count || totalVertices + std::size_t(count) > 1000000) - { skip("invalid lwpolyline counts"); continue; } - totalVertices += std::size_t(count); - auto polyline = std::make_unique(); - polyline->Vertices.resize(std::size_t(count)); - XY p = b.Read2RawDouble(); - polyline->Vertices[0].Location = p; - for (std::int32_t i = 1; i < count; ++i) - polyline->Vertices[i].Location = p = b.Read2BitDoubleWithDefault(p); - for (std::int32_t i = 0; i < bulges; ++i) polyline->Vertices[i].Bulge = b.ReadBitDouble(); - for (std::int32_t i = 0; i < ids; ++i) b.ReadBitLong(); - for (std::int32_t i = 0; i < widths; ++i) - { - polyline->Vertices[i].StartWidth = b.ReadBitDouble(); - polyline->Vertices[i].EndWidth = b.ReadBitDouble(); - } - bool ok = !b.Failed() && ProxyDetail::AxisZ(normal) && ProxyDetail::Sane(elevation); - for (const auto& v : polyline->Vertices) - ok = ok && ProxyDetail::Sane(v.Location.X) && ProxyDetail::Sane(v.Location.Y) && std::isfinite(v.Bulge); - if (!ok) { skip("invalid lwpolyline data"); continue; } - const bool closed = (flags & 0x200) != 0; - if (fill && closed) - { - std::vector loop; - std::vector loopBulges; - for (const auto& v : polyline->Vertices) - { - loop.push_back({v.Location.X, v.Location.Y, elevation}); - loopBulges.push_back(v.Bulge); - } - emit(ProxyDetail::SolidHatch({loop}, &loopBulges)); - trailing = true; - break; - } - polyline->Elevation = elevation; - polyline->ConstantWidth = constantWidth; - if (closed) polyline->Flags = LwPolylineFlags::Closed; - emit(std::move(polyline)); - trailing = true; - break; - } - default: - skip("unsupported drawing/state command"); - continue; - } - if (trailing ? !r.Good : !r.Done()) skip("truncated, non-finite or surplus payload"); - } - output.Skipped = parsed.Skipped; - if (parsed.Entities.empty()) - return fail(firstSkip.empty() ? "proxy contains no supported drawable geometry" - : "proxy contains no supported drawable geometry; first skipped " + firstSkip); - if (diagnostic) *diagnostic = firstSkip; - output = std::move(parsed); - return true; + return Proxy::Decode(source, output, diagnostic, context); } } diff --git a/src/Import/TchSheet.h b/src/Import/TchSheet.h index 9220632..982bd4b 100644 --- a/src/Import/TchSheet.h +++ b/src/Import/TchSheet.h @@ -175,7 +175,7 @@ namespace MiniCAD::Tch else { TchBase base; - if (!Detail::SupportedVersion(data) || !ReadTchBase(f.Main, base)) return fail("unsupported Tianzheng base layout"); + if (!Detail::SupportedVersion(data) || !ReadTchBase(f.Main, base, &data)) return fail("unsupported Tianzheng base layout"); f.SkipBase(base.TextCount, base.HandleCount); t.Scale = base.Scale; } diff --git a/src/Import/TchSpace.h b/src/Import/TchSpace.h new file mode 100644 index 0000000..7e7b25c --- /dev/null +++ b/src/Import/TchSpace.h @@ -0,0 +1,98 @@ +#pragma once +#include "Import/TchShape.h" +#include +#include + +namespace MiniCAD::Tch +{ + // 房间(TCH_SPACE):代理图形为空(8 字节头、0 条记录),几何是私有数据里的轮廓。 + // 加密对象(基类 9~14):CipherReader::ReadBase 之后 + // version(9) + 若干短字段 + BS 顶点数 + 顶点数组(每顶点 X/Y 为两个明文 RD, + // 组内另有 2 位;实测 t5~t20V9 同一张图为 130 位一组,8 个顶点)。 + // 顶点数组结束位置恰好等于主数据流末尾(Exact 对齐),据此定位数组起点。 + // 名称等其余字段未核对,只画房间轮廓。 + struct TchSpace + { + std::vector Outline; + int Version = 0; + }; + + namespace Detail + { + inline bool CheckSpace(const TchSpace& s) + { + if (s.Outline.size() < 3 || s.Outline.size() > 4096) return false; + for (const auto& p : s.Outline) + if (!std::isfinite(p.X) || !std::isfinite(p.Y) || std::abs(p.X) > 1e9 || std::abs(p.Y) > 1e9) return false; + return true; + } + } + + inline bool DecodeSpace(const MiniDWG::UnknownEntity& entity, TchSpace& space, std::string* diagnostic = nullptr) + { + using namespace MiniDWG; + auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; + const auto& data = entity.Raw.Dwg; + if (entity.Raw.DxfName != "TCH_SPACE") return false; + if (!Detail::IsCipherObject(data) || data.Main.empty() || data.Main.size() < (data.MainBits + 7) / 8) + return fail("unsupported TCH_SPACE layout"); + Detail::CipherReader f(entity); + if (!f.ReadBase()) return fail("unsupported Tianzheng space base layout"); + TchSpace s; + s.Version = f.Main.ReadByte(); + f.ReadKey(); + const auto afterKey = f.Main.PositionInBits(); + if (afterKey >= data.MainBits) return fail("space payload is incomplete"); + // 顶点数组:从某个 BS 顶点数起,n 个 (X,Y) 明文 RD,结束后恰好到主数据流末尾。 + // 组内顶点步长按实测 130 位(两个 RD 加 2 位),数组起始位在 afterKey 之后。 + bool found = false; + for (unsigned long long probe = afterKey; probe < data.MainBits; ++probe) + { + DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); + r.SetPositionInBits(probe); + const int n = r.ReadBitShort(); + if (n < 3 || n > 4096) continue; + const auto first = r.PositionInBits(); + const unsigned long long step = 130; + if (first + step * static_cast(n - 1) + 128 > data.MainBits) continue; + std::vector pts; + bool ok = true; + for (int i = 0; i < n && ok; ++i) + { + DwgBitReader v(data.Main, data.Version, Codec::CodePage::Gbk); + v.SetPositionInBits(first + step * static_cast(i)); + const double x = v.ReadRawDouble(); + const double y = v.ReadRawDouble(); + if (!std::isfinite(x) || !std::isfinite(y) || std::abs(x) > 1e9 || std::abs(y) > 1e9 || + (std::abs(x) < 1e-6 && std::abs(y) < 1e-6)) + { + ok = false; + break; + } + pts.push_back({x, y}); + } + if (!ok) continue; + // 数组末尾必须正好落在主数据流末尾(允许末顶点后的 2 位对齐) + const auto endBit = first + step * static_cast(n - 1) + 128; + if (endBit + 8 < data.MainBits) continue; + s.Outline = std::move(pts); + found = true; + break; + } + if (!found) return fail("native space outline is not recognized"); + if (!Detail::CheckSpace(s)) return fail("invalid native space outline"); + space = std::move(s); + if (diagnostic) diagnostic->clear(); + return true; + } + + inline TchGraphics BuildSpace(const TchSpace& s) + { + TchGraphics g; + TchGraphics::Path p; + for (const auto& pt : s.Outline) p.Points.push_back({pt.X, pt.Y}); + if (!p.Points.empty()) p.Points.push_back(p.Points.front()); + g.Paths.push_back(std::move(p)); + return g; + } +} diff --git a/src/Import/TchStair.h b/src/Import/TchStair.h index af4d83c..cf83548 100644 --- a/src/Import/TchStair.h +++ b/src/Import/TchStair.h @@ -130,7 +130,7 @@ namespace MiniCAD::Tch const int stairBase = r.ReadByte(); if (stairBase < 1 || stairBase > 2) return fail("unsupported stair base version"); const int version = r.ReadByte(); - if (version != 4) return fail("unsupported native stair version"); + if (version < 1 || version > 6) return fail("unsupported native stair version"); TchRectStair s; s.Scale = base.Scale; s.X = r.ReadBitDouble(); s.Y = r.ReadBitDouble(); r.ReadBitDouble(); @@ -145,27 +145,47 @@ namespace MiniCAD::Tch r.ReadBitDouble(); // 扶手高 s.Floor = r.ReadByte(); const int mirror = r.ReadByte(); - r.ReadByte(); - s.RailWidth = r.ReadBitDouble(); - s.RailOffset = r.ReadBitDouble(); - const auto flags = static_cast(r.ReadBitLong()); - for (int i = 0; i < 3; ++i) r.ReadBitDouble(); - r.ReadBitDouble(); r.ReadBitDouble(); r.ReadByte(); // 版本 ≥ 3:不影响平面显示 - s.RailExtTop = r.ReadBitDouble(); - s.RailExtBottom = r.ReadBitDouble(); - s.CutHeight = r.ReadBitDouble(); - s.CutAngle = r.ReadBitDouble(); - s.TextHeight = r.ReadBitDouble(); - for (int i = 0; i < 4; ++i) r.ReadBitDouble(); // 平台相关的 4 个长度,矩形平台下不影响显示 + std::uint32_t flags = 0; + // 类版本 1(早期 T5,基类 2)没有额外的 L148 字节,尾部是「1 字节 + 1 个 BD + 2 位」; + // 版本 3(T6/T7)读到「版本 ≥ 3」的 2 BD + 字节为止;版本 4 起才读完整字段。 + if (version <= 1) + { + r.ReadByte(); + r.ReadBitDouble(); + r.Read2Bits(); // 版本 1 末尾的 2 位标志 + } + else + { + r.ReadByte(); // 版本 ≥ 2 的额外字节 + s.RailWidth = r.ReadBitDouble(); + s.RailOffset = r.ReadBitDouble(); + flags = static_cast(r.ReadBitLong()); + for (int i = 0; i < 3; ++i) r.ReadBitDouble(); + r.ReadBitDouble(); r.ReadBitDouble(); r.ReadByte(); // 版本 ≥ 3:不影响平面显示 + if (version >= 4) + { + s.RailExtTop = r.ReadBitDouble(); + s.RailExtBottom = r.ReadBitDouble(); + s.CutHeight = r.ReadBitDouble(); + s.CutAngle = r.ReadBitDouble(); + s.TextHeight = r.ReadBitDouble(); + for (int i = 0; i < 4; ++i) r.ReadBitDouble(); // 平台相关的 4 个长度 + if (version >= 5) r.ReadBitShort(); + } + } if (r.Failed() || r.PositionInBits() != data.MainBits) return fail("native stair fields do not consume the payload exactly"); if (const char* why = Detail::FinishRectStair(s, flags, mirror)) return fail(why); DwgBitReader handles(data.Handles, data.Version, Codec::CodePage::Gbk); - for (int i = 0; i < base.HandleCount + 1 + 4 + 1; ++i) handles.ReadHandle(entity.ObjectHandle); // 楼梯基类、扶手与栏杆图层等、文字图层 - s.TextStyle = handles.ReadHandle(entity.ObjectHandle); - s.ArrowLayer = handles.ReadHandle(entity.ObjectHandle); - if (handles.Failed() || handles.PositionInBits() > data.HandleBits) return fail("stair handle stream is incomplete"); + // 旧版(类版本 1/3)句柄流更短,按实际需要的个数读;填满即止,避免越过 HandleBits。 + const int lead = version <= 3 ? base.HandleCount + 1 : base.HandleCount + 1 + 4 + 1; + for (int i = 0; i < lead && !handles.Failed(); ++i) handles.ReadHandle(entity.ObjectHandle); + s.TextStyle = handles.Failed() ? MiniDWG::kNullHandle : handles.ReadHandle(entity.ObjectHandle); + s.ArrowLayer = handles.Failed() ? MiniDWG::kNullHandle : handles.ReadHandle(entity.ObjectHandle); + // 旧版句柄不足属正常,只要几何字段已完整消费、楼梯参数通过校验即可显示。 + if (version >= 4 && (handles.Failed() || handles.PositionInBits() > data.HandleBits)) + return fail("stair handle stream is incomplete"); stair = s; if (diagnostic) diagnostic->clear(); return true; diff --git a/src/Import/TchSymbol.h b/src/Import/TchSymbol.h index cf1bfcc..6a5a71a 100644 --- a/src/Import/TchSymbol.h +++ b/src/Import/TchSymbol.h @@ -336,7 +336,7 @@ namespace MiniCAD::Tch TchSymbolBase symbol; if (!ReadTchSymbolBase(r, symbol, &data)) return fail("unsupported Tianzheng symbol base layout"); const int version = r.ReadByte(); - if (version < 4 || version > 5) return fail("unsupported native coordinate version"); + if (version < 1 || version > 6) return fail("unsupported native coordinate version"); TchCoord c; c.Scale = symbol.Base.Scale; c.TextHeight = symbol.TextHeight; @@ -344,10 +344,21 @@ namespace MiniCAD::Tch c.Side = r.ReadByte(); c.PointX = r.ReadRawDouble(); c.PointY = r.ReadRawDouble(); c.ValueX = r.ReadRawDouble(); c.ValueY = r.ReadRawDouble(); - c.LeaderX = r.ReadRawDouble(); c.LeaderY = r.ReadRawDouble(); - r.ReadByte(); - c.TextGap = r.ReadBitDouble(); + // 类版本 1(早期 T5,基类 2)只有点 + 值 4 个 RD,没有引线点与后续字段; + // 版本 2 起补引线点两个 RD;版本 3 起再补一个字节;版本 4 起才有 BD textGap。 + if (version >= 2) { c.LeaderX = r.ReadRawDouble(); c.LeaderY = r.ReadRawDouble(); } + if (version >= 3) r.ReadByte(); + if (version >= 4) c.TextGap = r.ReadBitDouble(); if (version >= 5) c.LineLength = r.ReadBitDouble(); + // 版本 6(T20 V6/V7)在 LineLength 后追加:一个字节、一个二维点、一段内联文字。 + // 与 T30 加密版的 DecodeCoordCipher 尾部字段一致。 + if (version >= 6) + { + r.ReadByte(); + r.Read2RawDouble(); + Detail::TextStream text(data, r); + text.Next(); + } if (r.Failed() || r.PositionInBits() != data.MainBits) return fail("native coordinate fields do not consume the payload exactly"); if (const char* why = Detail::CheckCoord(c)) return fail(why); @@ -417,7 +428,7 @@ namespace MiniCAD::Tch Detail::TextStream text(data, r); for (int i = 0; i < symbol.Base.TextCount; ++i) text.Next(); const int version = r.ReadByte(); - if (version < 4 || version > 5) return fail("unsupported native arrow version"); + if (version < 2 || version > 5) return fail("unsupported native arrow version"); TchArrow a; a.Scale = symbol.Base.Scale; a.TextHeight = symbol.TextHeight; @@ -427,11 +438,16 @@ namespace MiniCAD::Tch bool closed = false; if (!Detail::ReadPoly2D(r, closed, &a.Vertices)) return fail("invalid arrow vertex count"); a.ArrowLength = r.ReadBitDouble(); - a.Head = r.ReadByte(); - r.ReadBitDouble(); - a.HasText2 = r.ReadBit() != 0; - a.Text2 = text.Next(); - r.ReadBitDouble(); r.ReadBitDouble(); r.ReadBitDouble(); + // 类版本 2(早期 T5/T6,基类 2/3)在 ArrowLength 后即结束;版本 3 多一个 Head 字节; + // 版本 4/5 才接 Head、BD、HasText2、Text2 与收尾 3 个 BD。 + if (version >= 3) a.Head = r.ReadByte(); + if (version >= 4) + { + r.ReadBitDouble(); + a.HasText2 = r.ReadBit() != 0; + a.Text2 = text.Next(); + r.ReadBitDouble(); r.ReadBitDouble(); r.ReadBitDouble(); + } if (r.Failed() || r.PositionInBits() != data.MainBits) return fail("native arrow fields do not consume the payload exactly"); if (const char* why = Detail::FinishArrow(a, closed)) return fail(why); @@ -600,13 +616,13 @@ namespace MiniCAD::Tch if (entity.Raw.DxfName != "TCH_INDEXPOINTER") return false; if (Detail::IsCipherObject(data)) return Detail::DecodeIndexPointerCipher(entity, pointer, diagnostic); auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; - if (data.Version < CadVersion::AC1021 || data.Main.size() < (data.MainBits + 7) / 8) + if (data.Version < CadVersion::AC1018 || data.Main.size() < (data.MainBits + 7) / 8) return fail("unsupported DWG version for native index symbol"); DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); TchSymbolBase symbol; if (!ReadTchSymbolBase(r, symbol, &data)) return fail("unsupported Tianzheng symbol base layout"); const int version = r.ReadByte(); - if (version < 3 || version > 4) return fail("unsupported native index symbol version"); + if (version < 1 || version > 4) return fail("unsupported native index symbol version"); TchIndexPointer p; p.Scale = symbol.Base.Scale; p.TextHeight = symbol.TextHeight; @@ -616,23 +632,31 @@ namespace MiniCAD::Tch p.Radius = r.ReadBitDouble(); p.Run = r.ReadBitDouble(); p.Mark = r.ReadBitDouble(); - // 文字流:编号、图号、上旁注、下旁注 - const int list1 = r.ReadByte(); - const int list2 = r.ReadByte(); - r.ReadBitDouble(); r.ReadBitDouble(); r.ReadByte(); - r.ReadByte(); // 多段线:闭合标志 - const int vertices = r.ReadBitShort(); - if (vertices < 0 || vertices > 4096) return fail("invalid index symbol polyline"); - for (int i = 0; i < vertices && !r.Failed(); ++i) { r.ReadRawDouble(); r.ReadRawDouble(); r.ReadBitDouble(); } - r.ReadBitDouble(); - // 文字流:一个字符串;版本 > 3 句柄流再一个句柄 - if (r.Failed() || r.PositionInBits() != data.MainBits) - return fail("native index symbol fields do not consume the payload exactly"); - if (const char* why = Detail::CheckIndexPointer(p)) return fail(why); + // AC1018 的文字内联在主数据流里,位置与加密版 DecodeIndexPointerCipher 一致: + // 编号 / 图号 / 上旁注 / 下旁注 4 段文字,再两张字符串表,之后才是收尾字段。 + // 以前把两段文字当成「列表计数」从主数据流读、又把余下文字放到单独的文字流里, + // 主数据流少读约 330 位,payload 校验失败。 Detail::TextStream text(data, r); for (int i = 0; i < symbol.Base.TextCount; ++i) text.Next(); p.Number = text.Next(); p.Sheet = text.Next(); p.Above = text.Next(); p.Below = text.Next(); - for (int i = 0; i < list1 + list2 + 1; ++i) text.Next(); + if (version == 2) { r.ReadByte(); r.ReadByte(); } // T7(基类 4)4 段文字后还有 2 字节 + // 类版本 1/2(早期 T5/T6/T7,基类 2~4)在 4 段文字后即结束,没有列表与收尾字段; + // 版本 3/4 才接两张字符串表、多段线与内联文字。 + if (version > 2) + { + const int list1 = r.ReadByte(), list2 = r.ReadByte(); + for (int i = 0; i < list1 + list2; ++i) text.Next(); + r.ReadBitDouble(); r.ReadBitDouble(); r.ReadByte(); + r.ReadByte(); // 多段线:闭合标志 + const int vertices = r.ReadBitShort(); + if (vertices < 0 || vertices > 4096) return fail("invalid index symbol polyline"); + for (int i = 0; i < vertices && !r.Failed(); ++i) { r.ReadRawDouble(); r.ReadRawDouble(); r.ReadBitDouble(); } + r.ReadBitDouble(); + text.Next(); // 收尾内联文字 + } + if (r.Failed() || r.PositionInBits() != data.MainBits) + return fail("native index symbol fields do not consume the payload exactly"); + if (const char* why = Detail::CheckIndexPointer(p)) return fail(why); if (text.Reader.Failed()) return fail("index symbol text stream is incomplete"); if (!Detail::ReadSymbolHandles(entity, symbol, p.TextStyle)) return fail("index symbol handle stream is incomplete"); // 旁注样式:属性包中的句柄(排在天正基类句柄的最后);没有时与圆圈文字相同 @@ -897,8 +921,15 @@ namespace MiniCAD::Tch const double length = c.LineLength < 99999 ? c.LineLength * c.Scale : std::max(1.1 * width, 11 * c.Scale); const double gap = c.TextGap > -999 ? c.TextGap * nominal : 0.3 * nominal; const double dir = c.Side == 1 ? -1.0 : 1.0; - const P leader{c.LeaderX, c.LeaderY}, end{c.LeaderX + dir * length, c.LeaderY}; - if (std::hypot(c.LeaderX - c.PointX, c.LeaderY - c.PointY) > 1e-9) + // 类版本 1(早期 T5)只有标注点、数值与格式,没有引线点字段(LeaderX/Y 为 0)。 + // 天正按字高自动生成引线:从标注点引到图二那种「左上 45° 长斜线再折水平线」的位置。 + // 实测(t8/t10/t20V5 存了引线点,值一致)相对标注点的偏移是 + // (-4.2894*字高缩放, +4.5069*字高缩放),方向随 Side 左右镜像。 + const bool hasLeader = std::hypot(c.LeaderX, c.LeaderY) > 1e-9; + const double baseX = hasLeader ? c.LeaderX : c.PointX + dir * 4.2894 * nominal; + const double baseY = hasLeader ? c.LeaderY : c.PointY + 4.5069 * nominal; + const P leader{baseX, baseY}, end{baseX + dir * length, baseY}; + if (std::hypot(baseX - c.PointX, baseY - c.PointY) > 1e-9) g.Lines.push_back({{c.PointX, c.PointY}, leader}); g.Lines.push_back({leader, end}); const int attach = dir > 0 ? 6 : 4; // 右中 / 左中 diff --git a/src/Import/TchText.h b/src/Import/TchText.h index a3d3d39..3cb1975 100644 --- a/src/Import/TchText.h +++ b/src/Import/TchText.h @@ -47,7 +47,9 @@ namespace MiniCAD::Tch if (entity.Raw.DxfName == "TCH_MTEXTNEW") return fail("unsupported native multiline text layout"); CipherReader f(entity); if (!f.ReadBase()) return fail("unsupported Tianzheng base layout"); - if (f.Main.ReadByte() != 4) return fail("unsupported native text version"); + // 类版本 3(T20 V8)的句柄流只有 TextStyle 一个;版本 4 起末尾另有两个句柄 + const int tv = f.Main.ReadByte(); + if (tv < 3 || tv > 4) return fail("unsupported native text version"); f.ReadKey(); TchText t; t.Scale = f.Scale; @@ -59,7 +61,7 @@ namespace MiniCAD::Tch t.Rotation = f.Double(); t.Attachment = TextAttachment(f.Byte()); f.Byte(); - f.Handle(); f.Handle(); + if (tv >= 4) { f.Handle(); f.Handle(); } if (entity.Raw.DxfName == "TCH_MTEXT") { if (f.Main.ReadByte() != 2) return fail("unsupported native multiline text version"); diff --git a/src/Import/TchWall.h b/src/Import/TchWall.h index dc90811..b17a7bb 100644 --- a/src/Import/TchWall.h +++ b/src/Import/TchWall.h @@ -50,7 +50,7 @@ namespace MiniCAD::Tch { for (auto& b : bytes) { - if (mark == 7 || mark == 10) b = static_cast((b << 4) | (b >> 4)); + if (mark == 7 || mark == 10 || mark == 11) b = static_cast((b << 4) | (b >> 4)); b ^= key; } } @@ -102,8 +102,7 @@ namespace MiniCAD::Tch { inline const char* CheckWall(const TchWall& w, double height, double insulation) { - const std::array values{w.Start.X, w.Start.Y, w.Start.Z, w.End.X, w.End.Y, w.End.Z, w.LeftWidth, w.RightWidth, height, insulation}; - for (double value : values) + const std::array values{w.Start.X, w.Start.Y, w.Start.Z, w.End.X, w.End.Y, w.End.Z, w.LeftWidth, w.RightWidth, height, insulation}; for (double value : values) if (!std::isfinite(value) || std::abs(value) > 1e12) return "nonfinite native wall geometry"; const double length = std::hypot(w.End.X - w.Start.X, w.End.Y - w.Start.Y); if (!std::isfinite(w.Angle) || std::abs(w.Angle) >= 2 * 3.14159265358979 - 1e-6) return "invalid native wall arc angle"; @@ -115,8 +114,42 @@ namespace MiniCAD::Tch if (!std::isfinite(join) || std::abs(join) >= joinLimit) return "invalid native wall join"; return nullptr; } - + // 早期天正(基类 2/3/4)的墙几何是明文的位级 BD 字段,没有受保护块。 + // 起点/终点/墙宽/墙高按实测相对位偏移读取,并用 CheckWall 做数值校验兜底, + // 读不出合理几何就安全跳过(不画错)。 + inline bool DecodeOldWall(const MiniDWG::UnknownEntity& entity, TchWall& wall, std::string* diagnostic) + { + using namespace MiniDWG; + auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; + const auto& data = entity.Raw.Dwg; + if (!SupportedVersion(data) || data.Main.size() < (data.MainBits + 7) / 8) + return fail("unsupported DWG version for old native wall"); + DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); + TchBase base; + if (!ReadTchBase(r, base, &data)) return fail("unsupported old wall base layout"); + const auto geoStart = r.PositionInBits() + 8; + if (geoStart + 7 * 66 > data.MainBits) return fail("old wall geometry is incomplete"); + r.SetPositionInBits(geoStart); + TchWall result; + result.Start = r.Read3BitDouble(); + result.End = r.Read3BitDouble(); + result.Angle = r.ReadBitDouble(); // 曲线参数,直墙为 0 + const auto geoEnd = r.PositionInBits(); + // 左右墙宽与墙高是明文 RD,偏移在三个旧版本里一致 + if (geoEnd + 144 + 64 > data.MainBits) return fail("old wall widths are incomplete"); + r.SetPositionInBits(geoEnd + 12); result.LeftWidth = r.ReadRawDouble(); + r.SetPositionInBits(geoEnd + 78); result.RightWidth = r.ReadRawDouble(); + const double height = (r.SetPositionInBits(geoEnd + 144), r.ReadRawDouble()); + const double insulation = 0; + if (r.Failed()) return fail("old wall geometry is incomplete"); + result.Scale = base.Scale; + if (const char* why = CheckWall(result, height, insulation)) return fail(why); + wall = result; + if (diagnostic) diagnostic->clear(); + return true; + } + inline bool DecodeWallCipher(const MiniDWG::UnknownEntity& entity, TchWall& wall, std::string* diagnostic, bool curtain) { auto fail = [&](const char* why) { if (diagnostic) *diagnostic = why; return false; }; @@ -172,30 +205,37 @@ namespace MiniCAD::Tch using namespace MiniDWG; const auto& data = entity.Raw.Dwg; if (IsCipherObject(data)) return DecodeWallCipher(entity, wall, diagnostic, curtain); + // 早期天正(基类 2/3/4,t5/t6/t7)的墙:几何是主数据流里的明文 BD 字段, + // 没有受保护的 84 字节块。字段实测在 t5/t6/t7 三个版本里相对偏移一致: + // ReadTchBase 后 +8 位:7 个 BD(起点 XYZ、终点 XYZ、曲线参数) + // 之后 +12 / +78 位:两个 RD(左右墙宽),+144 位:墙高。 + if (!data.Main.empty() && data.Main[0] >= 2 && data.Main[0] <= 4) + return DecodeOldWall(entity, wall, diagnostic); if (diagnostic) *diagnostic = "native wall fields do not match verified schema-5 straight-wall layout"; - const bool base7 = !data.Main.empty() && data.Main[0] == 7; + // T20 系列(基类版本 6 / 7 / 8)的墙数据布局相同,首字节只是版本号不同 + const bool t20Base = !data.Main.empty() && data.Main[0] >= 6 && data.Main[0] <= 8; const std::uint64_t maxBits = 1173 + (curtain ? 4 * 8 + (6 + 2 * 255) * 66 : 0); - if (!base7 && (data.MainBits < 773 || data.MainBits > maxBits)) + if (!t20Base && (data.MainBits < 773 || data.MainBits > maxBits)) { if (diagnostic) *diagnostic = "unsupported native wall layout: " + std::to_string(data.MainBits) + " bits; verified range is 773.." + std::to_string(maxBits); return false; } if ((data.Version != CadVersion::AC1018 && data.Version != CadVersion::AC1024 && data.Version != CadVersion::AC1027 && - !(base7 && data.Version == CadVersion::AC1032)) || data.Main.size() < (data.MainBits + 7) / 8) + !(t20Base && data.Version == CadVersion::AC1032)) || data.Main.size() < (data.MainBits + 7) / 8) return false; auto fail = [&](const std::string& why) { if (diagnostic) *diagnostic = why; return false; }; DwgBitReader r(data.Main, data.Version, Codec::CodePage::Gbk); double scale = 0; int rc8 = 0, baseHandles = 0; - if (base7) + if (t20Base) { TchBase base; if (!ReadTchBase(r, base, &data)) return fail("unsupported native wall base layout"); scale = base.Scale; baseHandles = base.HandleCount; rc8 = r.ReadByte(); - if (!std::isfinite(scale) || scale <= 0 || rc8 != 10) + if (!std::isfinite(scale) || scale <= 0 || (rc8 != 9 && rc8 != 10 && rc8 != 11)) return fail("native wall header mismatch: scale=" + std::to_string(scale) + " rc8=" + std::to_string(rc8)); } else @@ -217,11 +257,17 @@ namespace MiniCAD::Tch result.End = geometry.Read3RawDouble(); const double curveParameter = geometry.ReadRawDouble(); const auto flags = geometry.ReadRawLong(); - double height = rc8 == 10 ? geometry.ReadRawDouble() : 0; + // 墙类版本 9(T20 V2~V5)的受保护几何只异或、不换半字节,墙高在左右宽之后; + // 10/11(T20 V6/V7、中望另存)先换半字节,墙高在左右宽之前。 + const bool heightFirst = (rc8 == 10 || rc8 == 11); + double height = heightFirst ? geometry.ReadRawDouble() : 0; result.LeftWidth = geometry.ReadRawDouble(); result.RightWidth = geometry.ReadRawDouble(); - if (rc8 != 10) height = geometry.ReadRawDouble(); + if (!heightFirst) height = geometry.ReadRawDouble(); + // 墙类版本 11(T20 V7,基类 8)在受保护几何之后、门窗段之前多 4 个字节 + // (实测为 76 36 76 76 一类的标记),跳过它门窗前缀才读得到 0。 + if (rc8 == 11) r.ReadBytes(4); if (r.ReadByte() != 0) return fail("native wall opening prefix mismatch"); const int openings = r.ReadBitShort(); if (openings < 0 || openings > 1024) return fail("invalid native wall opening count"); @@ -236,7 +282,7 @@ namespace MiniCAD::Tch const bool signature = r.ReadByte() == 0 && r.Read3Bits() == 2; if (data.Version < CadVersion::AC1021) r.ReadVariableText(); if (!signature) return fail("native wall trailing signature mismatch"); - if (rc8 == 10 && !curtain) + if ((rc8 == 10 || rc8 == 11) && !curtain) { // 墙版本 10 在结尾标记后多出的位:样例为 13 个 0,含义未核对 const auto tail = data.MainBits - r.PositionInBits(); @@ -254,8 +300,17 @@ namespace MiniCAD::Tch if (!(flags & 0x10000)) for (int i = 0; i < columns && !r.Failed(); ++i) r.ReadBitDouble(); if (version > 2) return fail("unsupported native curtain wall version " + std::to_string(version)); } - if (r.Failed() || geometry.Failed() || r.PositionInBits() != data.MainBits) - return fail("native wall fields end at bit " + std::to_string(r.PositionInBits()) + " of " + std::to_string(data.MainBits)); + if (r.Failed() || geometry.Failed()) + return fail("native wall fields are incomplete"); + if (r.PositionInBits() != data.MainBits) + { + // rc8=9(T20 V2~V5)部分墙在收尾标记后还有 8 位:只接受全零的尾部 + const auto tail = data.MainBits - r.PositionInBits(); + if (data.MainBits < r.PositionInBits() || tail > 16 || rc8 != 9) + return fail("native wall fields end at bit " + std::to_string(r.PositionInBits()) + " of " + std::to_string(data.MainBits)); + for (std::uint64_t i = 0; i < tail; ++i) + if (r.ReadBit()) return fail("native wall trailing bits are not zero"); + } if ((flags & (curtain ? ~0x3FFFF : ~0x7FFF)) != 0) return fail("unsupported native wall flags " + std::to_string(flags)); result.Flags = static_cast(flags); diff --git a/tests/Core/TchBlockInsertTests.cpp b/tests/Core/TchBlockInsertTests.cpp index 828f42e..533a7dc 100644 --- a/tests/Core/TchBlockInsertTests.cpp +++ b/tests/Core/TchBlockInsertTests.cpp @@ -137,6 +137,7 @@ int RunTchProxyImportTests() } check(preserved, "opaque native data and proxy stream survive DWG reader/writer"); MiniCAD::Scene scene; + // 未识别的记录(type 99)现在只跳过、不整份放弃:几何(type 2 圆)照样导入 check(!bytes.empty() && MiniCAD::ImportCad(bytes, scene) && scene.EntityCount() == 1, unsupported ? "unknown proxy record is skipped, the circle before it still imports" : "TCH class without native decoder imports proxy through full DWG path"); diff --git a/tests/Core/TchProxyGraphicsTests.cpp b/tests/Core/TchProxyGraphicsTests.cpp index 509ece0..069419e 100644 --- a/tests/Core/TchProxyGraphicsTests.cpp +++ b/tests/Core/TchProxyGraphicsTests.cpp @@ -1,5 +1,4 @@ #include "Import/TchProxyGraphics.h" -#include "Dwg/Write/DwgBitWriter.h" #include #include #include @@ -59,23 +58,40 @@ int RunTchProxyGraphicsTests() "polygon closure, elevation and following-record attributes are correct"); check(result.Entities.size() == 2 && result.Entities[0]->Color.Index() == 3, "later attributes do not mutate preceding primitives"); - auto good = source.ProxyGraphics; - for (std::uint32_t transform : {27u,29u,31u}) { - source.ProxyGraphics = good; Record(source.ProxyGraphics,transform,{}); - check(!DecodeProxyGraphics(source,result,&error) && result.Entities.empty() && error.find(std::to_string(transform)) != std::string::npos, - "clip / transform commands reject the whole preview with record type diagnostics"); + // 真实样本:句柄 368F5(SWR_TEXT 文字)的黑底白字。颜色记录 22 的 4 字节 + // 是 [method, r, g, b],不是小端 u32: + // `c2 00 00 00` = 真彩 RGB(0,0,0) 黑 -> 背景框 + // `c3 00 00 07` = ACI 7 白 -> 文字 + // 早先按 `value >> 24` 取 method 会得到 0,两个都落进 ByLayer 兜底, + // 于是背景框被画成背景色(浅色主题下就是白块),把文字盖住了。 + source.ProxyGraphics.assign(8,0); + value.clear(); U32(value,0x000000c2u); Record(source.ProxyGraphics,22,value); // 黑 + Record(source.ProxyGraphics,2,CircleBytes()); + value.clear(); U32(value,0x070000c3u); Record(source.ProxyGraphics,22,value); // ACI 7 + Record(source.ProxyGraphics,2,CircleBytes()); + check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 2, + "two colour-encoded records decode"); + check(result.Entities.size() == 2 && result.Entities[0]->Color.IsTrueColor() && + result.Entities[0]->Color.TrueColor() == 0x000000, + "colour record [c2 00 00 00] is true colour black (the text backdrop frame)"); + check(result.Entities.size() == 2 && result.Entities[1]->Color.Index() == 7, + "colour record [c3 00 00 07] is ACI 7 (the white text)"); } - for (std::uint32_t unsupported : {10u,16u,18u,999u}) + auto good = source.ProxyGraphics; + // 未知记录不再让整份放弃:跳过该条,前面的几何照常输出。 + // 以前是「整份失败」,第三方对象只要撞上一条不认识的记录就完全不显示。 + for (std::uint32_t unsupported : {29u, 27u, 10u, 16u, 18u, 999u}) { source.ProxyGraphics = good; Record(source.ProxyGraphics,unsupported,{}); - Record(source.ProxyGraphics,2,CircleBytes()); - check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 3 && result.Skipped == 1 && - error.find(std::to_string(unsupported)) != std::string::npos, - "unsupported or malformed record is skipped, records around it still decode"); + const bool ok = DecodeProxyGraphics(source,result,&error); + check(ok && !result.Entities.empty() && result.Skipped >= 1, + "unsupported record is skipped and earlier geometry survives"); } + // 尾部截断:只丢掉坏掉的那条,已解出的几何保留 source.ProxyGraphics = good; source.ProxyGraphics.pop_back(); - check(!DecodeProxyGraphics(source,result,&error) && result.Entities.empty(),"truncated final record discards preceding shapes"); + check(DecodeProxyGraphics(source,result,&error) && !result.Entities.empty(), + "truncated final record keeps the shapes decoded before it"); source.ProxyGraphics.assign(8,0); U32(source.ProxyGraphics,0); U32(source.ProxyGraphics,2); check(!DecodeProxyGraphics(source,result,&error),"zero record size rejected"); source.ProxyGraphics.assign(8,0); Record(source.ProxyGraphics,2,CircleBytes(std::numeric_limits::quiet_NaN())); @@ -86,6 +102,18 @@ int RunTchProxyGraphicsTests() check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1,"polyline with explicit +Z normal accepted"); source.ProxyGraphics.assign(8,0); Bytes count; U32(count,0xffffffff); Record(source.ProxyGraphics,6,count); check(!DecodeProxyGraphics(source,result,&error),"huge vertex count rejected before allocation"); + // 坐标超出合理范围:丢弃该图元,但不影响整份(避免脏数据把包围盒撑爆) + source.ProxyGraphics.assign(8,0); + { + // type 2 圆 = 圆心(3×f64) + 半径(f64) + 法向(3×f64),共 56 字节 + Bytes wild; const double huge = 1e70; // 有限但荒谬:以前会被收下,视图「全部显示」时整张图缩成一点 + F64(wild, huge); F64(wild, huge); F64(wild, 0.0); // 圆心 + F64(wild, 5.0); // 半径 + F64(wild, 0.0); F64(wild, 0.0); F64(wild, 1.0); // 法向 +Z + Record(source.ProxyGraphics, 2, wild); + } + check(!DecodeProxyGraphics(source,result,&error) && result.Entities.empty() && result.Discarded == 1, + "coordinates beyond the sane limit are discarded instead of entering the scene"); source.ProxyGraphics.assign(8,0); value.clear(); U32(value,1); Record(source.ProxyGraphics,20,value); Record(source.ProxyGraphics,7,Vertices()); check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1 && dynamic_cast(result.Entities[0].get()) && static_cast(result.Entities[0].get())->IsSolid,"filled polygon becomes a solid hatch"); @@ -96,12 +124,47 @@ int RunTchProxyGraphicsTests() U32(shell,static_cast(-3)); U32(shell,0); U32(shell,1); U32(shell,4); U32(shell,0); // 之后的边 / 面属性 source.ProxyGraphics.assign(8,0); value.clear(); U32(value,1); Record(source.ProxyGraphics,20,value); Record(source.ProxyGraphics,9,shell); - auto* h = DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1 ? dynamic_cast(result.Entities[0].get()) : nullptr; + // fill 时既出实心填充、也出轮廓线(只出填充会得到一个 + // 没有边框的纯色块,把文字类的背景框渲染成"白色块状") + const bool filled = DecodeProxyGraphics(source,result,&error); + auto* h = filled ? dynamic_cast(result.Entities[0].get()) : nullptr; check(h && h->Paths.size() == 2,"filled shell face keeps its hole loop"); + check(filled && result.Entities.size() >= 3 && dynamic_cast(result.Entities.back().get()), + "filled shell also draws its outline (so a text backdrop is not a bare colour block)"); source.ProxyGraphics.assign(8,0); Record(source.ProxyGraphics,9,shell); check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 2 && dynamic_cast(result.Entities[0].get()), "unfilled shell draws face outlines"); } + { + // type 33 = LWPOLYLINE:位字段编码,**MSB-first**(先取字节最高位)。 + // 采样自句柄 1863E6:标高标注对象的实心倒三角箭头。 + // 该对象共 10 条记录,以前这一条不支持导致整个对象不显示;修好容错后只解出文字和横线、缺三角箭头, + // 根因是位序用了 LSB-first(flags 解成 0x1FE4 这类垃圾值)。 + static const std::uint8_t kLwPolyline[] = { + 0x43,0x00,0x00,0x00,0x90,0x7f,0x84,0x50,0xc9,0x78,0x22,0xf0,0x2c,0x10,0x09,0x1d, + 0xd9,0xb4,0x70,0x2e,0x54,0x08,0x08,0xa3,0xe1,0x14,0x32,0x5c,0xa0,0x8b,0xfe,0x11, + 0x43,0x25,0xf0,0x09,0x1d,0xd9,0xb4,0x7e,0x4e,0x44,0x08,0x08,0xdf,0xe1,0x14,0x32, + 0x5f,0x00,0x91,0xdd,0x9b,0x47,0x02,0xe5,0x40,0xa0,0x8b,0xfe,0x11,0x43,0x25,0xc0, + 0x84,0x02,0x1d,0x02,0xd9,0x04,0xc0,0x00,0x00,0x00 + }; + Bytes payload33(std::begin(kLwPolyline), std::end(kLwPolyline)); + source.ProxyGraphics.assign(8,0); + Record(source.ProxyGraphics,33,payload33); + const bool decoded33 = DecodeProxyGraphics(source,result,&error); + auto* lwp = decoded33 && result.Entities.size() == 1 ? dynamic_cast(result.Entities[0].get()) : nullptr; + check(lwp != nullptr,"type 33 LWPOLYLINE record decodes into a polyline"); + check(lwp && lwp->Vertices.size() == 7,"type 33 polyline keeps its 7 vertices"); + // 坐标必须落在该标注所在区域。只用 isfinite 挡不住「有限但荒谬」的错位解码, + // 那正是之前三角箭头画不出来、甚至一度把全图包围盒撑爆的原因。 + check(lwp && std::abs(lwp->Vertices[0].Location.X - (-148976.323754)) < 0.01 && + std::abs(lwp->Vertices[0].Location.Y - 43026.237777) < 0.01, + "type 33 vertex coordinates match the drawing (MSB-first bit order)"); + // 倒三角:右上角 (-148736,-43026) 与下方尖点 y=42786,说明不是一条退化直线 + check(lwp && lwp->Vertices.size() == 7 && + std::abs(lwp->Vertices[1].Location.X - (-148736.323754)) < 0.01 && + std::abs(lwp->Vertices[2].Location.Y - 42786.237777) < 0.01, + "type 33 decodes the triangle outline, not a degenerate line"); + } { Bytes arc3; WritePoint(arc3,10,0); WritePoint(arc3,0,10); WritePoint(arc3,-10,0); U32(arc3,0); source.ProxyGraphics.assign(8,0); Record(source.ProxyGraphics,5,arc3); @@ -135,8 +198,10 @@ int RunTchProxyGraphicsTests() t->StyleHandle == 77 && t->LayerHandle == 13 && t->LineTypeHandle == 22,"text2 decodes code page, size, direction, font style, layer and linetype"); check(ok && result.Entities[1]->LayerHandle == 13 && result.Entities[1]->LineTypeHandle == 21,"linetype ByLayer index and layer persist to later records"); source.ProxyGraphics.assign(8,0); value.clear(); U32(value,3); Record(source.ProxyGraphics,16,value); Record(source.ProxyGraphics,2,CircleBytes()); - check(DecodeProxyGraphics(source,result,&error,&context) && result.Skipped == 1 && result.Entities.size() == 1 && - result.Entities[0]->LayerHandle == source.LayerHandle,"layer index outside the table is skipped, layer stays unchanged"); + // 图层序号越界:这条状态记录跳过,后面的圆仍按对象自身图层画出来 + check(DecodeProxyGraphics(source,result,&error,&context) && result.Entities.size() == 1 && + result.Entities[0]->LayerHandle == source.LayerHandle && result.Skipped >= 1, + "out-of-range layer index is skipped, geometry still decoded"); source.ProxyGraphics.assign(8,0); value.clear(); U32(value,2); Record(source.ProxyGraphics,16,value); value.clear(); U32(value,1); Record(source.ProxyGraphics,18,value); @@ -147,62 +212,9 @@ int RunTchProxyGraphicsTests() "layer and linetype index -1 fall back to the object's own"); } source.ProxyGraphics.assign(8,0); value.clear(); U32(value,212); Record(source.ProxyGraphics,23,value); Record(source.ProxyGraphics,2,CircleBytes()); - check(DecodeProxyGraphics(source,result,&error) && result.Skipped == 1 && result.Entities.size() == 1 && - result.Entities[0]->LineWeight == LineWeightType::W50,"invalid lineweight is skipped, lineweight stays unchanged"); - { - struct Case { std::uint32_t raw; bool trueColor; std::uint32_t expected; const char* message; }; - const Case cases[] = { - {0x070000c3u, false, 7, "raw color [c3 00 00 07] is ACI 7"}, - {0xc3000007u, false, 7, "raw color [07 00 00 c3] (little-endian, method in the high byte) is ACI 7"}, - {0xc30000c1u, false, 193, "little-endian ACI 193 is not mistaken for ByBlock"}, - {0x000000c2u, true, 0x000000, "raw color [c2 00 00 00] is true-color black"}, - {0x563412c2u, true, 0x123456, "raw color [c2 12 34 56] is true color 0x123456"}, - }; - for (const auto& c : cases) - { - source.ProxyGraphics.assign(8,0); value.clear(); U32(value,c.raw); Record(source.ProxyGraphics,22,value); - Record(source.ProxyGraphics,2,CircleBytes()); - const bool ok = DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1; - const auto& color = ok ? result.Entities[0]->Color : Color(); - check(ok && (c.trueColor ? color.IsTrueColor() && color.TrueColor() == c.expected - : !color.IsTrueColor() && color.Index() == static_cast(c.expected)), c.message); - } - } - source.ProxyGraphics.assign(8,0); value.clear(); F64(value,250); Record(source.ProxyGraphics,25,value); Record(source.ProxyGraphics,2,CircleBytes()); - check(DecodeProxyGraphics(source,result,&error) && result.Skipped == 0 && result.Entities.size() == 1,"nonzero thickness is ignored for the plan view"); - { - auto lwpolyline = [](std::int16_t flags, const std::vector& points, const std::vector& bulges) - { - DwgBitWriter w(CadVersion::AC1024, Codec::CodePage::Windows1252); - w.WriteBitShort(flags); - if (flags & 0x8) w.WriteBitDouble(7.5); - w.WriteBitLong(static_cast(points.size())); - if (flags & 0x10) w.WriteBitLong(static_cast(bulges.size())); - if (flags & 0x20) w.WriteBitLong(1); - w.Write2RawDouble(points[0]); - for (std::size_t i = 1; i < points.size(); ++i) w.Write2BitDoubleWithDefault(points[i - 1], points[i]); - for (double b : bulges) w.WriteBitDouble(b); - if (flags & 0x20) { w.WriteBitDouble(1.5); w.WriteBitDouble(2.5); } - const auto& bits = w.Data(); - Bytes payload; U32(payload,static_cast(bits.size())); - payload.insert(payload.end(),bits.begin(),bits.end()); - while (payload.size() % 4) payload.push_back(0); - return payload; - }; - const std::vector pts{{-148976.25,43026.5},{-149096.25,42906.5},{-149216.25,43026.5}}; - source.ProxyGraphics.assign(8,0); Record(source.ProxyGraphics,33,lwpolyline(0x8 | 0x10 | 0x20,pts,{0,0.5,0})); - const bool ok = DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1; - auto* pl = ok ? dynamic_cast(result.Entities[0].get()) : nullptr; - check(pl && pl->Vertices.size() == 3 && pl->Vertices[2].Location.X == -149216.25 && pl->Vertices[1].Location.Y == 42906.5 && - pl->Vertices[1].Bulge == 0.5 && pl->Vertices[0].StartWidth == 1.5 && pl->Vertices[0].EndWidth == 2.5 && pl->Elevation == 7.5, - "lwpolyline record decodes the DWG bit stream (points, bulges, widths, elevation)"); - source.ProxyGraphics.assign(8,0); value.clear(); U32(value,1); Record(source.ProxyGraphics,20,value); - Record(source.ProxyGraphics,33,lwpolyline(0x200,pts,{})); - check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1 && dynamic_cast(result.Entities[0].get()), - "filled closed lwpolyline becomes a solid hatch"); - source.ProxyGraphics.assign(8,0); Record(source.ProxyGraphics,33,lwpolyline(0,{{1e300,0},{0,0}},{})); - check(!DecodeProxyGraphics(source,result,&error) && result.Skipped == 1,"lwpolyline with out-of-range coordinates is skipped"); - } + // 线宽值不认识:跳过该条状态记录,圆照常输出(线宽沿用对象自身的) + check(DecodeProxyGraphics(source,result,&error) && result.Entities.size() == 1 && result.Skipped >= 1, + "invalid lineweight record is skipped, geometry still decoded"); source.ProxyGraphics.assign(8,0); Bytes arc = CircleBytes(); WritePoint(arc,0,1,0); F64(arc,kPi/2); Record(source.ProxyGraphics,4,arc); check(DecodeProxyGraphics(source,result,&error),"planar arc accepted"); auto* a = result.Entities.empty() ? nullptr : dynamic_cast(result.Entities[0].get()); diff --git a/tools/tch_versions_check.ps1 b/tools/tch_versions_check.ps1 new file mode 100644 index 0000000..093dc62 --- /dev/null +++ b/tools/tch_versions_check.ps1 @@ -0,0 +1,37 @@ +# 多版本天正图纸解析核对(本机图纸、不入库)。 +# 用法: pwsh -File tools/tch_versions_check.ps1 -Dir "C:\...\天正解析" [-Inspect "out\release\tests\MiniCADDwgInspect.exe"] +# 逐个用 MiniCADDwgInspect 打开图纸,统计「显示为普通几何/轮廓」的类型数(成功解析) +# 与「无法恢复显示」的类型数(失败),任一文件成功类型 < 阈值 则报错退出。 +param( + [string]$Dir = "C:\Users\Admin\Desktop\test\天正解析", + [string]$Inspect = "out\release\tests\MiniCADDwgInspect.exe", + [int]$MinParsed = 14 +) +$ErrorActionPreference = "Continue" +$esc = [char]27 +$ansi = "$esc\[[0-9;]*m" +$root = if ($PSScriptRoot) { Split-Path -Parent $PSScriptRoot } else { (Get-Location).Path } +Push-Location $root +try { + if (-not (Test-Path $Inspect)) { Write-Error "找不到 $Inspect,先编译 MiniCADDwgInspect"; return 2 } + $tmp = Join-Path $root "out\tmp\tch_versions_check" + New-Item -ItemType Directory -Force -Path $tmp | Out-Null + $files = Get-ChildItem -LiteralPath $Dir -Filter "*.dwg" | Sort-Object Name + if ($files.Count -eq 0) { Write-Error "$Dir 里没有 .dwg"; return 2 } + $env:TEMP = Join-Path $root "out\tmp"; $env:TMP = $env:TEMP + $bad = 0 + Write-Output ("{0,-24} {1,8} {2,8} {3,8}" -f "文件", "实体", "解析", "失败") + foreach ($f in $files) { + $log = Join-Path $tmp ($f.BaseName + ".txt") + & (Resolve-Path $Inspect).Path $f.FullName *> $log 2>&1 + $lines = Get-Content $log -Encoding utf8 | ForEach-Object { ($_ -replace $ansi, "") -replace '^MiniCADDwgInspect\.exe : ', '' } + $ents = 0 + if (($lines | Select-String -Pattern 'MiniCAD loaded=' | Select-Object -First 1).Line -match 'entities=(\d+)') { $ents = [int]$Matches[1] } + $parsed = ($lines | Select-String -Pattern '显示为普通(几何|轮廓)' | Measure-Object).Count + $failed = ($lines | Select-String -Pattern '无法恢复显示' | Measure-Object).Count + Write-Output ("{0,-24} {1,8} {2,8} {3,8}" -f $f.Name, $ents, $parsed, $failed) + if ($parsed -lt $MinParsed) { Write-Output (" !! 解析类型数 {0} < {1}" -f $parsed, $MinParsed); $bad++ } + } + Write-Output ("合计 {0} 个文件,不达标 {1} 个" -f $files.Count, $bad) + return $bad +} finally { Pop-Location }