640 lines
28 KiB
JavaScript
640 lines
28 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import process from 'node:process';
|
||
|
||
const API_BASE = process.env.CADLABEL_API_BASE ?? 'http://127.0.0.1:8891/api';
|
||
const DEFAULT_SOURCE_ROOT = fs.existsSync(path.resolve(process.cwd(), '../json_data/original'))
|
||
? '../json_data/original'
|
||
: '../output_json 2';
|
||
const SOURCE_ROOT = path.resolve(process.cwd(), process.env.CADLABEL_SOURCE_ROOT ?? DEFAULT_SOURCE_ROOT);
|
||
const APPLY = process.argv.includes('--apply');
|
||
const FORCE = process.argv.includes('--force');
|
||
const onlyModel = valueAfter('--model');
|
||
const MARKER = '[AI批量分析:v1]';
|
||
const API_TIMEOUT_MS = Number(process.env.CADLABEL_API_TIMEOUT_MS ?? 120_000);
|
||
|
||
const CLASS_DEFINITIONS = [
|
||
{
|
||
ids: ['061735', '144358', '240860'],
|
||
label: '圆形平板法兰盘',
|
||
scene: '通用圆形安装连接面,适用于螺栓固定、端面贴合和中心定位',
|
||
primaryFunction: '圆形法兰安装连接'
|
||
},
|
||
{
|
||
ids: ['020543', '080683', '202578', '029447'],
|
||
label: '带凸台轴颈法兰',
|
||
scene: '轴、套筒或设备端面的定位连接,利用凸台或轴颈建立同轴约束',
|
||
primaryFunction: '法兰连接与同轴导向'
|
||
},
|
||
{
|
||
ids: ['080632', '086232', '191643'],
|
||
label: '多孔工业圆法兰',
|
||
scene: '多螺栓圆周连接、载荷分散和精密安装定位',
|
||
primaryFunction: '多螺栓法兰连接'
|
||
},
|
||
{
|
||
ids: ['001563', '002935'],
|
||
label: '筒体接头转接法兰',
|
||
scene: '筒体、轴套或管状接口之间的同轴转接与支承',
|
||
primaryFunction: '筒体接口转接与支承'
|
||
},
|
||
{
|
||
ids: ['001942', '218172'],
|
||
label: '薄板环形/异形盲板',
|
||
scene: '薄板封盖、垫片式隔离或异形基板固定',
|
||
primaryFunction: '封盖隔离与异形安装'
|
||
},
|
||
{
|
||
ids: ['006947', '016033', '108524'],
|
||
label: '矩形/方形安装法兰板',
|
||
scene: '设备矩形安装面、轴孔定位和底板连接',
|
||
primaryFunction: '设备安装与定位'
|
||
},
|
||
{
|
||
ids: ['026613', '177126', '214404', '221476'],
|
||
label: '支架底座/耳板法兰',
|
||
scene: '支架、底座、耳板或设备主体的承载安装连接',
|
||
primaryFunction: '支架承载与底座固定'
|
||
},
|
||
{
|
||
ids: ['052729', '054798', '122225'],
|
||
label: '轮毂衬套/厚环',
|
||
scene: '轴孔导向、间隔定位、轮毂或厚壁环形连接',
|
||
primaryFunction: '轴孔导向与间隔定位'
|
||
},
|
||
{
|
||
ids: ['015133'],
|
||
label: '复杂多工位安装座',
|
||
scene: '多凸台、多孔位机械组件的承载、定位与多方向装配',
|
||
primaryFunction: '多工位承载与装配连接'
|
||
},
|
||
{
|
||
ids: ['083235'],
|
||
label: '多层凸台圆形安装座',
|
||
scene: '圆形基座、轴颈和多方向接口的同轴支承与紧固',
|
||
primaryFunction: '圆形基座支承与多接口连接'
|
||
}
|
||
];
|
||
|
||
const GROUP_DEFINITIONS = {
|
||
base: {
|
||
name: '主体承载基体',
|
||
type: '主体结构',
|
||
purpose: '建立零件的基本实体、总体包络和主要载荷传递路径。',
|
||
reason: '首个加料特征决定零件的基础形态,也是后续孔、槽、凸台和边缘特征的宿主。'
|
||
},
|
||
boss: {
|
||
name: '凸台、轴颈与加强结构',
|
||
type: '附加实体结构',
|
||
purpose: '形成局部凸台、轴颈、台阶或加强区域,提供定位、支承或局部连接界面。',
|
||
reason: '分层加料能够在控制总体重量的同时,把材料布置到定位和受力需要的位置。'
|
||
},
|
||
cut: {
|
||
name: '孔槽、内腔与避让结构',
|
||
type: '切除结构',
|
||
purpose: '形成通孔、内腔、槽口、轮廓减重或装配避让空间。',
|
||
reason: '切除特征用于建立配合空间、功能通道和局部间隙,并避免与配合件干涉。'
|
||
},
|
||
hole: {
|
||
name: '安装与定位孔组',
|
||
type: '孔系结构',
|
||
purpose: '提供螺栓、销或轴类零件的安装、定位和穿过接口。',
|
||
reason: '标准孔特征便于采用通用紧固件和加工刀具,并提高装配互换性。'
|
||
},
|
||
thread: {
|
||
name: '螺纹连接孔组',
|
||
type: '螺纹接口结构',
|
||
purpose: '提供内螺纹紧固接口,实现可拆卸连接和轴向夹紧。',
|
||
reason: '孔向导定义的标准螺纹便于紧固件选型,并使连接规格可追溯。'
|
||
},
|
||
counterbore: {
|
||
name: '沉头紧固孔组',
|
||
type: '沉头孔结构',
|
||
purpose: '容纳螺钉杆部及头部,使紧固件可靠定位并控制头部突出量。',
|
||
reason: '沉头或柱形沉头可提供稳定支承面,减少螺钉头与相邻零件的干涉。'
|
||
},
|
||
pattern: {
|
||
name: '阵列重复结构',
|
||
type: '规则阵列结构',
|
||
purpose: '按规则间距复制孔、槽或凸台,使重复结构保持一致。',
|
||
reason: '参数化阵列保证重复特征的间距、数量和方向一致,便于均匀载荷或多工位装配。'
|
||
},
|
||
edge: {
|
||
name: '边缘过渡与装配处理',
|
||
type: '边缘处理结构',
|
||
purpose: '通过倒角或圆角改善装配导入、去除锐边并降低局部应力集中。',
|
||
reason: '受控边缘过渡可提升装配性、加工安全性和边缘耐久性。'
|
||
},
|
||
auxiliary: {
|
||
name: '辅助几何结构',
|
||
type: '辅助结构',
|
||
purpose: '承载不能归入主体、孔槽或边缘处理的其他有效建模结果。',
|
||
reason: '保留辅助步骤与结构的对应关系,避免建模历史在语义层丢失。'
|
||
}
|
||
};
|
||
|
||
function valueAfter(flag) {
|
||
const index = process.argv.indexOf(flag);
|
||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||
}
|
||
|
||
async function api(relativePath, init) {
|
||
const response = await fetch(`${API_BASE}${relativePath}`, {
|
||
...init,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(init?.headers ?? {})
|
||
},
|
||
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok || payload.code !== 'OK') {
|
||
throw new Error(`${init?.method ?? 'GET'} ${relativePath}: ${payload.message ?? response.status}`);
|
||
}
|
||
return payload.data;
|
||
}
|
||
|
||
function post(relativePath, body) {
|
||
return api(relativePath, { method: 'POST', body: JSON.stringify(body) });
|
||
}
|
||
|
||
function classFor(modelName, geometry) {
|
||
const configured = CLASS_DEFINITIONS.find((item) => item.ids.includes(modelName));
|
||
if (configured) return configured;
|
||
if (modelName.includes('方板+均布圆周孔')) {
|
||
return {
|
||
label: '圆周孔方形安装板',
|
||
scene: '方形安装基面上的圆周均布紧固、定位和载荷分散',
|
||
primaryFunction: '方板承载与圆周孔系连接'
|
||
};
|
||
}
|
||
if (modelName.includes('方板+四角孔')) {
|
||
return {
|
||
label: '多孔方形安装板',
|
||
scene: '方形安装基面上的四角紧固、中心定位和附加接口连接',
|
||
primaryFunction: '方板承载与多点安装'
|
||
};
|
||
}
|
||
if (modelName.includes('方台')) {
|
||
return {
|
||
label: geometry.interfaceCount ? '多孔方形安装台' : '方形承载台',
|
||
scene: '小型机械组件的平面承载、垫高、定位和紧固安装',
|
||
primaryFunction: '方台承载与安装定位'
|
||
};
|
||
}
|
||
if (modelName.includes('圆盘+均布孔')) {
|
||
return {
|
||
label: '圆周孔圆形安装盘',
|
||
scene: '圆形安装面上的周向均布紧固和同轴定位',
|
||
primaryFunction: '圆盘承载与圆周孔系连接'
|
||
};
|
||
}
|
||
if (modelName.includes('圆盘')) {
|
||
return {
|
||
label: '圆形承载盘',
|
||
scene: '圆形端面的承载、间隔和同轴安装',
|
||
primaryFunction: '圆盘承载与端面定位'
|
||
};
|
||
}
|
||
const sequenceNumber = /^\d+$/.test(modelName) ? Number(modelName) : null;
|
||
if (sequenceNumber !== null && sequenceNumber >= 40 && sequenceNumber <= 51) {
|
||
return {
|
||
label: '多孔圆形法兰盘',
|
||
scene: '中心接口与周向孔系组合的法兰连接和同轴定位',
|
||
primaryFunction: '圆形法兰连接与孔系定位'
|
||
};
|
||
}
|
||
if (sequenceNumber !== null && sequenceNumber >= 52 && sequenceNumber <= 58) {
|
||
return {
|
||
label: '双耳异形连接板',
|
||
scene: '双圆头或长圆形接口之间的跨距连接、铰接和定位',
|
||
primaryFunction: '双接口跨距连接与载荷传递'
|
||
};
|
||
}
|
||
if (sequenceNumber !== null && sequenceNumber >= 59 && sequenceNumber <= 66) {
|
||
return {
|
||
label: '多孔异形安装板',
|
||
scene: '多规格孔位的组合安装、定位和多点紧固',
|
||
primaryFunction: '异形板承载与多孔连接'
|
||
};
|
||
}
|
||
if (sequenceNumber !== null && sequenceNumber >= 67) {
|
||
return {
|
||
label: '复合孔系圆形法兰盘',
|
||
scene: '多圈、多规格孔系的法兰连接、精密定位和装配适配',
|
||
primaryFunction: '复合孔系法兰连接'
|
||
};
|
||
}
|
||
return geometry.outerCircular ? {
|
||
label: '多孔圆形安装盘',
|
||
scene: '圆形安装面上的承载、周向紧固和同轴定位',
|
||
primaryFunction: '圆盘承载与多孔连接'
|
||
} : {
|
||
label: '机械安装连接件',
|
||
scene: '机械组件的承载、定位和紧固连接',
|
||
primaryFunction: '机械承载与装配连接'
|
||
};
|
||
}
|
||
|
||
function sourceFor(modelName) {
|
||
const filename = path.join(SOURCE_ROOT, `${modelName}.solidworks_rebuild_extract.json`);
|
||
if (!fs.existsSync(filename)) {
|
||
throw new Error(`缺少源 JSON: ${filename}`);
|
||
}
|
||
return JSON.parse(fs.readFileSync(filename, 'utf8'));
|
||
}
|
||
|
||
function boundingBox(source) {
|
||
const box = source.validation_hints?.part_box_m;
|
||
if (!Array.isArray(box) || box.length !== 6) return null;
|
||
const dimensions = [box[3] - box[0], box[4] - box[1], box[5] - box[2]].map((value) => value * 1000);
|
||
return dimensions.map(formatNumber);
|
||
}
|
||
|
||
function geometryAnalysis(source) {
|
||
const box = source.validation_hints?.part_box_m;
|
||
const size = Array.isArray(box) && box.length === 6
|
||
? [box[3] - box[0], box[4] - box[1], box[5] - box[2]].map((value) => value * 1000)
|
||
: [];
|
||
const maxSpan = size.length ? Math.max(...size) : 0;
|
||
const faces = (source.features ?? []).flatMap((feature) => feature.owned_faces ?? []);
|
||
const cylinders = faces
|
||
.filter((face) => face.surface?.is_cylinder && Array.isArray(face.surface.cylinder_params))
|
||
.map((face) => {
|
||
const params = face.surface.cylinder_params;
|
||
const center = params.slice(0, 3).map((value) => value * 1000);
|
||
const axis = params.slice(3, 6);
|
||
const axisIndex = axis.reduce(
|
||
(best, value, index) => Math.abs(value) > Math.abs(axis[best] ?? 0) ? index : best,
|
||
0
|
||
);
|
||
const radialDistance = Math.hypot(...center.filter((_, index) => index !== axisIndex));
|
||
return { radius: Math.abs(params[6] * 1000), center, radialDistance };
|
||
});
|
||
const outerCircular = cylinders.some((item) =>
|
||
maxSpan > 0 && item.radius >= maxSpan * 0.4 && item.radialDistance <= maxSpan * 0.08
|
||
);
|
||
const interfaces = cylinders.filter((item) =>
|
||
maxSpan > 0 && item.radius <= maxSpan * 0.2
|
||
);
|
||
const uniqueInterfaces = new Map();
|
||
for (const item of interfaces) {
|
||
const key = `${formatNumber(item.radius)}:${item.center.map((value) => formatNumber(value)).join(',')}`;
|
||
uniqueInterfaces.set(key, item);
|
||
}
|
||
const diameterGroups = new Map();
|
||
for (const item of uniqueInterfaces.values()) {
|
||
const diameter = formatNumber(item.radius * 2);
|
||
diameterGroups.set(diameter, (diameterGroups.get(diameter) ?? 0) + 1);
|
||
}
|
||
const interfaceSummary = [...diameterGroups.entries()]
|
||
.sort((left, right) => Number(right[0]) - Number(left[0]))
|
||
.map(([diameter, count]) => `${count}ר${diameter} mm`)
|
||
.join('、');
|
||
return {
|
||
size,
|
||
outerCircular,
|
||
interfaceCount: uniqueInterfaces.size,
|
||
interfaceSummary
|
||
};
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
if (!Number.isFinite(value)) return String(value);
|
||
return Number(value.toFixed(Math.abs(value) < 10 ? 3 : 2)).toString();
|
||
}
|
||
|
||
function cleanDimensionName(name, modelName) {
|
||
return String(name ?? '')
|
||
.replace(new RegExp(`@${modelName}\\.Part$`, 'i'), '')
|
||
.replace(/@[^@]+$/, '');
|
||
}
|
||
|
||
function dimensionEvidence(feature, step, modelName) {
|
||
const dimensions = feature?.dimensions ?? [];
|
||
const evidence = [];
|
||
for (const item of dimensions) {
|
||
const value = Number(item.value);
|
||
if (!Number.isFinite(value)) continue;
|
||
const name = cleanDimensionName(item.name, modelName);
|
||
const angleDimension = /角度|angle/i.test(item.name)
|
||
|| (step.type.includes('revolve') && item.name?.startsWith(`D1@${step.name}`))
|
||
|| (step.type === 'chamfer' && item.name?.startsWith(`D2@${step.name}`));
|
||
if (angleDimension && Math.abs(value) > 20) {
|
||
evidence.push(`${name}=${formatNumber(value / 1000 * 180 / Math.PI)}°`);
|
||
continue;
|
||
}
|
||
if (step.type.includes('pattern') && Math.abs(value) >= 1000 && Math.abs(value) <= 20_000) {
|
||
evidence.push(`${name}=${formatNumber(value / 1000)}(源阵列计数编码)`);
|
||
continue;
|
||
}
|
||
if (Math.abs(value) > 1000) continue;
|
||
evidence.push(`${name}=${formatNumber(value)} mm`);
|
||
}
|
||
return [...new Set(evidence)].slice(0, 10);
|
||
}
|
||
|
||
function stepGroup(step) {
|
||
const type = String(step.type ?? step.operationType ?? '').toLowerCase();
|
||
const name = String(step.name ?? step.historyName ?? '').toLowerCase();
|
||
if (type === 'chamfer' || type === 'fillet') return 'edge';
|
||
if (type.includes('pattern')) return 'pattern';
|
||
if (type === 'hole') {
|
||
if (/螺纹|thread|tapped/.test(name)) return 'thread';
|
||
if (/沉头|凹头|counter|螺钉/.test(name)) return 'counterbore';
|
||
return 'hole';
|
||
}
|
||
if (type.includes('cut')) return 'cut';
|
||
if (type.includes('add')) return (step.order ?? step.historyIndex) === 1 ? 'base' : 'boss';
|
||
return 'auxiliary';
|
||
}
|
||
|
||
function operationIntent(step) {
|
||
const type = step.type.toLowerCase();
|
||
if (type === 'extrude_add') return `通过“${step.name}”执行加料拉伸,形成或扩展实体轮廓。`;
|
||
if (type === 'revolve_add') return `通过“${step.name}”将截面绕中心轴旋转成形,建立回转主体、轴颈或环形台阶。`;
|
||
if (type === 'extrude_cut') return `通过“${step.name}”执行拉伸切除,形成孔、槽、内腔或装配避让区域。`;
|
||
if (type === 'revolve_cut') return `通过“${step.name}”执行旋转切除,形成同轴内孔、环槽、锥面或回转台阶。`;
|
||
if (type === 'hole') return `通过孔向导特征“${step.name}”建立标准化孔接口,服务于紧固、定位或穿轴装配。`;
|
||
if (type === 'linear_pattern') return `通过“${step.name}”按设定方向、间距和数量复制种子特征,形成规则重复结构。`;
|
||
if (type === 'chamfer') return `通过“${step.name}”处理选定锐边,形成装配导入面并去除尖锐边缘。`;
|
||
if (type === 'fillet') return `通过“${step.name}”对选定边进行圆角过渡,改善应力分布和边缘触感。`;
|
||
return `执行建模步骤“${step.name}”,在前序几何基础上形成该步骤对应的有效几何。`;
|
||
}
|
||
|
||
function stepAnnotation(modelName, step, feature, geometry) {
|
||
const evidence = dimensionEvidence(feature, step, modelName);
|
||
const faceCount = feature?.owned_faces?.length ?? 0;
|
||
const group = GROUP_DEFINITIONS[stepGroup(step)];
|
||
const compoundProfile = step.type === 'extrude_add' && geometry.interfaceCount > 0
|
||
? `该复合草图一次拉伸同时形成主体外轮廓与 ${geometry.interfaceCount} 个圆形内轮廓。`
|
||
: '';
|
||
return {
|
||
modelingDescription: `${operationIntent(step)}${compoundProfile}该步骤在本零件中归入“${group.name}”。`,
|
||
supplementDescription: [
|
||
`重建顺序第 ${step.order} 步,绑定源特征 ${step.sourceFeatureId}。`,
|
||
geometry.size.length ? `最终几何包络约为 ${geometry.size.map(formatNumber).join(' × ')} mm。` : null,
|
||
compoundProfile ? `圆形接口证据:${geometry.interfaceSummary}。` : null,
|
||
evidence.length ? `关键参数证据:${evidence.join(';')}。` : '源特征未提供可直接解释的标量尺寸,语义依据操作类型与几何结果确定。',
|
||
faceCount ? `源特征记录 ${faceCount} 个归属面,可用于后续拓扑复核。` : '源特征未记录独占面,需结合前后步骤或最终拓扑复核。'
|
||
].filter(Boolean).join(''),
|
||
remark: `${MARKER} 参数来自 SolidWorks 提取结果;角度、阵列计数及孔向导深度可能采用插件编码或构造深度,不应在未核对原模型前作为公差或有效加工深度。`
|
||
};
|
||
}
|
||
|
||
function groupSteps(histories) {
|
||
const result = new Map();
|
||
for (const step of histories) {
|
||
const key = stepGroup(step);
|
||
if (!result.has(key)) result.set(key, []);
|
||
result.get(key).push(step);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function structurePayload(groupKey, steps, classInfo) {
|
||
const definition = GROUP_DEFINITIONS[groupKey];
|
||
const names = steps.map((step) => `${step.order}.${step.name}`).join('、');
|
||
return {
|
||
structureName: definition.name,
|
||
structureType: definition.type,
|
||
purpose: definition.purpose,
|
||
solution: `由建模步骤 ${names} 共同实现;具体尺寸和几何范围见各步骤标注。`,
|
||
reason: `${definition.reason} 该判断同时结合“${classInfo.label}”的典型用途。`,
|
||
remark: `${MARKER} 结构由实际建模步骤按几何作用归组;若后续获得装配图,应复核其主/辅助结构属性。`,
|
||
historyIds: steps.map((step) => step.historyId)
|
||
};
|
||
}
|
||
|
||
function geometryInterfaceStructurePayload(step, geometry, classInfo) {
|
||
return {
|
||
structureName: '复合草图孔系与圆弧接口',
|
||
structureType: '孔系/圆弧轮廓结构',
|
||
purpose: '在主体成形步骤中同步建立圆形内轮廓,用于紧固、定位、穿轴或装配避让。',
|
||
solution: `由建模步骤 ${step.order}.${step.name} 的复合草图一次拉伸形成;检测到 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`,
|
||
reason: `复合草图将主体外轮廓与接口轮廓置于同一成形步骤,可保持相对位置一致;该判断同时结合“${classInfo.label}”的典型用途。`,
|
||
remark: `${MARKER} 圆形接口依据源特征归属面的圆柱几何识别;孔的用途、配合等级与加工公差需结合装配对象复核。`,
|
||
historyIds: [step.historyId]
|
||
};
|
||
}
|
||
|
||
function functionPayloads(classInfo, structures) {
|
||
const allIds = structures.map((item) => item.id);
|
||
const interfaceStructures = structures.filter((item) => ['cut', 'hole', 'thread', 'counterbore', 'pattern', 'geometry_interface'].includes(item.groupKey));
|
||
const edgeStructures = structures.filter((item) => item.groupKey === 'edge');
|
||
const payloads = [{
|
||
functionName: classInfo.primaryFunction,
|
||
functionType: '主功能',
|
||
goalDescription: `使该${classInfo.label}在“${classInfo.scene}”场景下完成主体承载、几何定位和连接载荷传递。`,
|
||
remark: `${MARKER} 主功能依据数据集分类、零件包络和完整建模历史综合推定,额定载荷与配合关系需结合装配数据确认。`,
|
||
structureIds: allIds
|
||
}];
|
||
if (interfaceStructures.length) {
|
||
payloads.push({
|
||
functionName: '紧固、定位与装配适配',
|
||
functionType: '装配功能',
|
||
goalDescription: '利用孔、槽、螺纹、沉头或阵列接口约束配合件位置,容纳标准紧固件并提供必要装配间隙。',
|
||
remark: `${MARKER} 具体紧固件规格、孔公差、配合等级和主辅接口关系需结合装配对象复核。`,
|
||
structureIds: interfaceStructures.map((item) => item.id)
|
||
});
|
||
}
|
||
if (edgeStructures.length) {
|
||
payloads.push({
|
||
functionName: '装配导入与边缘耐久',
|
||
functionType: '辅助功能',
|
||
goalDescription: '通过圆角和倒角减少锐边干涉,改善装配导入,并降低局部磕碰或应力集中风险。',
|
||
remark: `${MARKER} 边缘处理功能依据特征类型确定;是否属于关键疲劳区域需结合载荷工况判断。`,
|
||
structureIds: edgeStructures.map((item) => item.id)
|
||
});
|
||
}
|
||
return payloads;
|
||
}
|
||
|
||
function modelPayload(model, source, histories, classInfo, geometry) {
|
||
const box = boundingBox(source);
|
||
const boxText = box ? `零件几何包络约为 ${box.join(' × ')} mm。` : '源数据未提供可解析的零件包络。';
|
||
const interfaceText = geometry.interfaceCount > 0
|
||
? `复合草图形成 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`
|
||
: '未从源特征归属面中识别出独立的小直径圆形接口。';
|
||
const sequence = histories.map((step) => `${step.order}.${step.name}(${step.type})`).join(';');
|
||
const hasRevolve = histories.some((step) => step.type.includes('revolve'));
|
||
const hasHole = histories.some((step) => step.type === 'hole') || geometry.interfaceCount > 0;
|
||
const manufacturing = [
|
||
hasRevolve ? '回转表面车削或车铣复合加工' : 'CNC 铣削或轮廓加工',
|
||
hasHole ? '钻孔/扩孔/攻丝等孔系加工' : null,
|
||
histories.some((step) => step.type === 'chamfer' || step.type === 'fillet') ? '边缘倒角/圆角处理' : null
|
||
].filter(Boolean).join(';');
|
||
return {
|
||
applicationScenario: classInfo.scene,
|
||
material: 'SolidWorks 源模型未指定材料;数据集建议铝合金或钢制测试件,需按载荷与环境确认',
|
||
manufacturingMethod: `${manufacturing}(依据建模特征推定,待工艺确认)`,
|
||
userRequirement: `设计一件${classInfo.label},用于${classInfo.scene}。${boxText}${interfaceText}需保留建模历史中定义的主体、孔槽、连接接口和边缘处理,并保证各接口的相对位置与重建顺序一致。`,
|
||
designDescription: `${model.modelName} 被识别为${classInfo.label}。${boxText}${interfaceText}有效建模序列为:${sequence}。主体加料特征建立承载包络,独立切除、孔向导或复合草图内轮廓形成装配接口,阵列保证重复结构一致,倒角/圆角改善边缘装配性。材料、载荷、配合公差和制造基准未由源 JSON 完整给出,相关结论均需工程复核。`
|
||
};
|
||
}
|
||
|
||
function hasText(value) {
|
||
return typeof value === 'string' && value.trim().length > 0;
|
||
}
|
||
|
||
function isCompleteAnnotation(exportData, historyCount) {
|
||
const annotatedCount = (exportData.featureIndex ?? []).filter((item) =>
|
||
hasText(item.annotation?.modelingIntent)
|
||
&& hasText(item.annotation?.description)
|
||
&& hasText(item.annotation?.notes)
|
||
).length;
|
||
return annotatedCount === historyCount
|
||
&& (exportData.structures?.length ?? 0) > 0
|
||
&& (exportData.functions?.length ?? 0) > 0
|
||
&& (exportData.unassignedHistoryIds?.length ?? 0) === 0;
|
||
}
|
||
|
||
function hasUnmanagedAnnotation(exportData) {
|
||
const annotations = exportData.featureIndex ?? [];
|
||
const existingNotes = annotations.map((item) => item.annotation?.notes).filter(Boolean);
|
||
const structureNotes = (exportData.structures ?? []).map((item) => item.notes).filter(Boolean);
|
||
const functionNotes = (exportData.functions ?? []).map((item) => item.notes).filter(Boolean);
|
||
const notes = [...existingNotes, ...structureNotes, ...functionNotes];
|
||
return notes.some((note) => !String(note).startsWith(MARKER));
|
||
}
|
||
|
||
async function ensureStructure(modelId, payload, existingByName) {
|
||
const existing = existingByName.get(payload.structureName);
|
||
if (!APPLY) return { id: `dry:${payload.structureName}`, groupKey: null };
|
||
const saved = existing
|
||
? await post(`/cad-models/${modelId}/structures/${existing.id}`, payload)
|
||
: await post(`/cad-models/${modelId}/structures`, payload);
|
||
existingByName.set(saved.structureName, saved);
|
||
return saved;
|
||
}
|
||
|
||
async function ensureFunction(modelId, payload, existingByName) {
|
||
const existing = existingByName.get(payload.functionName);
|
||
if (!APPLY) return { id: `dry:${payload.functionName}` };
|
||
const saved = existing
|
||
? await post(`/cad-models/${modelId}/functions/${existing.id}`, payload)
|
||
: await post(`/cad-models/${modelId}/functions`, payload);
|
||
existingByName.set(saved.functionName, saved);
|
||
return saved;
|
||
}
|
||
|
||
async function annotateModel(model) {
|
||
const [histories, exportData] = await Promise.all([
|
||
api(`/cad-models/${model.id}/histories`),
|
||
api(`/cad-models/${model.id}/export/annotation`)
|
||
]);
|
||
if (!histories.length) return { model: model.modelName, status: 'skipped:no_histories' };
|
||
if (!FORCE && isCompleteAnnotation(exportData, histories.length)) {
|
||
return { model: model.modelName, status: 'skipped:already_complete' };
|
||
}
|
||
if (!FORCE && hasUnmanagedAnnotation(exportData)) {
|
||
return { model: model.modelName, status: 'skipped:existing_manual_annotation' };
|
||
}
|
||
|
||
const source = sourceFor(model.modelName);
|
||
const geometry = geometryAnalysis(source);
|
||
const classInfo = classFor(model.modelName, geometry);
|
||
const featuresById = new Map((source.features ?? []).map((feature) => [feature.id, feature]));
|
||
const normalizedHistories = histories.map((step) => ({
|
||
historyId: step.id,
|
||
sourceFeatureId: step.sourceFeatureId,
|
||
order: step.historyIndex,
|
||
name: step.historyName,
|
||
type: step.operationType
|
||
}));
|
||
const groups = groupSteps(normalizedHistories);
|
||
const geometryStep = geometry.interfaceCount > 0
|
||
? normalizedHistories.find((step) => step.type === 'extrude_add')
|
||
: null;
|
||
const modelUpdate = modelPayload(model, source, normalizedHistories, classInfo, geometry);
|
||
const dryRunStructures = [...groups.keys()].map((groupKey, index) => ({ id: index + 1, groupKey }));
|
||
if (geometryStep) dryRunStructures.push({ id: dryRunStructures.length + 1, groupKey: 'geometry_interface' });
|
||
|
||
if (!APPLY) {
|
||
return {
|
||
model: model.modelName,
|
||
status: 'dry-run',
|
||
histories: histories.length,
|
||
structures: dryRunStructures.length,
|
||
functions: functionPayloads(classInfo, dryRunStructures).length,
|
||
circularInterfaces: geometry.interfaceCount,
|
||
interfaceSummary: geometry.interfaceSummary || undefined
|
||
};
|
||
}
|
||
|
||
await post(`/cad-models/${model.id}`, modelUpdate);
|
||
for (const step of normalizedHistories) {
|
||
await post(
|
||
`/cad-models/${model.id}/histories/${step.historyId}/annotation`,
|
||
stepAnnotation(model.modelName, step, featuresById.get(step.sourceFeatureId), geometry)
|
||
);
|
||
}
|
||
|
||
const existingStructures = new Map((exportData.structures ?? []).map((item) => [item.name, { id: item.id }]));
|
||
const structures = [];
|
||
for (const [groupKey, steps] of groups) {
|
||
const saved = await ensureStructure(model.id, structurePayload(groupKey, steps, classInfo), existingStructures);
|
||
structures.push({ ...saved, groupKey });
|
||
}
|
||
if (geometryStep) {
|
||
const saved = await ensureStructure(
|
||
model.id,
|
||
geometryInterfaceStructurePayload(geometryStep, geometry, classInfo),
|
||
existingStructures
|
||
);
|
||
structures.push({ ...saved, groupKey: 'geometry_interface' });
|
||
}
|
||
|
||
const existingFunctions = new Map((exportData.functions ?? []).map((item) => [item.name, { id: item.id }]));
|
||
for (const payload of functionPayloads(classInfo, structures)) {
|
||
await ensureFunction(model.id, payload, existingFunctions);
|
||
}
|
||
|
||
const verified = await api(`/cad-models/${model.id}/export/annotation`);
|
||
const annotatedCount = (verified.featureIndex ?? []).filter((item) =>
|
||
hasText(item.annotation?.modelingIntent)
|
||
&& hasText(item.annotation?.description)
|
||
&& hasText(item.annotation?.notes)
|
||
).length;
|
||
if (!isCompleteAnnotation(verified, histories.length)) {
|
||
throw new Error(`${model.modelName} 校验失败: annotated=${annotatedCount}/${histories.length}, structures=${verified.structures?.length ?? 0}, functions=${verified.functions?.length ?? 0}, unassigned=${verified.unassignedHistoryIds}`);
|
||
}
|
||
return {
|
||
model: model.modelName,
|
||
status: 'applied',
|
||
histories: histories.length,
|
||
structures: verified.structures.length,
|
||
functions: verified.functions.length,
|
||
unassigned: verified.unassignedHistoryIds.length
|
||
};
|
||
}
|
||
|
||
async function main() {
|
||
const modelsByPage = [];
|
||
for (let pageNumber = 1; ; pageNumber += 1) {
|
||
const page = await api(`/cad-models?page=${pageNumber}&size=100`);
|
||
modelsByPage.push(...page.items);
|
||
if (modelsByPage.length >= page.total || page.items.length === 0) break;
|
||
}
|
||
let models = modelsByPage.sort((left, right) => left.modelName.localeCompare(right.modelName));
|
||
if (onlyModel) models = models.filter((model) => model.modelName === onlyModel);
|
||
if (!models.length) throw new Error(onlyModel ? `未找到模型 ${onlyModel}` : '数据库中没有模型');
|
||
|
||
console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'} models=${models.length} sourceRoot=${SOURCE_ROOT}`);
|
||
const results = [];
|
||
for (const model of models) {
|
||
const result = await annotateModel(model);
|
||
results.push(result);
|
||
console.log(JSON.stringify(result));
|
||
}
|
||
const applied = results.filter((item) => item.status === 'applied').length;
|
||
const dryRun = results.filter((item) => item.status === 'dry-run').length;
|
||
const skipped = results.length - applied - dryRun;
|
||
console.log(JSON.stringify({ total: results.length, applied, dryRun, skipped }));
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(error.stack ?? error.message);
|
||
process.exitCode = 1;
|
||
});
|