(() => {
'use strict';
const STORAGE_KEY = 'system-simulation:amesim-submodel-demo:v1';
const $ = id => document.getElementById(id);
const escape = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
// Local, deliberately bounded demonstration of variable-level interface matching.
// These declarations are not the production component catalogue or a numerical solver.
const ports = {
'A.1': { label: '储气罐 A · 端口 1', component: 'A', state: 'out' },
'N1.1': { label: '节点 N1 · 端口 1', component: 'N1', state: 'out', number: '1' },
'N1.2': { label: '节点 N1 · 端口 2(参考)', component: 'N1', state: 'in', number: '2 参考' },
'N1.3': { label: '节点 N1 · 端口 3', component: 'N1', state: 'out', number: '3' },
'N1.4': { label: '节点 N1 · 端口 4', component: 'N1', state: 'out', number: '4' },
'B.2': { label: '阀门 B · 端口 2', component: 'B', state: 'in' },
'C.1': { label: '封堵 C · 端口 1', component: 'C', state: 'in' },
'D.1': { label: '封堵 D · 端口 1', component: 'D', state: 'in' },
};
const fields = {
length: { label: '管长(m)', min: 0, exclusive: true, step: 'any', value: '' },
diameter: { label: '内径(mm)', min: 0, exclusive: true, step: 'any', value: '' },
roughness: { label: '相对粗糙度', min: 0, max: 0.1, step: 'any', value: '0.00001' },
pressure: { label: '初始压力(bar,绝压)', min: 0, exclusive: true, step: 'any', value: '1' },
temperature: { label: '初始温度(K)', min: 0, exclusive: true, step: 'any', value: '293.15' },
pressure2: { label: '管路端口 2 初始压力(bar,绝压)', min: 0, exclusive: true, step: 'any', value: '1' },
temperature2: { label: '管路端口 2 初始温度(K)', min: 0, exclusive: true, step: 'any', value: '293.15' },
};
const models = [
{ id: 'DIRECT', label: '理想连接', ends: null, effect: '不计管路压降,不增加储气容积。', fields: [] },
{ id: 'PNL00R', label: '阻性管路', ends: ['in', 'in'], effect: '考虑摩擦压降;不增加储气状态。', fields: ['length', 'diameter', 'roughness'] },
{ id: 'PNL0001', label: '单端储气管路', ends: ['in', 'out'], effect: '考虑摩擦压降;一处储气容积,2 个热力状态。', fields: ['length', 'diameter', 'roughness', 'pressure', 'temperature'] },
{ id: 'PNL0002', label: '中间储气管路', ends: ['in', 'in'], effect: '考虑摩擦压降;中间储气容积,2 个热力状态。', fields: ['length', 'diameter', 'roughness', 'pressure', 'temperature'] },
{ id: 'PNL0003', label: '双端储气管路', ends: ['out', 'out'], effect: '考虑摩擦压降;两端储气容积,4 个热力状态。', fields: ['length', 'diameter', 'roughness', 'pressure', 'temperature', 'pressure2', 'temperature2'] },
];
const modelById = id => models.find(model => model.id === id);
const initial = () => ({ version: 1, mode: 'sketch', selected: 'L1', edges: [
{ id: 'L1', a: 'A.1', b: 'N1.1', model: null, params: {} },
{ id: 'L2', a: 'N1.2', b: 'B.2', model: null, params: {} },
{ id: 'L3', a: 'C.1', b: 'N1.3', model: null, params: {} },
{ id: 'L4', a: 'N1.4', b: 'D.1', model: null, params: {} },
] });
let state = initial(), pendingPort = null, dialogEdge = null, positions = {};
let storageAvailable = true;
function matches(edge, model) {
const a = ports[edge.a].state, b = ports[edge.b].state;
if (!model.ends) return a !== b;
return (a !== model.ends[0] && b !== model.ends[1]) || (a !== model.ends[1] && b !== model.ends[0]);
}
function candidates(edge) { return models.filter(model => matches(edge, model)); }
function load() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
if (!saved || saved.version !== 1 || !Array.isArray(saved.edges) || saved.edges.length > 4) return;
const occupied = new Set(), ids = new Set();
const valid = saved.edges.every(edge => {
if (!edge || !/^L[1-9]\d*$/.test(edge.id) || ids.has(edge.id) || !ports[edge.a] || !ports[edge.b] ||
ports[edge.a].component === ports[edge.b].component || occupied.has(edge.a) || occupied.has(edge.b)) return false;
if (edge.model !== null && (!modelById(edge.model) || !matches(edge, modelById(edge.model)))) return false;
ids.add(edge.id); occupied.add(edge.a); occupied.add(edge.b);
return true;
});
if (!valid) return;
state = {
version: 1,
mode: ['sketch', 'submodel', 'parameters', 'check'].includes(saved.mode) ? saved.mode : 'sketch',
selected: ids.has(saved.selected) ? saved.selected : saved.edges[0]?.id || null,
edges: saved.edges.map(edge => ({ id: edge.id, a: edge.a, b: edge.b, model: edge.model,
params: Object.fromEntries(Object.keys(fields).filter(key => ['string', 'number'].includes(typeof edge.params?.[key])).map(key => [key, String(edge.params[key]).slice(0, 64)])) })),
};
} catch { storageAvailable = false; }
}
function save() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); storageAvailable = true; }
catch { storageAvailable = false; }
$('save-state').textContent = storageAvailable ? '仅保存此演示 · 与正式工程分离' : '当前浏览器未保存 · 刷新将丢失演示修改';
}
function selected() { return state.edges.find(edge => edge.id === state.selected); }
function message(text) { $('notice').textContent = text; }
function validField(key, value) {
const field = fields[key], number = Number(value);
return String(value ?? '').trim() !== '' && Number.isFinite(number) && (field.exclusive ? number > field.min : number >= field.min) && (field.max === undefined || number <= field.max);
}
function missingFields(edge) {
return (modelById(edge.model)?.fields || []).filter(key => !validField(key, edge.params[key]));
}
function edgeStatus(edge) {
if (!edge.model) return '未选子模型';
return missingFields(edge).length ? '待填参数' : '已配置';
}
function orientation(edge, model) {
if (!model.ends || model.ends[0] === model.ends[1]) return '两端接口对称';
const forward = ports[edge.a].state !== model.ends[0];
return `管路端口 1 → ${ports[forward ? edge.a : edge.b].label};端口 2 → ${ports[forward ? edge.b : edge.a].label}`;
}
function updateProgress() {
const chosen = state.edges.filter(edge => edge.model).length;
const configured = state.edges.filter(edge => edge.model && missingFields(edge).length === 0).length;
$('progress').textContent = `${state.edges.length} 条连接 · ${chosen} 条已选子模型 · ${configured} 条参数完整`;
}
function layout() {
const width = $('canvas').clientWidth, height = $('canvas').clientHeight;
$('wires').setAttribute('viewBox', `0 0 ${width} ${height}`);
positions = {
'A.1': [width * .16 + 29, height * .54],
'N1.1': [width * .48 - 19, height * .54], 'N1.2': [width * .48 + 19, height * .54],
'N1.3': [width * .48, height * .54 - 19], 'N1.4': [width * .48, height * .54 + 19],
'B.2': [width * .84 - 29, height * .54],
'C.1': [width * .48, height * .14 + 4], 'D.1': [width * .48, height * .87 - 4],
};
$('ports').innerHTML = Object.entries(ports).map(([id, port]) => ``).join('');
renderWires();
}
function renderWires() {
if (!positions['A.1']) return;
$('wires').innerHTML = state.edges.map(edge => {
const [ax, ay] = positions[edge.a], [bx, by] = positions[edge.b];
const horizontal = Math.abs(bx - ax) >= Math.abs(by - ay);
const path = horizontal ? `M${ax},${ay} H${(ax + bx) / 2} V${by} H${bx}` : `M${ax},${ay} V${(ay + by) / 2} H${bx} V${by}`;
const x = horizontal ? (ax + bx) / 2 : (ax + bx) / 2 + 13;
const y = horizontal ? (ay + by) / 2 - 12 : (ay + by) / 2;
const label = state.mode === 'sketch' ? edge.id : `${edge.id} · ${edge.model || '未选'}`;
return `
选择连接线,或在绘图模式连接两个空闲端口。
'; return; } const model = modelById(edge.model); let html = `先完成连接。进入子模型阶段后,为连接线选择计算模型。
'; } else { html += `${escape(orientation(edge, model))}
`; html += `理想连接无需管路参数。
'; } } $('inspector-body').innerHTML = html; updateParameterState(); } function updateParameterState() { const target = $('parameter-state'), edge = selected(); if (!target || !edge) return; const missing = missingFields(edge); target.classList.toggle('valid', missing.length === 0); target.textContent = missing.length ? `待填写或修正 ${missing.length} 项` : '参数完整 · 已保存到此演示'; } function renderChecks() { const occupied = new Set(state.edges.flatMap(edge => [edge.a, edge.b])); const free = Object.keys(ports).filter(id => !occupied.has(id)); const complete = !free.length && state.edges.every(edge => edge.model && matches(edge, modelById(edge.model)) && !missingFields(edge).length); $('inspector-body').innerHTML = `此检查覆盖示例端口匹配和必填参数;未调用系统编译器或求解器。