(() => { '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 `${escape(label)}`; }).join(''); $('connection-strip').innerHTML = state.edges.map(edge => ``).join('') || '依次点击两个空闲端口创建连接。'; } function setMode(mode) { state.mode = mode; pendingPort = null; render(); save(); } function selectEdge(id, open = false) { state.selected = id; pendingPort = null; render(); save(); if (open && state.mode === 'submodel') openChooser(id); } function renderInspector() { const edge = selected(); $('inspector-title').textContent = state.mode === 'check' ? '配置检查' : state.mode === 'parameters' ? '连线参数' : '连线'; $('selected-id').textContent = state.mode === 'check' ? '局部演示' : edge?.id || ''; if (state.mode === 'check') { renderChecks(); return; } if (!edge) { $('inspector-body').innerHTML = '

选择连接线,或在绘图模式连接两个空闲端口。

'; return; } const model = modelById(edge.model); let html = `
${escape(ports[edge.a].label)}
↔ ${escape(ports[edge.b].label)}
`; if (state.mode === 'sketch') { html += '

先完成连接。进入子模型阶段后,为连接线选择计算模型。

'; } else { html += `
当前子模型
${escape(model?.id || '尚未选择')}
${escape(model?.effect || '根据两端接口筛选,保留用户选择。')}
`; if (model) html += `

${escape(orientation(edge, model))}

`; html += `
${state.mode === 'submodel' && model ? '' : ''}
`; if (state.mode === 'parameters' && model) { html += model.fields.length ? `
${model.fields.map(key => { const field = fields[key]; const label = model.id === 'PNL0003' && ['pressure', 'temperature'].includes(key) ? '管路端口 1 ' + field.label : field.label; return ``; }).join('')}
` : '

理想连接无需管路参数。

'; } } $('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 = `
${complete ? '局部连接配置检查通过' : '还有配置需要完成'}

此检查覆盖示例端口匹配和必填参数;未调用系统编译器或求解器。

${state.edges.map(edge => `
${edge.id} · ${escape(edge.model || '未选')}
${escape(edgeStatus(edge))}
${edgeStatus(edge) !== '已配置' ? `` : ''}
`).join('')}${free.length ? `
${free.length} 个示例端口未连接
${free.map(id => escape(ports[id].label)).join('、')}
` : ''}
`; } function render() { document.querySelectorAll('[data-mode]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.mode === state.mode))); const hints = { sketch: '示例已连线,可进入“子模型”;也可删除连线后重新连接端口。', submodel: '点击连接线选择兼容子模型。pnode 保持不变,管路方向自动匹配。', parameters: '点击管路填写实际尺寸。理想连接无需参数。', check: '检查本示例的端口匹配和必填参数。', }; $('mode-hint').textContent = hints[state.mode]; const next = { sketch: '选择子模型 →', submodel: '填写参数 →', parameters: '检查配置 →', check: '返回绘图' }; $('next-mode').textContent = next[state.mode]; layout(); renderInspector(); updateProgress(); } function openChooser(id) { const edge = state.edges.find(item => item.id === id); if (!edge) return; dialogEdge = id; const available = candidates(edge), selectedModel = edge.model || available[0]?.id; $('dialog-title').textContent = `选择 ${id} 的子模型`; $('dialog-endpoints').textContent = `${ports[edge.a].label} ↔ ${ports[edge.b].label}`; $('model-options').innerHTML = available.map((model, index) => ``).join(''); const describe = id => `${ports[id].label}:${ports[id].state === 'out' ? '提供温度、压力,接收质量与能量流率' : '接收温度、压力,提供质量与能量流率'}`; $('port-requirements').innerHTML = `
${escape(describe(edge.a))}
${escape(describe(edge.b))}
`; $('excluded-models').innerHTML = models.filter(model => !matches(edge, model)).map(model => `
${model.id}端口变量供需不匹配
`).join(''); $('apply-model').disabled = !available.length; $('apply-next').disabled = !available.length; document.querySelector('.compatibility-details').open = false; if (!$('model-dialog').open) $('model-dialog').showModal(); } function applyModel(next) { const edge = state.edges.find(item => item.id === dialogEdge); const id = document.querySelector('input[name="model"]:checked')?.value; const model = modelById(id); if (!edge || !model || !matches(edge, model)) return; edge.model = id; // Keep shared inputs when changing a model. Inactive inputs remain private to this draft. model.fields.forEach(key => { if (edge.params[key] === undefined) edge.params[key] = fields[key].value; }); state.selected = edge.id; $('model-dialog').close(); render(); save(); message(`${edge.id} 已选择 ${id}。${model.effect}`); if (next) { const unassigned = state.edges.find(item => !item.model); if (unassigned) { selectEdge(unassigned.id); openChooser(unassigned.id); } else { setMode('parameters'); state.selected = state.edges.find(item => missingFields(item).length)?.id || edge.id; render(); save(); message('所有连接线已选型。填写实际管长、内径后检查配置。'); } } } function portClick(id) { if (state.mode !== 'sketch') { message('如需修改连接,请切换到“绘图”。'); return; } if (pendingPort === id) { pendingPort = null; layout(); message('已取消连线。'); return; } if (state.edges.some(edge => edge.a === id || edge.b === id)) { message('该端口已有连接。选中原连线可删除后重连。'); return; } if (!pendingPort) { pendingPort = id; layout(); message(`起点:${ports[id].label}。请选择另一个部件的空闲端口。`); return; } if (ports[pendingPort].component === ports[id].component) { message('请选择另一个部件的端口。'); return; } let index = 1; while (state.edges.some(edge => edge.id === `L${index}`)) index++; const edge = { id: `L${index}`, a: pendingPort, b: id, model: null, params: {} }; state.edges.push(edge); state.selected = edge.id; pendingPort = null; render(); save(); message(`${edge.id} 已连接。子模型稍后选择。`); } document.querySelectorAll('[data-mode]').forEach(button => button.addEventListener('click', () => setMode(button.dataset.mode))); $('next-mode').addEventListener('click', () => setMode({ sketch: 'submodel', submodel: 'parameters', parameters: 'check', check: 'sketch' }[state.mode])); $('wires').addEventListener('click', event => { const target = event.target.closest('[data-edge]'); if (target) selectEdge(target.dataset.edge, true); }); $('connection-strip').addEventListener('click', event => { const target = event.target.closest('[data-edge]'); if (target) selectEdge(target.dataset.edge, true); }); $('ports').addEventListener('click', event => { const target = event.target.closest('[data-port]'); if (target) portClick(target.dataset.port); }); $('inspector-body').addEventListener('click', event => { const fix = event.target.closest('[data-fix]'); if (fix) { state.selected = fix.dataset.fix; const edge = selected(); setMode(edge.model ? 'parameters' : 'submodel'); if (!edge.model) openChooser(edge.id); return; } const action = event.target.closest('[data-action]')?.dataset.action; if (action === 'choose') openChooser(state.selected); else if (action === 'delete') { state.edges = state.edges.filter(edge => edge.id !== state.selected); state.selected = state.edges[0]?.id || null; pendingPort = null; render(); save(); message('连线已删除。依次点击两个空闲端口即可重连。'); } else if (['sketch', 'submodel', 'parameters'].includes(action)) setMode(action); }); $('inspector-body').addEventListener('input', event => { const key = event.target.dataset.parameter, edge = selected(); if (!fields[key] || !edge) return; edge.params[key] = event.target.value; event.target.setAttribute('aria-invalid', String(!validField(key, event.target.value))); updateParameterState(); updateProgress(); save(); }); $('inspector-body').addEventListener('submit', event => event.preventDefault()); $('apply-model').addEventListener('click', () => applyModel(false)); $('apply-next').addEventListener('click', () => applyModel(true)); $('reset').addEventListener('click', () => { $('model-dialog').close(); state = initial(); pendingPort = null; dialogEdge = null; render(); save(); message('已恢复最初的四条连接;此演示的选型和参数已清空。'); }); document.addEventListener('keydown', event => { if (event.key === 'Escape' && pendingPort) { pendingPort = null; layout(); message('已取消连线。'); } }); load(); render(); save(); new ResizeObserver(layout).observe($('canvas')); })();