#include "StdAfx.h" #include "pcf.h" #include "uiiado.h" #include "base64.h" #include "coretools.h" #include "aecdbewpipe.h" #include "aecdbewfitting.h" #include "aecdbewequipment.h" #include "aecdbxequipment.h" #include "aecdbpipehanger.h" #include #include #include #include using namespace std; namespace { typedef const wchar_t *STR_PTR; typedef pair STR_PAIR; typedef map STR_MAP; // 定义属性字符串,避免直接输入造成笔误,用到那个,定义哪个 // ========================================== // 1. 基础单位 (Units) // ========================================== STR_PTR PCF_UNITS_MM = L"MM"; STR_PTR PCF_UNITS_METER = L"METER"; STR_PTR PCF_UNITS_INCH = L"INCH"; STR_PTR PCF_UNITS_FEET = L"FEET"; STR_PTR PCF_UNITS_INCH_SIXTEENTH = L"INCH-SIXTEENTH"; STR_PTR PCF_UNITS_MM_HUNDREDTHS = L"MM-HUNDREDTHS"; STR_PTR PCF_UNITS_DEGREES = L"DEGREES"; STR_PTR PCF_UNITS_RADIANS = L"RADIANS"; STR_PTR PCF_UNITS_KGS = L"KGS"; STR_PTR PCF_UNITS_LBS = L"LBS"; STR_PTR PCF_UNITS_NM = L"NM"; STR_PTR PCF_UNITS_LBFT = L"LBFT"; // ========================================== // 2. 全局头部属性 (Header Attributes) // ========================================== STR_PTR PCF_HEAD_ISOGEN_FILES = L"ISOGEN-FILES"; STR_PTR PCF_HEAD_UNITS_BORE = L"UNITS-BORE"; STR_PTR PCF_HEAD_UNITS_CO_ORDS = L"UNITS-CO-ORDS"; STR_PTR PCF_HEAD_UNITS_BOLT_DIA = L"UNITS-BOLT-DIA"; STR_PTR PCF_HEAD_UNITS_BOLT_LENGTH = L"UNITS-BOLT-LENGTH"; STR_PTR PCF_HEAD_UNITS_WEIGHT = L"UNITS-WEIGHT"; STR_PTR PCF_HEAD_PIPELINE_REFERENCE = L"PIPELINE-REFERENCE"; STR_PTR PCF_HEAD_PIPING_SPEC = L"PIPING-SPEC"; STR_PTR PCF_HEAD_REVISION = L"REVISION"; STR_PTR PCF_HEAD_PROJECT_IDENTIFIER = L"PROJECT-IDENTIFIER"; STR_PTR PCF_HEAD_SYSTEM_IDENTIFIER = L"SYSTEM-IDENTIFIER"; // ========================================== // 3. 组件主类型 (Component Types) - 增加前缀 COMP_ 以防命名冲突 // ========================================== STR_PTR PCF_COMP_PIPE = L"PIPE"; STR_PTR PCF_COMP_ELBOW = L"ELBOW"; STR_PTR PCF_COMP_TEE = L"TEE"; STR_PTR PCF_COMP_CROSS = L"CROSS"; STR_PTR PCF_COMP_REDUCER = L"REDUCER"; STR_PTR PCF_COMP_VALVE = L"VALVE"; STR_PTR PCF_COMP_FLANGE = L"FLANGE"; STR_PTR PCF_COMP_GASKET = L"GASKET"; STR_PTR PCF_COMP_BOLT = L"BOLT"; STR_PTR PCF_COMP_WELD = L"WELD"; STR_PTR PCF_COMP_CAP = L"CAP"; STR_PTR PCF_COMP_BLIND_FLANGE = L"FLANGE-BLIND"; STR_PTR PCF_COMP_REDUCER_CONC = L"REDUCER-CONCENTRIC"; STR_PTR PCF_COMP_REDUCER_ECC = L"REDUCER-ECCENTRIC"; STR_PTR PCF_COMP_INSTRUMENT = L"INSTRUMENT"; STR_PTR PCF_COMP_SUPPORT = L"SUPPORT"; STR_PTR PCF_COMP_EQUIPMENT = L"EQUIPMENT"; STR_PTR PCF_COMP_COUPLING = L"COUPLING"; STR_PTR PCF_COMP_OLET = L"OLET"; STR_PTR PCF_COMP_BEND = L"BEND"; // 设备(EQUIPMENT)块关键字 STR_PTR PCF_KW_REFERENCE = L"REFERENCE"; STR_PTR PCF_KW_LOCATION = L"LOCATION"; STR_PTR PCF_KW_ORIENTATION = L"ORIENTATION"; STR_PTR PCF_KW_NOZZLE = L"NOZZLE"; STR_PTR PCF_KW_DIRECTION = L"DIRECTION"; STR_PTR PCF_KW_SHAPE = L"SHAPE"; STR_PTR PCF_KW_CATEGORY = L"CATEGORY"; // 支吊架(SUPPORT)块关键字 / 取值 STR_PTR PCF_ATTR_NAME = L"NAME"; STR_PTR PCF_SKEY_SUPPORT_DEFAULT = L"01HG"; STR_PTR PCF_CAT_ERECTION = L"ERECTION"; STR_PTR PCF_ATTR_COMPONENT_ATTRIBUTE0 = L"COMPONENT-ATTRIBUTE0"; STR_PTR PCF_KEY_SUPPORT_DN = L"DN"; // 支吊架专业数据键(取自图集表的列名) STR_PTR PCF_KEY_SUPPORT_MODE = L"MODE"; // 支吊架类型 0~4 // 焊口(WELD)块关键字 / 取值 STR_PTR PCF_ATTR_SKEY_WELD = L"WW"; // Isogen 焊接符号键 STR_PTR PCF_CAT_FABRICATION = L"FABRICATION"; // ========================================== // 4. 坐标与节点类型 (Point Types) // ========================================== STR_PTR PCF_PT_END_POINT = L"END-POINT"; STR_PTR PCF_PT_CENTRE_POINT = L"CENTRE-POINT"; STR_PTR PCF_PT_BRANCH1_POINT = L"BRANCH1-POINT"; STR_PTR PCF_PT_BRANCH2_POINT = L"BRANCH2-POINT"; STR_PTR PCF_PT_CO_ORDS = L"CO-ORDS"; // ========================================== // 5. 组件明细与业务属性 (Component Attributes) // ========================================== STR_PTR PCF_ATTR_SKEY = L"SKEY"; STR_PTR PCF_ATTR_ITEM_CODE = L"ITEM-CODE"; STR_PTR PCF_ATTR_ITEM_DESCRIPTION = L"ITEM-DESCRIPTION"; STR_PTR PCF_ATTR_MATERIAL_IDENTIFIER = L"MATERIAL-IDENTIFIER"; STR_PTR PCF_ATTR_WEIGHT = L"WEIGHT"; STR_PTR PCF_ATTR_WALL_THICKNESS = L"WALL-THICKNESS"; STR_PTR PCF_ATTR_RATING = L"RATING"; STR_PTR PCF_ATTR_FABRICATION_ITEM = L"FABRICATION-ITEM"; STR_PTR PCF_ATTR_ERECTION_ITEM = L"ERECTION-ITEM"; STR_PTR PCF_ATTR_SPINDLE_DIRECTION = L"SPINDLE-DIRECTION"; STR_PTR PCF_ATTR_MESSAGE_TEXT = L"MESSAGE-TEXT"; class PcfRow { public: private: ostream &m_out; // 统一使用窄字符流基类! bool m_isFirstItem; public: PcfRow(ostream &outStream, int numOfSpace) : m_out(outStream), m_isFirstItem(true) { for (int i = 0; i < numOfSpace; ++i) m_out << " "; } ~PcfRow() { m_out << "\n"; } // 1. 基础类型模板 (如 int, double, AcGePoint3d) template PcfRow &operator<<(const T &value) { if (!m_isFirstItem) m_out << " "; m_out << fixed << setprecision(4) << value; m_isFirstItem = false; return *this; } // 3. 核心升级:拦截 AutoCAD 宽字符 (处理带中文的描述信息) PcfRow &operator<<(STR_PTR wValue) { if (!m_isFirstItem) m_out << " "; // 自动转为 UTF-8 并推入底层字节流 m_out << Base64::unicode2Ansii(wValue, CP_UTF8); m_isFirstItem = false; return *this; } // 写一个属性 PcfRow &write_Attribute(STR_PTR attri, STR_PTR svalue) { if ((attri && svalue) && (wcslen(attri) > 0 && wcslen(svalue) > 0)) { *this<< attri << svalue; } return *this; } }; PcfRow &operator<<(PcfRow &row, const AcGePoint3d &pt) { // 自动拆解并保持间距 row << pt.x << pt.y << pt.z; return row; } void writePcfAttribute(ostream &out, int indent, STR_PTR attri, STR_PTR svalue) { // 只有当属性名和属性值都切实有效时,才实例化 PcfRow if (attri && svalue && wcslen(attri) > 0 && wcslen(svalue) > 0) PcfRow(out, indent) << attri << svalue; } STR_PAIR makePair(STR_PTR field, STR_PTR v) { return make_pair(field, v); } template STR_PAIR makePair(STR_PTR field, T &v, const int &prec = 0) { std::wstringstream wss; wss << fixed << setprecision(prec) << v; // 显式构造 std::pair,避免再匹配到本模板造成无限递归(C4717) return std::make_pair(std::wstring(field), wss.str()); } wstring getValue(STR_MAP mm, STR_PTR key) { STR_MAP::const_iterator it(mm.find(key)); if (it != mm.end()) return it->second; return L""; } // ============================ 规格库(物料码)查询 ============================ // 合法等级名集合(取自 AecSpecname),用于在实体专业数据里反查"等级"。 // 进程内只加载一次。 const std::set &validLevels(CUIISQLiteServer *pDb) { static std::set s_levels; static bool s_bLoaded = false; if (!s_bLoaded && pDb) { CStringArray ar; if (pDb->GetSelData(_T("SELECT DISTINCT 等级名称 FROM AecSpecname"), ar) > 0) { for (int i = 0; i < ar.GetSize(); ++i) { CString s(ar[i]); s.Trim(); if (!s.IsEmpty()) s_levels.insert(s); } } // 加载失败(库没打开等)时下次再试 s_bLoaded = !s_levels.empty(); } return s_levels; } /* 解析组件的"等级"。 先读 PIPE_LEVEL("Level");为空时扫描专业数据里的全部字符串值, 命中 AecSpecname 里的合法等级名即采用。 实测图纸里 PIPE_LEVEL 经常为空(例如插入阀门时 command.cpp:18836 会把它清空), 但规格库里的等级是有效的,所以必须做这层兜底,否则 ITEM-CODE 永远查不出来。 */ CString resolveLevel(AecDbObjXDataMap *pMap, CUIISQLiteServer *pDb) { if (!pMap) return _T(""); CString sLevel; if (pMap->GetAt(PIPE_LEVEL, sLevel) && !sLevel.IsEmpty()) { sLevel.Trim(); return sLevel; } const std::set &levels = validLevels(pDb); if (levels.empty()) return _T(""); // 需要非 const 迭代器才能遍历专业数据 for (AecDbObjXDataMap::It it = pMap->Begin(); it != pMap->End(); ++it) { const AecXData &xd = it->second; if (xd.nVt == VT_BSTR && xd.pszVal) { CString s(xd.pszVal); s.Trim(); if (levels.find(s) != levels.end()) return s; } } return _T(""); } // 拼规格查询 SQL:按 等级 + 若干等值过滤列 void buildSpecSql(CString &sql, LPCTSTR pszTable, const CString &sLevel , const std::vector > &filters) { sql.Format(_T("SELECT * FROM %s t") _T(" INNER JOIN AecSpecname a") _T(" ON t.[项目名称] = a.[项目名称] AND t.[等级名称] = a.[等级名称]") _T(" WHERE a.[等级名称] = '%s'"), pszTable, (LPCTSTR)sLevel); for (size_t k = 0; k < filters.size(); ++k) { sql += _T(" AND t.["); sql += filters[k].first; sql += _T("] = '"); sql += filters[k].second; sql += _T("'"); } } // 按 等级 + 若干等值过滤列 查规格库,返回所有匹配行(列名 -> 值) bool querySpecRows(CUIISQLiteServer *pDb, LPCTSTR pszTable, const CString &sLevel , const std::vector > &filters , std::vector > &rows) { rows.clear(); if (!pDb || !pszTable || sLevel.IsEmpty()) return false; CString sql; buildSpecSql(sql, pszTable, sLevel, filters); return pDb->GetSelDataEx(sql, rows) > 0 && !rows.empty(); } // 按 等级 + 若干等值过滤列 查规格库,返回第一行。查不到返回 false。 bool querySpecRow(CUIISQLiteServer *pDb, LPCTSTR pszTable, const CString &sLevel , const std::vector > &filters , std::map &row) { std::vector > rows; if (!querySpecRows(pDb, pszTable, sLevel, filters, rows) || rows.empty()) return false; row = rows[0]; return true; } // 从规格行里取一列(大小写与前后空格不敏感) CString specField(const std::map &row, LPCTSTR pszCol) { CString sWant(pszCol ? pszCol : _T("")); sWant.Trim(); for (std::map::const_iterator it = row.begin(); it != row.end(); ++it) { CString sKey(it->first); sKey.Trim(); if (sKey.CompareNoCase(sWant) == 0) { CString sVal(it->second); sVal.Trim(); return sVal; } } return _T(""); } // 用规格行填充 PCF 的 ITEM-CODE / ITEM-DESCRIPTION / PIPING-SPEC。 // pszDescOverride 非空时用它替换规格库里的"描述"(阀门用型号名比"BB"这类类型码更可读)。 bool writeSpecAttributes(std::ostream &out, const std::map &row , LPCTSTR pszDescOverride = NULL) { const CString sCode = specField(row, _T("元件代码")); CString sDesc = specField(row, _T("描述")); if (pszDescOverride && *pszDescOverride) sDesc = pszDescOverride; const CString sSpec = specField(row, _T("等级名称")); if (sCode.IsEmpty() && sDesc.IsEmpty()) return false; writePcfAttribute(out, 4, PCF_ATTR_ITEM_CODE, (LPCTSTR)sCode); writePcfAttribute(out, 4, PCF_ATTR_ITEM_DESCRIPTION, (LPCTSTR)sDesc); writePcfAttribute(out, 4, PCF_HEAD_PIPING_SPEC, (LPCTSTR)sSpec); return true; } // 数值 -> 规格库里的"公称直径"文本(库中以整数字符串存储,如 "50") CString dnText(const double &dn) { CString s; s.Format(_T("%d"), (int)(dn + 0.5)); return s; } /* pcf 文件头(仅单位部分)。PIPELINE-REFERENCE 见 pcf_pipeline_reference(), 因为设备(EQUIPMENT)定义块必须写在 PIPELINE-REFERENCE 之前。 */ bool pcf_begin(std::ostream &out , STR_PTR isogen_files = L"ISOGEN.FLS" , STR_PTR bore = PCF_UNITS_MM , STR_PTR co_ords = PCF_UNITS_MM , STR_PTR bolt_length = PCF_UNITS_MM , STR_PTR bolt_dia = PCF_UNITS_MM , STR_PTR weight = PCF_UNITS_KGS ) { if (!out.good()) return false; PcfRow(out, 0) << PCF_HEAD_ISOGEN_FILES << isogen_files; PcfRow(out, 0) << PCF_HEAD_UNITS_BORE << bore; PcfRow(out, 0) << PCF_HEAD_UNITS_CO_ORDS << co_ords; PcfRow(out, 0) << PCF_HEAD_UNITS_BOLT_LENGTH << bolt_length; PcfRow(out, 0) << PCF_HEAD_UNITS_BOLT_DIA << bolt_dia; PcfRow(out, 0) << PCF_HEAD_UNITS_WEIGHT << weight; return true; } // 管线引用(放在设备定义之后) bool pcf_pipeline_reference(std::ostream &out, STR_PTR pipeline_reference) { if (!out.good()) return false; PcfRow(out, 0) << PCF_HEAD_PIPELINE_REFERENCE << pipeline_reference; return true; } /* 遍历模型空间,收集所有 管道/管件/设备 的 ObjectId 及其管道系统(PIPE_SYS)。 管道系统存放在实体专业数据里,键为 PIPE_SYS。 */ void collectComponents(std::vector &ids, std::vector &systems) { ids.clear(); systems.clear(); AcDbDatabase *pDb = acdbHostApplicationServices()->workingDatabase(); if (!pDb) return; AcDbBlockTable *pTbl = NULL; if (Acad::eOk != pDb->getBlockTable(pTbl, AcDb::kForRead)) return; AcDbBlockTableRecord *pBTR = NULL; if (Acad::eOk != pTbl->getAt(ACDB_MODEL_SPACE, pBTR, AcDb::kForRead)) { pTbl->close(); return; } pTbl->close(); AcDbBlockTableRecordIterator *pIter = NULL; if (Acad::eOk != pBTR->newIterator(pIter)) { pBTR->close(); return; } AcDbEntity *pEnt = NULL; for (pIter->start(); !pIter->done(); pIter->step()) { if (Acad::eOk != pIter->getEntity(pEnt, AcDb::kForRead) || !pEnt) continue; // 注意:AecDbEwPipe / AecDbXEquipment 派生自 AecDbCurveBase(→AcDbCurve), // 与 AecDbEntityBase(→AcDbEntity) 是“兄弟”类,并非其派生类。 // 因此不能用 AecDbEntityBase::cast 去读管道的专业数据(会返回 NULL), // 必须按具体类型分别调用 GetAt,否则管道取不到 PIPE_SYS 而被漏掉。 CString sSys; bool bIsComponent = false; if (AecDbEwPipe *pPipe = AecDbEwPipe::cast(pEnt)) { pPipe->GetAt(PIPE_SYS, sSys); bIsComponent = true; } else if (AecDbEwFitting *pFit = AecDbEwFitting::cast(pEnt)) { pFit->GetAt(PIPE_SYS, sSys); bIsComponent = true; } else if (AecDbEwEquipment *pEqu = AecDbEwEquipment::cast(pEnt)) { pEqu->GetAt(PIPE_SYS, sSys); bIsComponent = true; } else if (AecDbXEquipment *pXEqu = AecDbXEquipment::cast(pEnt)) { pXEqu->GetAt(PIPE_SYS, sSys); bIsComponent = true; } if (bIsComponent) { ids.push_back(pEnt->objectId()); systems.push_back(sSys); } pEnt->close(); } DELETE_PTR(pIter); pBTR->close(); } // 收集图纸中出现过的所有管道系统(去重,忽略空值) void collectSystems(std::vector &out) { out.clear(); std::vector ids; std::vector systems; collectComponents(ids, systems); for (size_t k = 0; k < systems.size(); ++k) { if (systems[k].IsEmpty()) continue; bool bFound = false; for (size_t m = 0; m < out.size(); ++m) { if (out[m] == systems[k]) { bFound = true; break; } } if (!bFound) out.push_back(systems[k]); } } // 按管道系统(PIPE_SYS)收集 管道/管件/设备 bool getComponentBySystem(LPCTSTR pszSys, AcDbObjectIdArray &ids) { if (!pszSys || _tcslen(pszSys) == 0) return false; std::vector allIds; std::vector systems; collectComponents(allIds, systems); for (size_t k = 0; k < allIds.size(); ++k) { if (systems[k] == pszSys) ids.append(allIds[k]); } return !ids.isEmpty(); } /* * ================================================================================= * [PCF 降级说明:无 ITEM-CODE 时的保底策略] * 适用组件:PIPE (直管) * ================================================================================= * 如果当前管道没有物料码 (ITEM-CODE),你至少需要以下字段才能描述一段管道: * * 1. 语法绝对必填字段(仅画几何骨架): * - END-POINT (起点): 定义管道空间的起点三维坐标 (X, Y, Z) 及 该端的公称直径。 * - END-POINT (终点): 定义管道空间的终点三维坐标 (X, Y, Z) 及 该端的公称直径。 * *意义:有了这两个端点,引擎就能在两点之间画出一条中心线。这是底线。* * * 2. 工程强烈建议补充的字段(为了让下游软件不报错,且 BOM 表不为空): * - ITEM-DESCRIPTION : 极其重要!物料的文本描述(如 L"非标工艺直管"),下游软件的 BOM 会直接打印这句话。 * - WALL-THICKNESS : 壁厚数值。部分查看器(如 Navisworks)依赖“管径+壁厚”来推算真实的管道外径,不传可能会画出极细的线。 * - MATERIAL-IDENTIFIER : 材质(如 L"CS" 或 L"碳钢"),辅助下游进行管道应力分析。 * - FABRICATION-ITEM (或 ERECTION-ITEM) : 预制/现场安装标识。不带值(直接写属性名),表示这是一根工厂预制管。 * ================================================================================= */ // 处理普通管道,非法兰管道 // 直管规格查询:按 等级 + 公称直径 在 AecSpecPipe 中查物料码/描述/等级 bool getPipeSpecRow(CUIISQLiteServer *pDb, const CString &sLevel, const double &dn , std::map &row) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); return querySpecRow(pDb, _T("AecSpecPipe"), sLevel, filters, row); } // 读取数值型专业数据,兼容存为 double / 字符串 / 整数 三种情况。 // 实测部分图纸把 PIPE_DN / PIPE_THICK 存成字符串(如 "450"), // 若只用 double 读取会失败,导致直管被静默跳过。 template bool getNumericValue(T *pEnt, LPCTSTR pszKey, double &dVal) { if (!pEnt) return false; if (pEnt->GetAt(pszKey, dVal)) return true; CString s; if (pEnt->GetAt(pszKey, s) && !s.IsEmpty()) { dVal = _ttof(s); return true; } int n(0); if (pEnt->GetAt(pszKey, n)) { dVal = (double)n; return true; } return false; } // pStart/pEnd 用于"长直管中点切断":把一根管按参数段拆成多根虚拟管输出, // 传 NULL 时按管子自身的起止点输出。 bool pcf_pipe_normal(CUIISQLiteServer *pDb, AecDbEwPipe *pPipe, std::ostream &out , const AcGePoint3d *pStart = NULL, const AcGePoint3d *pEnd = NULL) { if (!pPipe || !out.good()) return false; AcGePoint3d ptStart = pStart ? *pStart : pPipe->StartPoint(); AcGePoint3d ptEnd = pEnd ? *pEnd : pPipe->EndPoint(); // 公称直径:先读专业数据(兼容字符串),再退回 PipeParam double bore(.0); if (!getNumericValue(pPipe, PIPE_DN, bore) || bore <= 0.0) { WPipe::PipeParam *pParam = pPipe->GetPipeParam(); if (pParam) bore = pParam->m_dDN; } if (bore <= 0.0) return false; // 壁厚:可选,缺失不阻断导出 double thickness(.0); if (!getNumericValue(pPipe, PIPE_THICK, thickness) || thickness <= 0.0) { WPipe::PipeParam *pParam = pPipe->GetPipeParam(); if (pParam) thickness = pParam->m_dThick; } // 等级:先读 PIPE_LEVEL,为空时在专业数据里反查合法等级名 AecDbObjXDataMap *pMap = pPipe->GetSpecialtyData(AcDb::kForRead); const CString level = resolveLevel(pMap, pDb); // 写入组件主类标 (顶格,无缩进) PcfRow(out, 0) << PCF_COMP_PIPE; // 写入起止坐标与管径 (必须紧跟在 PIPE 后面,缩进 4 格) PcfRow(out, 4) << PCF_PT_END_POINT << ptStart << bore; PcfRow(out, 4) << PCF_PT_END_POINT << ptEnd << bore; // 写入壁厚,保证在没有item-code和piping-spec时能够正常显示 if (thickness > 0.0) PcfRow(out, 4) << PCF_ATTR_WALL_THICKNESS << thickness; // 等级 -> 规格库 -> 物料码 / 描述 / 等级 if (!level.IsEmpty()) { std::map row; if (getPipeSpecRow(pDb, level, bore, row)) writeSpecAttributes(out, row); } // 预制管件 PcfRow(out, 4) << PCF_ATTR_FABRICATION_ITEM; out << "\n"; return true; } // ============================ Phase 2: 管件导出 ============================ // 读取管件某接口的管径:优先接口自身 dSize1,缺失时退回管件参数 double fitInfBore(AecDbEwFitting *pFit, const AecInterface &inf, int nInf) { double d = inf.dSize1; if (d <= 0.0 && pFit) { WPipe::PipeParam *pParam = pFit->GetPipeParam(nInf); if (pParam) d = pParam->m_dDN; } return d; } // 管件壁厚:优先专业数据(兼容字符串),再退回管件参数 double fitThickness(AecDbEwFitting *pFit) { double t(0.0); if (!getNumericValue(pFit, PIPE_THICK, t) || t <= 0.0) { WPipe::PipeParam *pParam = pFit ? pFit->GetPipeParam(0) : NULL; if (pParam) t = pParam->m_dThick; } return t; } // 变径类型:集中式/偏心式,用于选择 PCF 组件名 STR_PTR getReducerTypeName(AecDbEwFitting *pFit) { WPipe::PipeParam *pParam = pFit ? pFit->GetPipeParam(0) : NULL; if (pParam && pParam->m_eRed != WPipe::eCen) return PCF_COMP_REDUCER_ECC; return PCF_COMP_REDUCER_CONC; } // 弯头实际夹角:两接口方向向量的夹角(0~180°)。 // 90° 弯头约为 90°;45° 弯头约为 135°(即转弯 45°)。取不到返回 -1。 double fitBendAngle(AecDbEwFitting *pFit) { if (!pFit) return -1.0; AcArray arInfs; if (!pFit->GetInterface(arInfs) || arInfs.length() < 2) return -1.0; AcGeVector3d u = arInfs.at(0).vtDir; AcGeVector3d v = arInfs.at(1).vtDir; if (u.length() < 1e-9 || v.length() < 1e-9) return -1.0; u.normalize(); v.normalize(); double c = u.dotProduct(v); if (c > 1.0) c = 1.0; if (c < -1.0) c = -1.0; return acos(c) * 180.0 / 3.14159265358979323846; } /* 管件在规格库里的"类型"候选名(按优先级)。 实测 AecSpecFit.类型 只有: 弯头 / 三通 / 半管接头 / 补强圈 / 变径 / 管帽 / 斜三通 / 90度弯头 / 45度弯头 / 同心变径 / 偏心变径 而 AecSpecFlange.类型 只有 PL。 */ void fitTypeCandidates(int nType, AecDbEwFitting *pFit, std::vector &names) { names.clear(); switch (nType) { case eElbow: case eReduceElbow: // 弯头的 90/45 度在 getFitSpecRow 里按实测夹角单独挑行,这里只列候选名。 names.push_back(_T("弯头")); names.push_back(_T("90度弯头")); names.push_back(_T("45度弯头")); break; case eReduce: if (pFit && wcscmp(getReducerTypeName(pFit), PCF_COMP_REDUCER_ECC) == 0) names.push_back(_T("偏心变径")); else names.push_back(_T("同心变径")); names.push_back(_T("变径")); break; case eTee: names.push_back(_T("三通")); break; case eCross: names.push_back(_T("四通")); names.push_back(_T("三通")); break; case eLateral: case eWye: names.push_back(_T("斜三通")); names.push_back(_T("三通")); break; case eWcap: names.push_back(_T("管帽")); break; case eRP: names.push_back(_T("补强圈")); break; case eHP: names.push_back(_T("半管接头")); break; case eFlange: names.push_back(_T("PL")); break; } } // 管件规格查询:法兰走 AecSpecFlange,其余走 AecSpecFit。 // 普通管件按类型候选名逐个试,最后退化为"只按管径"。 // 弯头特殊处理:90/45 度有时写在"类型"列、有时只在"描述"里区分, // 需取回全部候选行后按实测夹角挑选。 bool getFitSpecRow(CUIISQLiteServer *pDb, AecDbEwFitting *pFit , const CString &sLevel, const double &dn, std::map &row) { if (!pFit || sLevel.IsEmpty() || dn <= 0.0) return false; const int nType = pFit->GetFittingType(); const bool bFlange = (nType == eFlange); LPCTSTR pszTable = bFlange ? _T("AecSpecFlange") : _T("AecSpecFit"); // ---- 弯头:按实测夹角在 90度/45度 行里挑 ---- if (nType == eElbow || nType == eReduceElbow) { std::vector > rows; const LPCTSTR aNames[3] = { _T("弯头"), _T("90度弯头"), _T("45度弯头") }; for (int i = 0; i < 3; ++i) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); filters.push_back(std::make_pair(CString(_T("类型")), CString(aNames[i]))); std::vector > part; if (querySpecRows(pDb, pszTable, sLevel, filters, part)) rows.insert(rows.end(), part.begin(), part.end()); } if (rows.empty()) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); querySpecRows(pDb, pszTable, sLevel, filters, rows); } if (rows.empty()) return false; const double ang = fitBendAngle(pFit); const bool bWant45 = (ang >= 112.5); // 夹角约 135° -> 45 度弯头 int nBest = -1; for (size_t i = 0; i < rows.size(); ++i) { CString sAll = specField(rows[i], _T("类型")) + specField(rows[i], _T("描述")); const bool bIs45 = (sAll.Find(_T("45度")) >= 0); const bool bIs90 = (sAll.Find(_T("90度")) >= 0); if (bWant45 && bIs45) { nBest = (int)i; break; } if (!bWant45 && bIs90) { nBest = (int)i; break; } } if (nBest < 0) nBest = 0; row = rows[nBest]; return true; } // ---- 其余管件:按类型候选名逐个试 ---- std::vector names; fitTypeCandidates(nType, pFit, names); for (size_t k = 0; k < names.size(); ++k) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); filters.push_back(std::make_pair(CString(_T("类型")), names[k])); if (querySpecRow(pDb, pszTable, sLevel, filters, row)) return true; } // 再退化为只按管径(类型名对不上时的兜底) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); if (querySpecRow(pDb, pszTable, sLevel, filters, row)) return true; } // 法兰表查不到时,退回管件表再试一次 if (bFlange) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(dn))); if (querySpecRow(pDb, _T("AecSpecFit"), sLevel, filters, row)) return true; } return false; } // 写出管件公共属性:壁厚 / 规格库(物料码,描述,等级) / 预制标记 void writeFittingCommon(CUIISQLiteServer *pDb, AecDbEwFitting *pFit , const double &bore, std::ostream &out) { double thickness = fitThickness(pFit); if (thickness > 0.0) PcfRow(out, 4) << PCF_ATTR_WALL_THICKNESS << thickness; AecDbObjXDataMap *pMap = pFit->GetSpecialtyData(AcDb::kForRead); const CString level = resolveLevel(pMap, pDb); if (!level.IsEmpty() && pDb && bore > 0.0) { std::map row; if (getFitSpecRow(pDb, pFit, level, bore, row)) writeSpecAttributes(out, row); } // 预制管件 PcfRow(out, 4) << PCF_ATTR_FABRICATION_ITEM; } // 求两条射线(点+方向)在空间中的交点。 // 弯头两端方向一般相交(如 90° 弯头),相交时返回角点;接近平行时退化为两端点中点。 AcGePoint3d intersectRays(const AcGePoint3d &p0, const AcGeVector3d &d0 , const AcGePoint3d &p1, const AcGeVector3d &d1) { AcGeVector3d u(d0), v(d1); u.normalize(); v.normalize(); AcGeVector3d w(p0 - p1); const double a = u.dotProduct(u); const double b = u.dotProduct(v); const double c = v.dotProduct(v); const double d = u.dotProduct(w); const double e = v.dotProduct(w); const double denom = a * c - b * b; if (fabs(denom) < 1e-9) return AcGePoint3d((p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5, (p0.z + p1.z) * 0.5); const double sc = (b * e - c * d) / denom; return p0 + u * sc; } // 方向向量 -> PCF 方位(NORTH/SOUTH/EAST/WEST/UP/DOWN) STR_PTR dirToOrientation(const AcGeVector3d &v) { const double ax = fabs(v.x), ay = fabs(v.y), az = fabs(v.z); if (az >= ax && az >= ay) return (v.z >= 0.0) ? L"UP" : L"DOWN"; if (ax >= ay) return (v.x >= 0.0) ? L"EAST" : L"WEST"; return (v.y >= 0.0) ? L"NORTH" : L"SOUTH"; } /* * 通用:写出组件头与点位(不含属性)。 * 参考 PCF Reference Guide 的点位要求: * 1 个接口 -> END-POINT * 2 个接口 -> END-POINT x2 * 2 个接口 + bCentre -> END-POINT x2 + CENTRE-POINT * 3 个接口 -> END-POINT x2 + BRANCH1-POINT * 4+ 个接口 -> END-POINT x2 + BRANCH1/2-POINT * 接口按 nLevel 排序:0/1=主管两端,>=2=支管。bores 与 arInfs 一一对应。 */ bool writeComponentPoints(std::ostream &out, AcArray &arInfs , const std::vector &bores, STR_PTR compType, bool bCentre, double &dBore) { if (!out.good() || arInfs.isEmpty()) return false; // 按 nLevel 升序排序(插入排序,接口数很少) std::vector arOrder; for (int i = 0; i < arInfs.length(); ++i) { int lv = arInfs.at(i).nLevel; size_t nPos = arOrder.size(); for (size_t k = 0; k < arOrder.size(); ++k) { if (lv < arInfs.at(arOrder[k]).nLevel) { nPos = k; break; } } arOrder.insert(arOrder.begin() + nPos, i); } const int nCount = (int)arOrder.size(); const int nMain = nCount >= 2 ? 2 : 1; PcfRow(out, 0) << compType; dBore = 0.0; for (int i = 0; i < nMain; ++i) { const int idx = arOrder[i]; double d = (idx < (int)bores.size()) ? bores[idx] : arInfs.at(idx).dSize1; if (dBore <= 0.0) dBore = d; PcfRow(out, 4) << PCF_PT_END_POINT << arInfs.at(idx).ptPos << d; } if (bCentre && nCount >= 2) { const AecInterface &i0 = arInfs.at(arOrder[0]); const AecInterface &i1 = arInfs.at(arOrder[1]); PcfRow(out, 4) << PCF_PT_CENTRE_POINT << intersectRays(i0.ptPos, i0.vtDir, i1.ptPos, i1.vtDir); } for (int i = 2; i < nCount; ++i) { const int idx = arOrder[i]; double d = (idx < (int)bores.size()) ? bores[idx] : arInfs.at(idx).dSize1; STR_PTR ptType = (i == 2) ? PCF_PT_BRANCH1_POINT : PCF_PT_BRANCH2_POINT; PcfRow(out, 4) << ptType << arInfs.at(idx).ptPos << d; } return true; } // 管件:接口管径已解析后写入点位,再补管件属性 bool pcf_genericFitting(CUIISQLiteServer *pDb, AecDbEwFitting *pFit , STR_PTR compType, std::ostream &out, bool bCentre) { if (!pFit || !out.good()) return false; AcArray arInfs; if (!pFit->GetInterface(arInfs) || arInfs.isEmpty()) return false; std::vector bores; for (int i = 0; i < arInfs.length(); ++i) bores.push_back(fitInfBore(pFit, arInfs.at(i), i)); double dBore = 0.0; if (!writeComponentPoints(out, arInfs, bores, compType, bCentre, dBore)) return false; writeFittingCommon(pDb, pFit, dBore, out); out << "\n"; return true; } // ============================ Phase 3: 设备导出 ============================ // 阀门类型枚举(AecEqui::ValveType, EarThwormDef.h:2267) -> 规格库 AecSpecValve.[类别] 名称。 // 名称须与 g_vlvType[] (PipelineCmd/command.cpp:158) 及 Plumbing.db 的 [类别] 值一致。 LPCTSTR valveCategoryName(int nType) { switch (nType) { case 1: return _T("闸阀"); case 2: return _T("截止阀"); case 3: return _T("球阀"); case 4: return _T("止回阀"); case 5: return _T("蝶阀"); case 6: return _T("角阀"); case 7: return _T("安全阀"); case 8: return _T("三通阀"); case 9: return _T("四通阀"); case 10: return _T("调节阀"); case 11: return _T("疏水阀"); case 12: return _T("旋塞阀"); case 13: return _T("隔膜阀"); case 14: return _T("减压阀"); case 15: return _T("电磁阀"); case 16: return _T("橡胶接头"); case 17: return _T("排气阀"); case 18: return _T("排泥阀"); case 19: return _T("浆液阀"); case 20: return _T("闸门"); case 21: return _T("孔板"); case 22: return _T("桶式过滤器"); case 23: return _T("临时过滤器"); case 24: return _T("T型过滤器"); case 25: return _T("T型过滤器"); case 26: return _T("视镜"); case 27: return _T("流量计"); case 28: return _T("阻火器"); case 31: return _T("伸缩接头"); case 32: return _T("传力接头"); default: return _T(""); } } // 写出阀门/管道附件类设备的公共属性:壁厚 / 名称 / 阀门规格库 / 预制标记 void writeEquipmentCommon(CUIISQLiteServer *pDb, AecDbEwEquipment *pEqu , const double &bore, std::ostream &out) { double thickness(0.0); if (getNumericValue(pEqu, PIPE_THICK, thickness) && thickness > 0.0) PcfRow(out, 4) << PCF_ATTR_WALL_THICKNESS << thickness; // 名称:阀门型号名(如 D373H法兰硬密封蝶阀DN450),比规格库"描述"更具可读性,用作 ITEM-DESCRIPTION CString sName; pEqu->GetAt(KEY_NAME, sName); // 阀门规格库:按 等级 + 类别(阀门类型) + 公称直径 在 AecSpecValve 中查物料码/描述/等级。 // 同一等级+管径下闸阀/止回阀/蝶阀等多行并存,必须再按"类别"过滤,否则会取错行。 bool bSpec = false; AecDbObjXDataMap *pMap = pEqu->GetSpecialtyData(AcDb::kForRead); const CString level = resolveLevel(pMap, pDb); if (!level.IsEmpty() && pDb && bore > 0.0) { int nVlvType = -1; if (pMap) pMap->GetAt(PIPE_VALVE, nVlvType); const LPCTSTR pszCat = valveCategoryName(nVlvType); // 必须按 类别 精确匹配:同一等级+管径下闸阀/止回阀/蝶阀等并存, // 类别未知或库中无此类别时宁可不写物料码,也不能退化为按管径取到别的阀种。 std::map row; bool bFound = false; if (pszCat && *pszCat) { std::vector > filters; filters.push_back(std::make_pair(CString(_T("公称直径")), dnText(bore))); filters.push_back(std::make_pair(CString(_T("类别")), CString(pszCat))); bFound = querySpecRow(pDb, _T("AecSpecValve"), level, filters, row); } if (bFound) bSpec = writeSpecAttributes(out, row, (LPCTSTR)sName); } if (!bSpec) writePcfAttribute(out, 4, PCF_ATTR_ITEM_DESCRIPTION, (LPCTSTR)sName); PcfRow(out, 4) << PCF_ATTR_FABRICATION_ITEM; } // 阀门/管道附件类设备(AecDbEwEquipment):作为 VALVE 组件导出 bool pcf_ewEquipment(CUIISQLiteServer *pDb, AecDbEwEquipment *pEqu, std::ostream &out) { if (!pEqu || !out.good()) return false; AcArray arInfs; if (!pEqu->GetInterface(arInfs) || arInfs.isEmpty()) return false; std::vector bores; for (int i = 0; i < arInfs.length(); ++i) bores.push_back(arInfs.at(i).dSize1); double dBore = 0.0; if (!writeComponentPoints(out, arInfs, bores, PCF_COMP_VALVE, true, dBore)) return false; writeEquipmentCommon(pDb, pEqu, dBore, out); out << "\n"; return true; } // 容器/设备类(AecDbXEquipment):导出为 EQUIPMENT 定义块(含 NOZZLE),需放在 PIPELINE-REFERENCE 之前 bool pcf_xEquipment(CUIISQLiteServer *pDb, AecDbXEquipment *pXEqu, std::ostream &out) { if (!pXEqu || !out.good()) return false; AcGePoint3d ptIns; AcGeVector3d vtDir; pXEqu->GetInsert(ptIns, vtDir); // 设备名:优先 NAME,其次唯一名,最后给个兜底 CString sName; pXEqu->GetAt(KEY_NAME, sName); if (sName.IsEmpty()) sName = pXEqu->getUniqueName(); if (sName.IsEmpty()) sName = _T("EQUIPMENT"); PcfRow(out, 0) << PCF_COMP_EQUIPMENT; PcfRow(out, 4) << PCF_KW_REFERENCE << (LPCTSTR)sName; PcfRow(out, 4) << PCF_KW_LOCATION << ptIns; PcfRow(out, 4) << PCF_KW_ORIENTATION << dirToOrientation(vtDir); // 管嘴 AcArray arInfs; if (pXEqu->GetInterface(arInfs)) { for (int i = 0; i < arInfs.length(); ++i) { const AecInterface &inf = arInfs.at(i); CString sNoz; sNoz.Format(_T("%sN%d"), (LPCTSTR)sName, i + 1); PcfRow(out, 4) << PCF_KW_NOZZLE; PcfRow(out, 8) << PCF_KW_REFERENCE << (LPCTSTR)sNoz; PcfRow(out, 8) << PCF_KW_DIRECTION << dirToOrientation(inf.vtDir); PcfRow(out, 8) << PCF_PT_CO_ORDS << inf.ptPos << inf.dSize1; } } out << "\n"; return true; } // 管件按 EFittingType 分发到通用导出 bool pcf_fitting(CUIISQLiteServer *pDb, AecDbEwFitting *pFit, std::ostream &out) { if (!pFit) return false; switch (pFit->GetFittingType()) { case eElbow: case eReduceElbow: return pcf_genericFitting(pDb, pFit, PCF_COMP_ELBOW, out, true); case eReduce: return pcf_genericFitting(pDb, pFit, getReducerTypeName(pFit), out, false); case eTee: case eWye: case eLateral: return pcf_genericFitting(pDb, pFit, PCF_COMP_TEE, out, false); case eCross: return pcf_genericFitting(pDb, pFit, PCF_COMP_CROSS, out, false); case eFlange: return pcf_genericFitting(pDb, pFit, PCF_COMP_FLANGE, out, false); case eWcap: return pcf_genericFitting(pDb, pFit, PCF_COMP_CAP, out, false); case eSPB://八字盲板 return pcf_genericFitting(pDb, pFit, PCF_COMP_BLIND_FLANGE, out, false); case eGastket://垫片 return pcf_genericFitting(pDb, pFit, PCF_COMP_GASKET, out, false); case eFlxJoin://箍 case eThrough://直通 case eUJoin://活接 case eRJoin://缩接 case eOJoin://外螺纹接头 case eIJoin://内螺纹接头 return pcf_genericFitting(pDb, pFit, PCF_COMP_COUPLING, out, false); default: break; } return false; } // ============================ Phase 4: 支吊架导出 ============================ // 点到线段的最短距离 double distPointSegment(const AcGePoint3d &p, const AcGePoint3d &a, const AcGePoint3d &b) { AcGeVector3d ab(b - a); const double len2 = ab.lengthSqrd(); if (len2 < 1e-12) return p.distanceTo(a); double t = AcGeVector3d(p - a).dotProduct(ab) / len2; if (t < 0.0) t = 0.0; else if (t > 1.0) t = 1.0; return p.distanceTo(a + ab * t); } // 取组件主轴线段(两个 nLevel<2 的接口);接口不足两个时退化为一个点 bool mainAxisSegment(AcArray &arInfs, AcGePoint3d &a, AcGePoint3d &b) { if (arInfs.isEmpty()) return false; std::vector arMain; for (int i = 0; i < arInfs.length(); ++i) { if (arInfs.at(i).nLevel < 2) arMain.push_back(&arInfs.at(i)); } if (arMain.empty()) arMain.push_back(&arInfs.at(0)); a = arMain[0]->ptPos; b = (arMain.size() >= 2) ? arMain[1]->ptPos : a; return true; } // 支吊架既没有 PIPE_SYS,也没有宿主管道句柄,只能按空间位置就近归属到组件, // 再取该组件的管道系统。这里把组件的中心线段连同系统名收集起来备用。 struct SupportTarget { CString sys; AcGePoint3d a; AcGePoint3d b; }; void collectSupportTargets(std::vector &targets) { targets.clear(); AcDbDatabase *pDb = acdbHostApplicationServices()->workingDatabase(); if (!pDb) return; AcDbBlockTable *pTbl = NULL; if (Acad::eOk != pDb->getBlockTable(pTbl, AcDb::kForRead)) return; AcDbBlockTableRecord *pBTR = NULL; if (Acad::eOk != pTbl->getAt(ACDB_MODEL_SPACE, pBTR, AcDb::kForRead)) { pTbl->close(); return; } pTbl->close(); AcDbBlockTableRecordIterator *pIter = NULL; if (Acad::eOk != pBTR->newIterator(pIter)) { pBTR->close(); return; } AcDbEntity *pEnt = NULL; for (pIter->start(); !pIter->done(); pIter->step()) { if (Acad::eOk != pIter->getEntity(pEnt, AcDb::kForRead) || !pEnt) continue; SupportTarget t; bool bOk = false; if (AecDbEwPipe *pPipe = AecDbEwPipe::cast(pEnt)) { t.a = pPipe->StartPoint(); t.b = pPipe->EndPoint(); pPipe->GetAt(PIPE_SYS, t.sys); bOk = true; } else if (AecDbEwFitting *pFit = AecDbEwFitting::cast(pEnt)) { AcArray arInfs; if (pFit->GetInterface(arInfs) && mainAxisSegment(arInfs, t.a, t.b)) { pFit->GetAt(PIPE_SYS, t.sys); bOk = true; } } else if (AecDbEwEquipment *pEqu = AecDbEwEquipment::cast(pEnt)) { AcArray arInfs; if (pEqu->GetInterface(arInfs) && mainAxisSegment(arInfs, t.a, t.b)) { pEqu->GetAt(PIPE_SYS, t.sys); bOk = true; } } else if (AecDbXEquipment *pXEqu = AecDbXEquipment::cast(pEnt)) { AcArray arInfs; if (pXEqu->GetInterface(arInfs) && mainAxisSegment(arInfs, t.a, t.b)) { pXEqu->GetAt(PIPE_SYS, t.sys); bOk = true; } } pEnt->close(); if (bOk && !t.sys.IsEmpty()) targets.push_back(t); } DELETE_PTR(pIter); pBTR->close(); } // 支吊架(插入点 pt)是否属于指定管道系统:取最近组件所属的系统 bool supportBelongsToSystem(const AcGePoint3d &pt, LPCTSTR pszSys , const std::vector &targets) { double dMin = 0.0; CString sBest; for (size_t k = 0; k < targets.size(); ++k) { const double d = distPointSegment(pt, targets[k].a, targets[k].b); if (sBest.IsEmpty() || d < dMin) { dMin = d; sBest = targets[k].sys; } } return (sBest == pszSys); } // 收集图纸中所有支吊架(AecDbPipeHanger, DXF 名 AEC_PIPEHNGR) void collectPipeHangers(std::vector &ids) { ids.clear(); AcDbDatabase *pDb = acdbHostApplicationServices()->workingDatabase(); if (!pDb) return; AcDbBlockTable *pTbl = NULL; if (Acad::eOk != pDb->getBlockTable(pTbl, AcDb::kForRead)) return; AcDbBlockTableRecord *pBTR = NULL; if (Acad::eOk != pTbl->getAt(ACDB_MODEL_SPACE, pBTR, AcDb::kForRead)) { pTbl->close(); return; } pTbl->close(); AcDbBlockTableRecordIterator *pIter = NULL; if (Acad::eOk != pBTR->newIterator(pIter)) { pBTR->close(); return; } AcDbEntity *pEnt = NULL; for (pIter->start(); !pIter->done(); pIter->step()) { if (Acad::eOk != pIter->getEntity(pEnt, AcDb::kForRead) || !pEnt) continue; if (AecDbPipeHanger::cast(pEnt)) ids.push_back(pEnt->objectId()); pEnt->close(); } DELETE_PTR(pIter); pBTR->close(); } /* 支吊架 -> SUPPORT 组件块(单坐标)。 参考 PCF Reference Guide: SUPPORT 必填 CO-ORDS(含公称尺寸) + SKEY;默认 SKEY 为 01HG(双平行线支架符号); CATEGORY 取 ERECTION(现场安装);名称走 NAME / ITEM-DESCRIPTION。 支架类型(0~4)不是 PCF 标准属性,借 COMPONENT-ATTRIBUTE0 带出,方便下游重映射符号。 */ bool pcf_support(AecDbPipeHanger *pHanger, std::ostream &out) { if (!pHanger || !out.good()) return false; const AcGePoint3d pt = pHanger->position(); double dn(0.0); getNumericValue(pHanger, PCF_KEY_SUPPORT_DN, dn); PcfRow(out, 0) << PCF_COMP_SUPPORT; if (dn > 0.0) PcfRow(out, 4) << PCF_PT_CO_ORDS << pt << dn; else PcfRow(out, 4) << PCF_PT_CO_ORDS << pt; PcfRow(out, 4) << PCF_ATTR_SKEY << PCF_SKEY_SUPPORT_DEFAULT; CString sName; pHanger->GetAt(KEY_NAME, sName); writePcfAttribute(out, 4, PCF_ATTR_NAME, (LPCTSTR)sName); writePcfAttribute(out, 4, PCF_ATTR_ITEM_DESCRIPTION, (LPCTSTR)sName); int nMode(-1); if (pHanger->GetAt(PCF_KEY_SUPPORT_MODE, nMode)) { CString sMode; sMode.Format(_T("%d"), nMode); writePcfAttribute(out, 4, PCF_ATTR_COMPONENT_ATTRIBUTE0, (LPCTSTR)sMode); } PcfRow(out, 4) << PCF_KW_CATEGORY << PCF_CAT_ERECTION; out << "\n"; return true; } // ============================ Phase 5: 自动分图 ============================ // 接图标识的 PCF 属性名。写在下游组件属性之后,随该组件一起被识别。 STR_PTR PCF_ATTR_CONTINUATION = L"MESSAGE-TEXT"; // ---- 5.1 组件通用读取 ---- // 候选"管号"键,按优先级排列。 // 注意:工程里"管号"存在 PIPE_NO("No"),但"改管"路径 // (PipelineCmd\AecPipeLine.cpp:3647) 会用"顺序号"把 PIPE_NO 覆盖掉, // 所以不能硬编码,由 probeGroupKey() 实测后挑一个可用的键。 LPCTSTR g_pszNoKeys[] = { PIPE_NO, // "No" 管号(可能已被顺序号覆盖) PIPE_LINENO, // "LineNo" 管线号(PID 用,现场常为空) PIPE_SPEC, // "Spec" 管子上标 KEY_NO, // "No." 顺序号 PIPE_UNITNO, // "UnitNo" 单元号 PIPE_ZONE, // "Zone" 区号 }; template bool readKeyString(T *pEnt, LPCTSTR pszKey, CString &s) { s.Empty(); if (!pEnt || !pszKey) return false; if (pEnt->GetAt(pszKey, s) && !s.IsEmpty()) return true; s.Empty(); return false; } // 管道/管件/设备是"兄弟"类,必须分别 cast 才能读到专业数据 bool readComponentKey(AcDbObject *pObj, LPCTSTR pszKey, CString &s) { s.Empty(); if (!pObj) return false; if (AecDbEwPipe *pPipe = AecDbEwPipe::cast(pObj)) return readKeyString(pPipe, pszKey, s); if (AecDbEwFitting *pFit = AecDbEwFitting::cast(pObj)) return readKeyString(pFit, pszKey, s); if (AecDbEwEquipment *pEqu = AecDbEwEquipment::cast(pObj)) return readKeyString(pEqu, pszKey, s); if (AecDbXEquipment *pXEqu = AecDbXEquipment::cast(pObj)) return readKeyString(pXEqu, pszKey, s); return false; } // 组件的 EFittingType;非本插件组件返回 -1 int getCompType(AcDbObject *pObj) { if (!pObj) return -1; if (AecDbEwPipe::cast(pObj)) return ePipe; if (AecDbEwFitting *pFit = AecDbEwFitting::cast(pObj)) return pFit->GetFittingType(); if (AecDbEwEquipment::cast(pObj)) return eEquipment; if (AecDbXEquipment::cast(pObj)) return eEquipment; return -1; } // ---- 5.3 网络图与切点 ---- // 网络里的一个组件节点(长直管被切断时,同一实体可对应多个节点) struct NetNode { AcDbObjectId id; int nType; // EFITTINGTYPE AcGePoint3d a, b; // 该节点占据的主轴线段 AcArray infs; int nSplitIdx; // 对应的 PipeSplit 下标(用于把切段精确落到图上) NetNode() : nType(-1), nSplitIdx(-1) {} }; // 一个可用于连接的接口点 struct ConnPt { AcGePoint3d pt; int nConn; // AecInterface::nConnectType: 0焊接 1承插 2螺纹 3法兰 4卡箍 double bore; // 接口管径 ConnPt() : nConn(-1), bore(0.0) {} }; // 两个组件之间的连接 —— 潜在切点 / 焊缝 struct NetJoint { int nA, nB; // NetNode 下标 AcGePoint3d pt; // 接口位置 bool bFlange; // 至少一端是法兰 -> 法兰面 bool bWeld; // 焊接接口 -> 需要输出 WELD 记录 int nConnA, nConnB; // 两侧接口的连接类型 double dBore; // 接口管径 NetJoint() : nA(-1), nB(-1), bFlange(false), bWeld(false) , nConnA(-1), nConnB(-1), dBore(0.0) {} }; // 切点优先级:法兰面(2) > 现场焊缝(1) > 普通接口(0) int jointPriority(const NetJoint &j) { if (j.bFlange) return 2; if (j.bWeld) return 1; return 0; } // 直管某端(起点/终点)的接口连接类型;接口数据不足时返回 -1 int pipeEndConnect(const NetNode &nd, const AcGePoint3d &pt) { int nBest = -1; double dMin = 1e18; for (int i = 0; i < nd.infs.length(); ++i) { const double d = nd.infs.at(i).ptPos.distanceTo(pt); if (d < dMin) { dMin = d; nBest = nd.infs.at(i).nConnectType; } } return nBest; } // 直管某端的接口管径 double pipeEndBore(const NetNode &nd, const AcGePoint3d &pt) { double dBore = 0.0; double dMin = 1e18; for (int i = 0; i < nd.infs.length(); ++i) { const double d = nd.infs.at(i).ptPos.distanceTo(pt); if (d < dMin) { dMin = d; dBore = nd.infs.at(i).dSize1; } } return dBore; } // 节点上所有可用于连接的接口(带连接类型与管径) void nodeConnPoints(const NetNode &nd, std::vector &pts) { pts.clear(); if (nd.nType == ePipe) { ConnPt c0, c1; c0.pt = nd.a; c0.nConn = pipeEndConnect(nd, nd.a); c0.bore = pipeEndBore(nd, nd.a); c1.pt = nd.b; c1.nConn = pipeEndConnect(nd, nd.b); c1.bore = pipeEndBore(nd, nd.b); pts.push_back(c0); pts.push_back(c1); return; } for (int i = 0; i < nd.infs.length(); ++i) { ConnPt c; c.pt = nd.infs.at(i).ptPos; c.nConn = nd.infs.at(i).nConnectType; c.bore = nd.infs.at(i).dSize1; pts.push_back(c); } if (pts.empty()) { ConnPt c0, c1; c0.pt = nd.a; pts.push_back(c0); if (nd.a.distanceTo(nd.b) > 1e-9) { c1.pt = nd.b; pts.push_back(c1); } } } // 接口重合判定的容差(图纸单位)。本图纸里相邻组件的接口位置是严格重合的, // 取 0.01 足够;不再借用底层按垫片尺寸(dGasket)放大的容差。 double netTolerance() { double dUnit = 1.0; struct resbuf rb; if (acedGetVar(_T("INSUNITS"), &rb) == RTNORM && rb.resval.rint == AcDb::kUnitsMeters) dUnit = 0.001; return __max(0.01 * dUnit, 0.01); } // 读一个实体的接口与主轴线段(接口不足两个时退化为点) bool readAxisSegment(AcDbObject *pObj, AcArray &infs , AcGePoint3d &a, AcGePoint3d &b) { if (AecDbEwPipe *pPipe = AecDbEwPipe::cast(pObj)) { a = pPipe->StartPoint(); b = pPipe->EndPoint(); pPipe->GetInterface(infs); return true; } if (AecDbEwFitting *pFit = AecDbEwFitting::cast(pObj)) { pFit->GetInterface(infs); } else if (AecDbEwEquipment *pEqu = AecDbEwEquipment::cast(pObj)) { pEqu->GetInterface(infs); } else if (AecDbXEquipment *pXEqu = AecDbXEquipment::cast(pObj)) { pXEqu->GetInterface(infs); } else { return false; } if (!mainAxisSegment(infs, a, b)) { a = b = AcGePoint3d::kOrigin; return false; } return true; } /* 组装网络图。 dSplitLen > 0 时,长度超过 dSplitLen 的直管会被拆成多段"虚拟节点" (按 dSplitLen 等分),这样后续既可以在节点数上控制图幅,也能在长直管 的中点处自然形成切点(符合"长直管段中心点切断"的要求)。 pipeSplits 回传每个被拆直管的切段参数,供写 PCF 时输出多根虚拟管。 */ struct PipeSplit { AcDbObjectId id; double t0, t1; // 沿管子起点->终点方向的归一化参数 }; void buildNetwork(const AcDbObjectIdArray &ids, double dSplitLen , std::vector &nodes, std::vector &joints , std::vector &pipeSplits) { nodes.clear(); joints.clear(); pipeSplits.clear(); for (int i = 0; i < ids.length(); ++i) { AcDbObjectPointer pObj(ids[i], AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; const int nType = getCompType(pObj.object()); if (nType < 0) continue; AcGePoint3d a, b; AcArray infs; if (!readAxisSegment(pObj.object(), infs, a, b)) continue; // 长直管:按 dSplitLen 等分,逐段建节点 const double dLen = a.distanceTo(b); if (dSplitLen > 0.0 && nType == ePipe && dLen > dSplitLen * 1.5) { const int nSeg = (int)(dLen / dSplitLen) + 1; const AcGeVector3d vt(b - a); for (int k = 0; k < nSeg; ++k) { const double t0 = (double)k / nSeg; const double t1 = (double)(k + 1) / nSeg; NetNode nd; nd.id = ids[i]; nd.nType = ePipe; nd.a = a + vt * t0; nd.b = a + vt * t1; nd.infs = infs; // 保留接口,供判断端部连接类型(焊接/法兰) nd.nSplitIdx = (int)pipeSplits.size(); nodes.push_back(nd); PipeSplit sp; sp.id = ids[i]; sp.t0 = t0; sp.t1 = t1; pipeSplits.push_back(sp); } continue; } NetNode nd; nd.id = ids[i]; nd.nType = nType; nd.a = a; nd.b = b; nd.infs = infs; nd.nSplitIdx = (int)pipeSplits.size(); nodes.push_back(nd); PipeSplit sp; sp.id = ids[i]; sp.t0 = 0.0; sp.t1 = 1.0; pipeSplits.push_back(sp); } // 连接关系:接口位置重合即视为相接 const double dTol = netTolerance(); for (size_t i = 0; i < nodes.size(); ++i) { std::vector ptI; nodeConnPoints(nodes[i], ptI); for (size_t k = i + 1; k < nodes.size(); ++k) { std::vector ptK; nodeConnPoints(nodes[k], ptK); int nHitX = -1, nHitY = -1; for (size_t x = 0; x < ptI.size() && nHitX < 0; ++x) { for (size_t y = 0; y < ptK.size() && nHitX < 0; ++y) { if (ptI[x].pt.distanceTo(ptK[y].pt) <= dTol) { nHitX = (int)x; nHitY = (int)y; } } } if (nHitX < 0) continue; NetJoint jt; jt.nA = (int)i; jt.nB = (int)k; jt.pt = ptI[nHitX].pt; jt.nConnA = ptI[nHitX].nConn; jt.nConnB = ptK[nHitY].nConn; jt.dBore = (ptI[nHitX].bore > 0.0) ? ptI[nHitX].bore : ptK[nHitY].bore; // 法兰面:一端是法兰管件,或某一侧接口本身就是法兰连接(3) jt.bFlange = (nodes[i].nType == eFlange) || (nodes[k].nType == eFlange) || (jt.nConnA == 3) || (jt.nConnB == 3); /* 螺栓连接的法兰面不算焊口: - 任一接口的连接类型是"法兰(3)";或 - 一端是法兰管件,另一端是阀门/设备(均以 eEquipment 表示)或另一片法兰。 注意:法兰的另一端(焊颈)与直管/管件相接仍是焊口,故不能"凡法兰即不算焊"。 */ bool bBoltedFace = (jt.nConnA == 3) || (jt.nConnB == 3); if (!bBoltedFace && (nodes[i].nType == eFlange || nodes[k].nType == eFlange)) { const int nOther = (nodes[i].nType == eFlange) ? nodes[k].nType : nodes[i].nType; if (nOther == eEquipment || nOther == eFlange) bBoltedFace = true; } // 焊接接头:两侧接口的连接类型都是"焊接(0)",且不是螺栓法兰面。 // 连接类型取不到(-1,接口数据缺失)时,退回"非螺栓面且至少一端是直管"的旧判据。 if (bBoltedFace) jt.bWeld = false; else if (jt.nConnA == 0 && jt.nConnB == 0) jt.bWeld = true; else if (jt.nConnA < 0 && jt.nConnB < 0) jt.bWeld = (nodes[i].nType == ePipe || nodes[k].nType == ePipe); joints.push_back(jt); } } } // ---- 5.3b 连通子网剥离 ---- /* 把一组组件按"接口相接"的连通性拆成若干连通子网。 连通性完全以本文件的接口重合判定为准(即 buildNetwork 建的图),这样 "拆子网"、"选切点"、"跨图续接标注"三处用的是同一套几何规则,结果自洽。 不用底层 GetPipeNetNodes:它的容差取的是目标接口的垫片尺寸(dGasket), 在本图纸上会把接口位置完全重合(距离 0)的法兰与阀门判成不连通,导致 一个系统被切成上百个碎片。 */ void splitNetByConnectivity(const AcDbObjectIdArray &ids, std::vector &nets) { nets.clear(); if (ids.isEmpty()) return; std::vector nodes; std::vector joints; std::vector splits; buildNetwork(ids, 0.0, nodes, joints, splits); const int n = (int)nodes.size(); if (n <= 0) return; // 并查集:接口重合即视为同一子网 std::vector par(n); for (int i = 0; i < n; ++i) par[i] = i; for (size_t j = 0; j < joints.size(); ++j) { int a = joints[j].nA, b = joints[j].nB; while (par[a] != a) { par[a] = par[par[a]]; a = par[a]; } while (par[b] != b) { par[b] = par[par[b]]; b = par[b]; } if (a != b) par[a] = b; } // 按根聚拢;节点顺序沿用系统内的组件顺序,保证导出顺序稳定 std::map rootToNet; for (int i = 0; i < n; ++i) { int r = i; while (par[r] != r) { par[r] = par[par[r]]; r = par[r]; } std::map::iterator it = rootToNet.find(r); int idx; if (it == rootToNet.end()) { idx = (int)nets.size(); rootToNet.insert(std::make_pair(r, idx)); nets.push_back(AcDbObjectIdArray()); } else { idx = it->second; } if (nets[idx].find(nodes[i].id) == -1) nets[idx].append(nodes[i].id); } acutPrintf(_T("\n连通子网 %d 个。"), (int)nets.size()); } // 管号里常见的占位值(空、全分隔符)视为"没填" bool isBlankKey(const CString &s) { if (s.IsEmpty()) return true; for (int i = 0; i < s.GetLength(); ++i) { const TCHAR c = s[i]; if (c != _T('-') && c != _T('_') && c != _T('.') && c != _T(' ') && c != _T('/')) return false; } return true; } /* 取一个连通子网的"管号":子网内出现次数最多的非空管号。 阀门/附件这类不带管号的组件因此会跟着它所在的那条管线走, 而不是被单独分出去。取不到时退回系统名。 */ CString dominantKey(const AcDbObjectIdArray &ids, LPCTSTR pszKey, LPCTSTR pszDefault) { CString sDefault(pszDefault ? pszDefault : _T("")); if (!pszKey || _tcslen(pszKey) == 0) return sDefault; std::map hist; for (int i = 0; i < ids.length(); ++i) { AcDbObjectPointer pObj(ids[i], AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; CString s; if (!readComponentKey(pObj.object(), pszKey, s) || isBlankKey(s)) continue; hist[s]++; } CString sBest; int nBest = 0; for (std::map::const_iterator it = hist.begin(); it != hist.end(); ++it) { if (it->second > nBest) { nBest = it->second; sBest = it->first; } } return sBest.IsEmpty() ? sDefault : sBest; } // ---- 5.4 尺寸/密度二次切分 ---- /* 一张图放不下时,沿管线走向从一端开始累加组件;一旦再加就会超过 nMaxNodes 个组件或 dMaxSpan 的跨度,就回退到"最近的优先切点"再切。 因为只在组件之间的接口处切,所以绝不会切在弯头/三通内部。 */ void splitBySize(const std::vector &nodes, const std::vector &joints , int nMaxNodes, double dMaxSpan, std::vector > &sheets) { sheets.clear(); const int n = (int)nodes.size(); if (n <= 0) return; // 邻接表:node -> (邻居, joint 下标) std::vector > > adj(n); for (int j = 0; j < (int)joints.size(); ++j) { adj[joints[j].nA].push_back(std::make_pair(joints[j].nB, j)); adj[joints[j].nB].push_back(std::make_pair(joints[j].nA, j)); } std::vector used(n, 0); for (int s = 0; s < n; ++s) { if (used[s]) continue; // 优先从"端点"(度为 1)起步,符合管线走向 int start = s; for (int k = 0; k < n; ++k) { if (!used[k] && adj[k].size() <= 1) { start = k; break; } } std::vector order; // 遍历顺序 std::vector entry; // 进入 order[k] 所用的 joint;order[0] = -1 std::vector stack; order.push_back(start); entry.push_back(-1); used[start] = 1; stack.push_back(start); AcGePoint3d ptMin, ptMax; bool bBox = false; while (!stack.empty()) { const int u = stack.back(); int v = -1, je = -1; for (size_t t = 0; t < adj[u].size(); ++t) { const int w = adj[u][t].first; if (used[w]) continue; v = w; je = adj[u][t].second; break; } if (v < 0) { stack.pop_back(); continue; } // 组件数上限 if (nMaxNodes > 0 && (int)order.size() >= nMaxNodes) break; // 空间跨度上限 if (dMaxSpan > 0.0) { AcGePoint3d mn = nodes[v].a, mx = nodes[v].b; if (mn.x > mx.x) { const double t = mn.x; mn.x = mx.x; mx.x = t; } if (mn.y > mx.y) { const double t = mn.y; mn.y = mx.y; mx.y = t; } if (mn.z > mx.z) { const double t = mn.z; mn.z = mx.z; mx.z = t; } AcGePoint3d tmn = mn, tmx = mx; if (bBox) { if (ptMin.x < tmn.x) tmn.x = ptMin.x; if (ptMin.y < tmn.y) tmn.y = ptMin.y; if (ptMin.z < tmn.z) tmn.z = ptMin.z; if (ptMax.x > tmx.x) tmx.x = ptMax.x; if (ptMax.y > tmx.y) tmx.y = ptMax.y; if (ptMax.z > tmx.z) tmx.z = ptMax.z; } const double dx = tmx.x - tmn.x; const double dy = tmx.y - tmn.y; const double dz = tmx.z - tmn.z; if (__max(dx, __max(dy, dz)) > dMaxSpan) break; ptMin = tmn; ptMax = tmx; bBox = true; } order.push_back(v); entry.push_back(je); used[v] = 1; stack.push_back(v); } int nKeep = (int)order.size(); // 是否"因超限被截断"(最后一个节点还有没用到的邻居) bool bTruncated = false; { const int last = order.back(); for (size_t t = 0; t < adj[last].size(); ++t) { if (!used[adj[last][t].first]) { bTruncated = true; break; } } } // 回退到最近的优先切点(法兰面/现场焊缝),让切点落在该落的地方 if (bTruncated) { for (int k = nKeep - 1; k >= 1; --k) { if (jointPriority(joints[entry[k]]) >= 1) { // order[k..] 交还给下一轮 for (int t = k; t < nKeep; ++t) used[order[t]] = 0; nKeep = k; break; } } } sheets.push_back(std::vector(order.begin(), order.begin() + nKeep)); } } // ---- 5.5 分图导出 ---- // 文件名非法字符替换 CString sanitizeFileName(LPCTSTR psz) { CString s(psz ? psz : _T("")); LPCTSTR pszBad = _T("\\/:*?\"<>|"); for (int i = 0; i < (int)_tcslen(pszBad); ++i) s.Replace(pszBad[i], _T('_')); s.Trim(); if (s.IsEmpty()) s = _T("PCF"); return s; } // 一个焊口:位置 + 管径 + 两侧组件(用于把焊口挂到组件所在的那张图上) struct WeldRec { AcGePoint3d pt; double bore; AcDbObjectId idA, idB; WeldRec() : bore(0.0) {} }; /* 焊口(WELD)。PCF 参考手册里 WELD 的标准写法: WELD END-POINT x y z END-POINT x y z SKEY WW CATEGORY FABRICATION (两个 END-POINT 分别代表焊缝两侧;本模型里两侧位置重合,故坐标相同。) */ bool pcf_weld(const WeldRec &w, std::ostream &out) { if (!out.good()) return false; PcfRow(out, 0) << PCF_COMP_WELD; if (w.bore > 0.0) { PcfRow(out, 4) << PCF_PT_END_POINT << w.pt << w.bore; PcfRow(out, 4) << PCF_PT_END_POINT << w.pt << w.bore; } else { PcfRow(out, 4) << PCF_PT_CO_ORDS << w.pt; } PcfRow(out, 4) << PCF_ATTR_SKEY << PCF_ATTR_SKEY_WELD; PcfRow(out, 4) << PCF_KW_CATEGORY << PCF_CAT_FABRICATION; out << "\n"; return true; } // 设备(EQUIPMENT)定义块:必须写在 PIPELINE-REFERENCE 之前 void writePcfEquipmentDefs(std::ostream &out, CUIISQLiteServer *pDb , const AcDbObjectIdArray &ids, int &nDef, int &nDefOk) { nDef = nDefOk = 0; for (int i(0); i < ids.length(); ++i) { AcDbObjectPointer pObj(ids[i], AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; if (pObj->isA() != AecDbXEquipment::desc()) continue; ++nDef; AecDbXEquipment *pXEqu = AecDbXEquipment::cast(pObj.object()); if (pcf_xEquipment(pDb, pXEqu, out)) ++nDefOk; } } // 管道 / 管件 / 阀门(设备) / 焊口 void writePcfComponents(std::ostream &out, CUIISQLiteServer *pDb , const AcDbObjectIdArray &ids , const std::vector *pSplits , const std::vector > *pMsgs , const std::vector *pWelds , int &nPipe, int &nPipeOk, int &nFit, int &nFitOk, int &nEqu, int &nEquOk , int &nWeld, int &nWeldOk) { nPipe = nPipeOk = nFit = nFitOk = nEqu = nEquOk = 0; nWeld = nWeldOk = 0; // 同一实体被拆成多段时,先按实体归拢切段参数 std::vector hit; for (int i(0); i < ids.length(); ++i) { const AcDbObjectId &id(ids[i]); AcDbObjectPointer pObj(id, AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; bool bOk = false; if (pObj->isA() == AecDbEwPipe::desc()) { AecDbEwPipe *pPipe = AecDbEwPipe::cast(pObj.object()); // 收集本实体对应的切段(通常只有一段) hit.clear(); if (pSplits) { for (size_t k = 0; k < pSplits->size(); ++k) if ((*pSplits)[k].id == id) hit.push_back(k); } if (!hit.empty()) { // 长直管被切断:本图只输出落在本图上的那几段虚拟管 const AcGePoint3d ptA = pPipe->StartPoint(); const AcGeVector3d vt(pPipe->EndPoint() - ptA); nPipe += (int)hit.size(); for (size_t k = 0; k < hit.size(); ++k) { const PipeSplit &sp = (*pSplits)[hit[k]]; const AcGePoint3d ptS = ptA + vt * sp.t0; const AcGePoint3d ptE = ptA + vt * sp.t1; if (pcf_pipe_normal(pDb, pPipe, out, &ptS, &ptE)) ++nPipeOk; } bOk = true; } else { ++nPipe; if (pcf_pipe_normal(pDb, pPipe, out)) { ++nPipeOk; bOk = true; } } } else if (pObj->isA() == AecDbEwFitting::desc()) { ++nFit; AecDbEwFitting *pFit = AecDbEwFitting::cast(pObj.object()); if (pcf_fitting(pDb, pFit, out)) { ++nFitOk; bOk = true; } } else if (pObj->isA() == AecDbEwEquipment::desc()) { ++nEqu; AecDbEwEquipment *pEqu = AecDbEwEquipment::cast(pObj.object()); if (pcf_ewEquipment(pDb, pEqu, out)) { ++nEquOk; bOk = true; } } // 接图标识:紧跟在本组件属性之后写,随该组件一起被下游识别。 // 同一个组件可能是两个切缝的端点(例如三通的两条支管各去了不同的图), // 所以这里要把该组件的所有接图标识都写出来,不能 break。 if (bOk && pMsgs) { for (size_t k = 0; k < pMsgs->size(); ++k) { if ((*pMsgs)[k].first == id) writePcfAttribute(out, 4, PCF_ATTR_CONTINUATION, (LPCTSTR)(*pMsgs)[k].second); } } // 焊口:挂在焊缝 A 端组件之后,保持与相邻组件的前后关系 if (bOk && pWelds) { for (size_t k = 0; k < pWelds->size(); ++k) { if ((*pWelds)[k].idA != id) continue; ++nWeld; if (pcf_weld((*pWelds)[k], out)) ++nWeldOk; } } } } // 支吊架:本身不带 PIPE_SYS,也没有宿主管道句柄,按空间就近归属到本管道系统 void writePcfSupports(std::ostream &out, LPCTSTR pszSys , const std::vector &hangers , const std::vector &targets, int &nSup, int &nSupOk) { nSup = nSupOk = 0; for (size_t k(0); k < hangers.size(); ++k) { AcDbObjectPointer pObj(hangers[k], AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; AecDbPipeHanger *pHanger = AecDbPipeHanger::cast(pObj.object()); if (!pHanger) continue; if (!supportBelongsToSystem(pHanger->position(), pszSys, targets)) continue; ++nSup; if (pcf_support(pHanger, out)) ++nSupOk; } } // 一张图(一个 PCF 文件)的统计 struct PcfSheetCounts { int nPipe, nPipeOk, nFit, nFitOk, nEqu, nEquOk, nEquDef, nEquDefOk, nSup, nSupOk, nWeld, nWeldOk; PcfSheetCounts() : nPipe(0), nPipeOk(0), nFit(0), nFitOk(0), nEqu(0), nEquOk(0) , nEquDef(0), nEquDefOk(0), nSup(0), nSupOk(0), nWeld(0), nWeldOk(0) {} }; /* 写一张图的完整 PCF 内容(文件流的打开/关闭由调用方负责)。 pszPipelineRef : PIPELINE-REFERENCE 取值(管号,取不到时用系统名) pSplits : 长直管切段(可空) pMsgs : 接图标识(可空) */ void writePcfSheet(std::ostream &out, CUIISQLiteServer *pDb, LPCTSTR pszPipelineRef , const AcDbObjectIdArray &ids , const std::vector &targets , const std::vector &hangers , LPCTSTR pszSupportSys , const std::vector *pSplits , const std::vector > *pMsgs , const std::vector *pWelds , PcfSheetCounts &c) { pcf_begin(out); writePcfEquipmentDefs(out, pDb, ids, c.nEquDef, c.nEquDefOk); pcf_pipeline_reference(out, pszPipelineRef); writePcfComponents(out, pDb, ids, pSplits, pMsgs, pWelds , c.nPipe, c.nPipeOk, c.nFit, c.nFitOk, c.nEqu, c.nEquOk, c.nWeld, c.nWeldOk); writePcfSupports(out, pszSupportSys, hangers, targets, c.nSup, c.nSupOk); } // 一次遍历同时统计所有候选键的取值分布(避免按 6 个键反复打开实体) void probeGroupKeys(const AcDbObjectIdArray &ids, std::vector > &hists) { const int nKeys = (int)(sizeof(g_pszNoKeys) / sizeof(g_pszNoKeys[0])); hists.assign(nKeys, std::map()); for (int i = 0; i < ids.length(); ++i) { AcDbObjectPointer pObj(ids[i], AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) continue; for (int k = 0; k < nKeys; ++k) { CString s; if (readComponentKey(pObj.object(), g_pszNoKeys[k], s) && !s.IsEmpty()) hists[k][s]++; } } } // 跨图连接判定用:一个连接点及其所属组件 struct SheetPoint { AcGePoint3d pt; AcDbObjectId id; }; // 取一个实体的节点信息(接口 + 主轴),定义见下方 bool readNode(AcDbObjectId id, NetNode &nd); // 预计算一张图的全部连接点,避免跨图两两比较时反复打开实体 void collectSheetPoints(const AcDbObjectIdArray &ids, std::vector &pts) { pts.clear(); for (int i = 0; i < ids.length(); ++i) { NetNode nd; if (!readNode(ids[i], nd)) continue; std::vector ar; nodeConnPoints(nd, ar); for (size_t k = 0; k < ar.size(); ++k) { SheetPoint sp; sp.pt = ar[k].pt; sp.id = ids[i]; pts.push_back(sp); } } } // 取一个实体的节点信息(接口 + 主轴),供跨图连接判定使用 bool readNode(AcDbObjectId id, NetNode &nd) { AcDbObjectPointer pObj(id, AcDb::kForRead); if (pObj.openStatus() != Acad::eOk) return false; const int nType = getCompType(pObj.object()); if (nType < 0) return false; AcGePoint3d a, b; AcArray infs; if (!readAxisSegment(pObj.object(), infs, a, b)) return false; nd.id = id; nd.nType = nType; nd.a = a; nd.b = b; nd.infs = infs; return true; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 默认输出路径(实现在文件末尾) static CString makeDefaultPcfPath(LPCTSTR pszSystem); // 对外只暴露这一个接口 // 按管道系统(pszSystem)得到该系统下的所有 管道/管件/设备,并输出到 pcf 文件 bool PCF::makePCF(LPCTSTR pszFile, LPCTSTR pszSystem) { if (!pszFile || _tcslen(pszFile) == 0) return false; // 未指定管道系统时,遍历图纸中出现的所有管道系统,取第一个 CString sSystem; if (pszSystem && _tcslen(pszSystem) > 0) { sSystem = pszSystem; } else { std::vector systems; collectSystems(systems); if (systems.empty()) { acutPrintf(_T("\n图纸中未找到任何管道系统(PIPE_PSYSTEM)!")); return false; } sSystem = systems[0]; } AcDbObjectIdArray ids; if (!getComponentBySystem(sSystem, ids)) { acutPrintf(_T("\n管道系统 [%s] 下没有 管道/管件/设备。"), (LPCTSTR)sSystem); return false; } // 规格库(物料码/描述/等级)用于填充 PCF 的 ITEM-CODE 等属性; // 打不开时降级为只输出几何骨架,不阻断导出。 CUIISQLiteServer *pDb = new CUIISQLiteServer; if (!pDb->InitADOServer(FilePath::GetSysDir() + _T("\\Plumbing.db"))) { AfxMessageBox(_T("数据库驱动加载数据库Plumbing.db失败!将只导出几何骨架。")); DELETE_PTR(pDb); pDb = NULL; } std::ofstream out(pszFile, std::ios::binary); if (!out.good()) { DELETE_PTR(pDb); return false; } // 支吊架按空间就近归属,先备好归属用的组件主轴表 std::vector targets; collectSupportTargets(targets); std::vector hangers; collectPipeHangers(hangers); // 焊口:整系统建一次网络,取出所有焊接接头(两侧接口连接类型都是"焊接") std::vector welds; { std::vector nodes; std::vector joints; std::vector splits; buildNetwork(ids, 0.0, nodes, joints, splits); for (size_t j = 0; j < joints.size(); ++j) { if (!joints[j].bWeld) continue; WeldRec w; w.pt = joints[j].pt; w.bore = joints[j].dBore; w.idA = nodes[joints[j].nA].id; w.idB = nodes[joints[j].nB].id; welds.push_back(w); } } // 头部(单位) + 设备定义 + 管线引用 + 组件 + 焊口 + 支吊架 PcfSheetCounts c; writePcfSheet(out, pDb, (LPCTSTR)sSystem, ids, targets, hangers, (LPCTSTR)sSystem , NULL, NULL, &welds, c); out.close(); DELETE_PTR(pDb); acutPrintf(_T("\n管道系统 [%s]: 直管 %d/%d, 管件 %d/%d, 阀门/附件 %d/%d, 设备定义 %d/%d, 焊口 %d/%d, 支吊架 %d/%d。") , (LPCTSTR)sSystem, c.nPipeOk, c.nPipe, c.nFitOk, c.nFit, c.nEquOk, c.nEqu , c.nEquDefOk, c.nEquDef, c.nWeldOk, c.nWeld, c.nSupOk, c.nSup); return true; } /* 自动分图:把一个管道系统拆成多张 ISO 图,每张图导出一个 PCF 文件。 见 pcf.h 里的规则说明。 */ bool PCF::makePCFSplit(LPCTSTR pszDir, LPCTSTR pszSystem, int nMaxNodes, double dMaxSpan) { // 1. 选系统 CString sSystem; if (pszSystem && _tcslen(pszSystem) > 0) { sSystem = pszSystem; } else { std::vector systems; collectSystems(systems); if (systems.empty()) { acutPrintf(_T("\n图纸中未找到任何管道系统(PIPE_PSYSTEM)!")); return false; } sSystem = systems[0]; } // 2. 取该系统的全部组件 AcDbObjectIdArray ids; if (!getComponentBySystem(sSystem, ids)) { acutPrintf(_T("\n管道系统 [%s] 下没有 管道/管件/设备。"), (LPCTSTR)sSystem); return false; } // 3. 探测可用的"管号"键 acutPrintf(_T("\n---- 自动分图: 管道系统 [%s],共 %d 个组件 ----") , (LPCTSTR)sSystem, ids.length()); acutPrintf(_T("\n候选管号键的填充情况:")); std::vector > hists; probeGroupKeys(ids, hists); LPCTSTR pszUseKey = NULL; const int nKeys = (int)(sizeof(g_pszNoKeys) / sizeof(g_pszNoKeys[0])); for (int k = 0; k < nKeys; ++k) { int nFilled = 0; for (std::map::const_iterator it = hists[k].begin(); it != hists[k].end(); ++it) nFilled += it->second; acutPrintf(_T("\n %-10s 有值 %d/%d,取值 %d 种") , g_pszNoKeys[k], nFilled, ids.length(), (int)hists[k].size()); // 可用的分组键:多数组件有值,且取值数明显少于组件数(否则等于按每根管分组) if (!pszUseKey && nFilled * 2 >= ids.length() && !hists[k].empty() && (int)hists[k].size() * 2 <= ids.length()) pszUseKey = g_pszNoKeys[k]; } if (pszUseKey) acutPrintf(_T("\n采用分组键: [%s]。"), pszUseKey); else acutPrintf(_T("\n未找到可用的管号键(取值过于分散或全为空),改按连通子网分图。")); // 4. 先按接口连通性把整个系统拆成连通子网,再按子网内的"多数管号"归并成管线组。 // 顺序很重要:阀门/附件这类不带管号的组件必须先靠连通性归到它所在的管线上, // 否则会被单独分出去(实测 97 个阀门会各自成一张图)。 std::vector nets; splitNetByConnectivity(ids, nets); std::map groups; std::vector groupOrder; for (size_t k = 0; k < nets.size(); ++k) { const CString sKey = dominantKey(nets[k], pszUseKey, (LPCTSTR)sSystem); if (groups.find(sKey) == groups.end()) groupOrder.push_back(sKey); for (int i = 0; i < nets[k].length(); ++i) groups[sKey].append(nets[k][i]); } acutPrintf(_T("\n按管号归并为 %d 个管线组。"), (int)groupOrder.size()); // 5. 组内按图幅二次切分(同一管线组可能由多条互不相连的子网组成,各自成图) struct SheetPlan { CString ref; // PIPELINE-REFERENCE(管号) AcDbObjectIdArray ids; std::vector splits; std::vector welds; }; std::vector plans; for (size_t gi = 0; gi < groupOrder.size(); ++gi) { const CString &sKey = groupOrder[gi]; const AcDbObjectIdArray &groupIds = groups[sKey]; std::vector nodes; std::vector joints; std::vector splits; // 长直管按 dMaxSpan 预切段,使"长直管中心点"也能成为切点 buildNetwork(groupIds, dMaxSpan, nodes, joints, splits); // 单张图的限制按组件个数换算到节点个数 const int nLimit = (nMaxNodes > 0) ? nMaxNodes : 0; std::vector > sheets; splitBySize(nodes, joints, nLimit, dMaxSpan, sheets); // 节点 -> 图号 std::vector nodeSheet(nodes.size(), -1); for (size_t s = 0; s < sheets.size(); ++s) { for (size_t t = 0; t < sheets[s].size(); ++t) nodeSheet[sheets[s][t]] = (int)s; } // 焊口按 A 端所在图归属(两端分属不同图时归 A 端那张,保证全系统只出现一次) std::vector > sheetWelds(sheets.size()); for (size_t j = 0; j < joints.size(); ++j) { if (!joints[j].bWeld) continue; int s = -1; if (joints[j].nA >= 0 && joints[j].nA < (int)nodeSheet.size()) s = nodeSheet[joints[j].nA]; if (s < 0 && joints[j].nB >= 0 && joints[j].nB < (int)nodeSheet.size()) s = nodeSheet[joints[j].nB]; if (s < 0) continue; WeldRec w; w.pt = joints[j].pt; w.bore = joints[j].dBore; w.idA = nodes[joints[j].nA].id; w.idB = nodes[joints[j].nB].id; sheetWelds[s].push_back(w); } for (size_t s = 0; s < sheets.size(); ++s) { SheetPlan plan; plan.ref = sKey; plan.welds = sheetWelds[s]; // 节点 -> 组件(去重,保持顺序) for (size_t t = 0; t < sheets[s].size(); ++t) { const NetNode &nd = nodes[sheets[s][t]]; if (plan.ids.find(nd.id) == -1) plan.ids.append(nd.id); } // 只保留本图用到的切段(按节点精确对应,避免把别张图的段带进来) for (size_t t = 0; t < sheets[s].size(); ++t) { const int q = nodes[sheets[s][t]].nSplitIdx; if (q < 0 || q >= (int)splits.size()) continue; if (splits[q].t0 <= 0.0 && splits[q].t1 >= 1.0) continue; // 未切 plan.splits.push_back(splits[q]); } plans.push_back(plan); } } if (plans.empty()) { acutPrintf(_T("\n没有可导出的分图。")); return false; } // 6. 生成图号并建立"接图纸"映射: // 每个切点两侧各写一条"接图纸 <对方图号>",方便施工拼图。 std::vector sheetCodes; for (size_t s = 0; s < plans.size(); ++s) { CString sCode; sCode.Format(_T("ISO-%03d"), (int)(s + 1)); sheetCodes.push_back(sCode); } // 图号 -> 该图包含的组件 std::vector > sheetIds(plans.size()); for (size_t s = 0; s < plans.size(); ++s) { for (int i = 0; i < plans[s].ids.length(); ++i) sheetIds[s].push_back(plans[s].ids[i]); } // 逐对检查两张图之间是否存在"跨图连接":A 图某组件的接口位置与 B 图某组件的 // 接口位置重合即认为这两张图原本是接在一起的,两侧互相写"接图纸"标识。 // 先把各图的连接点算好,之后只是纯坐标比较,不再反复打开实体。 std::vector > sheetPts(plans.size()); for (size_t s = 0; s < plans.size(); ++s) collectSheetPoints(plans[s].ids, sheetPts[s]); std::vector > msgs; { const double dTol = netTolerance(); for (size_t a = 0; a < plans.size(); ++a) { for (size_t b = 0; b < plans.size(); ++b) { if (a == b) continue; CString sMsg; sMsg.Format(_T("接图纸 %s"), (LPCTSTR)sheetCodes[b]); // 一对图之间可能有好几处切缝,逐处都标,不能只标第一处 std::vector hits; for (size_t x = 0; x < sheetPts[a].size(); ++x) { for (size_t y = 0; y < sheetPts[b].size(); ++y) { if (sheetPts[a][x].pt.distanceTo(sheetPts[b][y].pt) > dTol) continue; const AcDbObjectId &idHit = sheetPts[a][x].id; bool bDup = false; for (size_t k = 0; k < hits.size(); ++k) { if (hits[k] == idHit) { bDup = true; break; } } if (!bDup) hits.push_back(idHit); break; // 本连接点已配上,换下一个 } } for (size_t k = 0; k < hits.size(); ++k) msgs.push_back(std::make_pair(hits[k], sMsg)); } } } // 7. 输出目录 CString sDir = (pszDir && _tcslen(pszDir) > 0) ? pszDir : _T(""); if (sDir.IsEmpty()) { CString sPath = makeDefaultPcfPath((LPCTSTR)sSystem); const int nPos = sPath.ReverseFind(_T('\\')); sDir = (nPos >= 0) ? sPath.Left(nPos) : _T(""); } if (!sDir.IsEmpty() && sDir[sDir.GetLength() - 1] != _T('\\')) sDir += _T("\\"); // 8. 支吊架(全图收集一次,逐图按空间就近归属) std::vector targets; collectSupportTargets(targets); std::vector hangers; collectPipeHangers(hangers); // 9. 逐图写文件 CUIISQLiteServer *pDb = new CUIISQLiteServer; if (!pDb->InitADOServer(FilePath::GetSysDir() + _T("\\Plumbing.db"))) { AfxMessageBox(_T("数据库驱动加载数据库Plumbing.db失败!将只导出几何骨架。")); DELETE_PTR(pDb); pDb = NULL; } int nOk = 0; for (size_t s = 0; s < plans.size(); ++s) { CString sFile; sFile.Format(_T("%s%s_%s.pcf") , (LPCTSTR)sDir , (LPCTSTR)sheetCodes[s] , (LPCTSTR)sanitizeFileName(plans[s].ref)); std::ofstream out((LPCTSTR)sFile, std::ios::binary); if (!out.good()) { acutPrintf(_T("\n分图 %s 无法写入: %s"), (LPCTSTR)sheetCodes[s], (LPCTSTR)sFile); continue; } // 本图相关的接图标识 std::vector > sheetMsgs; for (size_t m = 0; m < msgs.size(); ++m) { for (size_t i = 0; i < sheetIds[s].size(); ++i) { if (msgs[m].first == sheetIds[s][i]) { sheetMsgs.push_back(msgs[m]); break; } } } PcfSheetCounts c; writePcfSheet(out, pDb, (LPCTSTR)plans[s].ref, plans[s].ids, targets, hangers , (LPCTSTR)sSystem, &plans[s].splits, &sheetMsgs, &plans[s].welds, c); out.close(); ++nOk; acutPrintf(_T("\n %s -> %s (直管 %d, 管件 %d, 阀门/附件 %d, 设备定义 %d, 焊口 %d, 支吊架 %d)") , (LPCTSTR)sheetCodes[s], (LPCTSTR)sFile , c.nPipeOk, c.nFitOk, c.nEquOk, c.nEquDefOk, c.nWeldOk, c.nSupOk); } DELETE_PTR(pDb); acutPrintf(_T("\n自动分图完成: 系统 [%s] 共输出 %d 张图,目录 %s") , (LPCTSTR)sSystem, nOk, (LPCTSTR)sDir); return nOk > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 默认输出路径:图纸所在目录\<系统名>.pcf;图纸未保存时退回系统目录 static CString makeDefaultPcfPath(LPCTSTR pszSystem) { CString sSys = (pszSystem && _tcslen(pszSystem) > 0) ? pszSystem : _T("PCF"); CString sPath; const TCHAR *pszDwg = NULL; acdbCurDwg()->getFilename(pszDwg); if (pszDwg && _tcslen(pszDwg) > 0) { sPath = pszDwg; int nPos = sPath.ReverseFind(_T('\\')); if (nPos < 0) nPos = sPath.ReverseFind(_T('/')); sPath = (nPos >= 0) ? sPath.Left(nPos + 1) : _T(""); } if (sPath.IsEmpty()) sPath = FilePath::GetSysDir() + _T("\\"); sPath += sSys; sPath += _T(".pcf"); return sPath; } // 命令:按管道系统导出 PCF 中间文件 // 交互用法:EW_PCF -> 列出系统 -> 输入系统(可空=自动取第一个) -> 输入输出路径(可空=采用默认路径) // 非交互用法:EW_PCF + 两次回车,即自动选取第一个系统并导出到默认路径,不会挂起等待输入。 void EW_PCFCmdFnc() { // 1. 列出图纸中现有的管道系统,方便选择 std::vector systems; collectSystems(systems); if (!systems.empty()) { acutPrintf(_T("\n图纸中的管道系统共 %d 个:"), (int)systems.size()); for (size_t k = 0; k < systems.size(); ++k) acutPrintf(_T("\n [%d] %s"), (int)(k + 1), (LPCTSTR)systems[k]); } else { acutPrintf(_T("\n未找到管道系统(PIPE_PSYSTEM),将无法导出。")); } // 2. 输入系统(可空=自动取第一个) TCHAR szSys[256] = { 0 }; if (acedGetString(FALSE, _T("\n请输入管道系统<直接回车=自动选取第一个>: "), szSys) != RTNORM) return; CString sSystem = szSys; // 留空时直接取第一个系统,便于默认文件名带上系统名 if (sSystem.IsEmpty() && !systems.empty()) sSystem = systems[0]; // 3. 输出路径:先算出默认值并显示,直接回车即采用默认,避免非交互调用时挂起等待 CString sDef = makeDefaultPcfPath(sSystem.IsEmpty() ? NULL : (LPCTSTR)sSystem); CString sPrompt; sPrompt.Format(_T("\n请输入输出的PCF文件完整路径<直接回车采用默认: %s>: "), (LPCTSTR)sDef); TCHAR szFile[512] = { 0 }; if (acedGetString(FALSE, sPrompt, szFile) != RTNORM) return; CString sFile = szFile; if (sFile.IsEmpty()) sFile = sDef; // 4. 导出 LPCTSTR pszSys = sSystem.IsEmpty() ? NULL : (LPCTSTR)sSystem; if (PCF::makePCF(sFile, pszSys)) acutPrintf(_T("\nPCF导出成功: %s"), (LPCTSTR)sFile); else acutPrintf(_T("\nPCF导出失败!请检查管道系统下是否存在 管道/管件/设备。")); } // 命令:自动分图 —— 把一个管道系统自动拆成多张 ISO 图(每张图一个 PCF 文件) // 拆分规则见 PCF::makePCFSplit()。全部提示直接回车即采用默认值,便于非交互调用。 void EW_PCFSPLITCmdFnc() { // 1. 列出图纸中现有的管道系统 std::vector systems; collectSystems(systems); if (!systems.empty()) { acutPrintf(_T("\n图纸中的管道系统共 %d 个:"), (int)systems.size()); for (size_t k = 0; k < systems.size(); ++k) acutPrintf(_T("\n [%d] %s"), (int)(k + 1), (LPCTSTR)systems[k]); } else { acutPrintf(_T("\n未找到管道系统(PIPE_PSYSTEM),将无法分图。")); } // 2. 管道系统 TCHAR szSys[256] = { 0 }; if (acedGetString(FALSE, _T("\n请输入要分图的管道系统<直接回车=自动选取第一个>: "), szSys) != RTNORM) return; CString sSystem = szSys; if (sSystem.IsEmpty() && !systems.empty()) sSystem = systems[0]; // 3. 单张图最大组件数(超过则继续切分) TCHAR szNum[64] = { 0 }; if (acedGetString(FALSE, _T("\n请输入单张图最大组件数<直接回车=60,0=不限制>: "), szNum) != RTNORM) return; int nMaxNodes = 60; if (_tcslen(szNum) > 0) nMaxNodes = _ttoi(szNum); // 4. 单张图最大空间跨度(超过则在长直管中点继续切分) TCHAR szSpan[64] = { 0 }; if (acedGetString(FALSE, _T("\n请输入单张图最大空间跨度<直接回车=0,表示不限制>: "), szSpan) != RTNORM) return; double dMaxSpan = 0.0; if (_tcslen(szSpan) > 0) dMaxSpan = _ttof(szSpan); // 5. 输出目录:默认取图纸所在目录 CString sDefDir; { CString sDef = makeDefaultPcfPath(sSystem.IsEmpty() ? NULL : (LPCTSTR)sSystem); const int nPos = sDef.ReverseFind(_T('\\')); sDefDir = (nPos >= 0) ? sDef.Left(nPos) : _T(""); } CString sPrompt; sPrompt.Format(_T("\n请输入分图输出目录<直接回车=图纸所在目录: %s>: "), (LPCTSTR)sDefDir); TCHAR szDir[512] = { 0 }; if (acedGetString(FALSE, sPrompt, szDir) != RTNORM) return; CString sDir = szDir; if (sDir.IsEmpty()) sDir = sDefDir; // 6. 分图导出 LPCTSTR pszSys = sSystem.IsEmpty() ? NULL : (LPCTSTR)sSystem; if (PCF::makePCFSplit(sDir, pszSys, nMaxNodes, dMaxSpan)) acutPrintf(_T("\n自动分图完成。")); else acutPrintf(_T("\n自动分图失败!请检查管道系统下是否存在 管道/管件/设备。")); }