Clean up unused BMAD workflow, agent, and command files across all IDE configurations (.agent, .clinerules, .cursor, .gemini, .github, .kilocode, .opencode) and internal module files (_bmad/bmb, _bmad/bmm). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
641 lines
35 KiB
HTML
641 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Entropyk — Éditeur visuel de machine</title>
|
||
<style>
|
||
:root { --bg: #0f1419; --surface: #1a1f26; --card: #252d38; --accent: #58a6ff; --green: #3fb950; --orange: #d29922; --red: #f85149; --text: #e6edf3; --muted: #8b949e; }
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
||
.topbar { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; background: var(--surface); border-bottom: 1px solid #30363d; flex-shrink: 0; }
|
||
.topbar h1 { font-size: 1rem; font-weight: 600; margin: 0; color: var(--accent); }
|
||
.topbar input, .topbar select { padding: 0.35rem 0.6rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); }
|
||
.topbar .spacer { flex: 1; }
|
||
.topbar button { padding: 0.4rem 0.8rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; }
|
||
.topbar button.primary { background: var(--accent); color: #fff; }
|
||
.topbar button.secondary { background: var(--card); color: var(--text); border: 1px solid #30363d; }
|
||
.topbar button:hover { filter: brightness(1.1); }
|
||
.main { display: flex; flex: 1; min-height: 0; }
|
||
.palette { width: 200px; background: var(--surface); border-right: 1px solid #30363d; padding: 0.75rem; overflow-y: auto; flex-shrink: 0; }
|
||
.palette h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
|
||
.palette-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem; margin-bottom: 0.25rem; background: var(--card); border-radius: 8px; cursor: grab; border: 2px solid transparent; font-size: 0.875rem; }
|
||
.palette-item:hover { border-color: var(--accent); }
|
||
.palette-item .icon { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.75rem; }
|
||
.palette-item.Compressor .icon { background: #23863633; color: var(--green); }
|
||
.palette-item.Condenser .icon, .palette-item.CondenserWater .icon, .palette-item.CondenserAir .icon { background: #1f6feb33; color: var(--accent); }
|
||
.palette-item.Evaporator .icon, .palette-item.EvaporatorWater .icon, .palette-item.EvaporatorAir .icon { background: #8957e533; color: #a371f7; }
|
||
.palette-item.ExpansionValve .icon { background: #d2992233; color: var(--orange); }
|
||
.palette-item.Placeholder .icon { background: #6e768133; color: var(--muted); }
|
||
.palette-item.Pump .icon { background: #388bfd33; color: #58a6ff; }
|
||
.palette-item.HeatExchanger .icon { background: #da363633; color: var(--red); }
|
||
.palette-item.WaterSource .icon { background: #23863633; color: var(--green); }
|
||
.palette-item.WaterSink .icon { background: #da363633; color: var(--red); }
|
||
.palette-item.AirSource .icon { background: #58a6ff33; color: #79c0ff; }
|
||
.palette-item.AirSink .icon { background: #6e768133; color: var(--muted); }
|
||
.palette-item.RefrigerantSource .icon { background: #1f6feb33; color: var(--accent); }
|
||
.palette-item.RefrigerantSink .icon { background: #8957e533; color: #a371f7; }
|
||
.palette-item.FlowSplitter .icon { background: #d2992233; color: var(--orange); }
|
||
.palette-item.FlowMerger .icon { background: #d2992233; color: var(--orange); }
|
||
.canvas-wrap { flex: 1; position: relative; overflow: hidden; background: #0d1117; }
|
||
#canvas { display: block; width: 100%; height: 100%; cursor: default; }
|
||
.canvas-wrap.mode-add #canvas { cursor: crosshair; }
|
||
.canvas-wrap.mode-connect #canvas { cursor: pointer; }
|
||
.props { width: 280px; background: var(--surface); border-left: 1px solid #30363d; padding: 0.75rem; overflow-y: auto; flex-shrink: 0; }
|
||
.props h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
|
||
.props label { display: block; margin-top: 0.5rem; font-size: 0.8rem; color: var(--muted); }
|
||
.props input, .props select { width: 100%; padding: 0.4rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); margin-top: 0.2rem; }
|
||
.props .empty { font-size: 0.875rem; color: var(--muted); margin-top: 1rem; }
|
||
.props button.danger { background: var(--red); color: #fff; border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer; margin-top: 0.75rem; font-size: 0.8rem; }
|
||
.result { padding: 0.5rem; margin-top: 0.5rem; border-radius: 6px; font-size: 0.8rem; white-space: pre-wrap; max-height: 120px; overflow: auto; }
|
||
.result.ok { background: #23863622; border: 1px solid var(--green); }
|
||
.result.err { background: #f8514922; border: 1px solid var(--red); }
|
||
.mode-btns { display: flex; gap: 0.25rem; margin-bottom: 0.5rem; }
|
||
.mode-btns button { padding: 0.35rem 0.6rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); cursor: pointer; font-size: 0.8rem; }
|
||
.mode-btns button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<h1>Entropyk — Machine thermodynamique</h1>
|
||
<input type="text" id="projectName" placeholder="Nom du projet" style="width:180px" />
|
||
<label style="font-size:0.8rem;color:var(--muted)">Fluide</label>
|
||
<select id="fluid" style="width:100px"><option value="R410A">R410A</option><option value="R134a">R134a</option><option value="R744">R744</option></select>
|
||
<div class="spacer"></div>
|
||
<select id="exampleSelect" style="width:200px">
|
||
<option value="">Charger un exemple…</option>
|
||
<option value="/examples/simple_working.json">Cycle simple (placeholders)</option>
|
||
<option value="/examples/chiller_r410a_minimal.json">Chiller 2 circuits</option>
|
||
</select>
|
||
<button type="button" id="btnValidate" class="secondary">Valider</button>
|
||
<button type="button" id="btnRun" class="primary">Lancer simulation</button>
|
||
<button type="button" id="btnExport" class="secondary">Télécharger JSON</button>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<aside class="palette">
|
||
<h3>Composants</h3>
|
||
<p style="font-size:0.8rem;color:var(--muted);margin:0 0 0.5rem">Cliquez pour sélectionner, puis cliquez sur le schéma pour placer. Ou glissez-déposez.</p>
|
||
<div class="palette-info" style="font-size:0.7rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:0.4rem 0.5rem;margin-bottom:0.5rem;line-height:1.35">
|
||
<strong>Logique HVAC</strong> : Condenseur = réfrigérant (chaud) + eau/air (froid). Évaporateur = eau/air (chaud) + réfrigérant (froid). Compresseur = réfrigérant uniquement. Condenseur à air = ventilateur intégré (vitesse 0–1). Circuit air = Source air → … → Puits air (export Placeholder tant que le CLI n’a pas de circuit air dédié).
|
||
</div>
|
||
<div class="mode-btns">
|
||
<button type="button" id="modeMove" class="active">Déplacer</button>
|
||
<button type="button" id="modeAdd">Ajouter</button>
|
||
<button type="button" id="modeConnect">Relier</button>
|
||
</div>
|
||
<p id="modeHint" style="font-size:0.75rem;color:var(--muted);margin:0.25rem 0 0.5rem">Déplacer : glissez les blocs.</p>
|
||
<p id="connectStatus" style="font-size:0.8rem;font-weight:600;color:var(--accent);margin:0.25rem 0 0.5rem;min-height:1.2em;"></p>
|
||
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Sources & puits</h3>
|
||
<div class="palette-item WaterSource" data-type="WaterSource" draggable="true"><span class="icon">↓S</span> Source eau</div>
|
||
<div class="palette-item WaterSink" data-type="WaterSink" draggable="true"><span class="icon">S↑</span> Puits eau</div>
|
||
<div class="palette-item AirSource" data-type="AirSource" draggable="true"><span class="icon">↓A</span> Source air</div>
|
||
<div class="palette-item AirSink" data-type="AirSink" draggable="true"><span class="icon">A↑</span> Puits air</div>
|
||
<div class="palette-item RefrigerantSource" data-type="RefrigerantSource" draggable="true"><span class="icon">↓R</span> Source réfrig.</div>
|
||
<div class="palette-item RefrigerantSink" data-type="RefrigerantSink" draggable="true"><span class="icon">R↑</span> Puits réfrig.</div>
|
||
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Circuit frigorifique</h3>
|
||
<div class="palette-item Compressor" data-type="Compressor" draggable="true"><span class="icon">C</span> Compresseur (réfrigérant)</div>
|
||
<div class="palette-item CondenserWater" data-type="CondenserWater" draggable="true"><span class="icon">Co</span> Condenseur à eau</div>
|
||
<div class="palette-item CondenserAir" data-type="CondenserAir" draggable="true"><span class="icon">Co</span> Condenseur à air</div>
|
||
<div class="palette-item EvaporatorWater" data-type="EvaporatorWater" draggable="true"><span class="icon">E</span> Évaporateur à eau</div>
|
||
<div class="palette-item EvaporatorAir" data-type="EvaporatorAir" draggable="true"><span class="icon">E</span> Évaporateur à air</div>
|
||
<div class="palette-item ExpansionValve" data-type="ExpansionValve" draggable="true"><span class="icon">V</span> Détendeur</div>
|
||
<div class="palette-item HeatExchanger" data-type="HeatExchanger" draggable="true"><span class="icon">H</span> Échangeur générique (2 fluides)</div>
|
||
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Circuit eau / secondaire</h3>
|
||
<div class="palette-item Pump" data-type="Pump" draggable="true"><span class="icon">P</span> Pompe</div>
|
||
<div class="palette-item FlowSplitter" data-type="FlowSplitter" draggable="true"><span class="icon">⊳</span> Splitter</div>
|
||
<div class="palette-item FlowMerger" data-type="FlowMerger" draggable="true"><span class="icon">⊲</span> Merger</div>
|
||
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Divers</h3>
|
||
<div class="palette-item Placeholder" data-type="Placeholder" draggable="true"><span class="icon">?</span> Placeholder</div>
|
||
</aside>
|
||
|
||
<div class="canvas-wrap" id="canvasWrap">
|
||
<svg id="canvas" viewBox="0 0 1200 700" xmlns="http://www.w3.org/2000/svg"></svg>
|
||
</div>
|
||
|
||
<aside class="props">
|
||
<h3>Propriétés</h3>
|
||
<div id="propsEmpty" class="empty">Cliquez sur un composant sur le schéma pour modifier son nom et ses paramètres.</div>
|
||
<div id="propsForm" style="display:none">
|
||
<label>Nom (pour les connexions)</label>
|
||
<input type="text" id="propName" placeholder="ex: comp" />
|
||
<label>Type</label>
|
||
<input type="text" id="propType" readonly style="opacity:0.8" />
|
||
<div id="propParams"></div>
|
||
<button type="button" class="danger" id="btnDeleteNode">Supprimer le composant</button>
|
||
</div>
|
||
<div id="resultBox" style="display:none" class="result"></div>
|
||
</aside>
|
||
</div>
|
||
|
||
<script>
|
||
const COMPONENT_DEFAULTS = {
|
||
Placeholder: { n_equations: 2 },
|
||
WaterSource: { n_equations: 2 },
|
||
WaterSink: { n_equations: 2 },
|
||
AirSource: { n_equations: 2, t_air_c: 35, pressure_bar: 1.01, rh_percent: 50 },
|
||
AirSink: { n_equations: 2, pressure_bar: 1.01 },
|
||
RefrigerantSource: { n_equations: 2 },
|
||
RefrigerantSink: { n_equations: 2 },
|
||
FlowSplitter: { n_equations: 2 },
|
||
FlowMerger: { n_equations: 2 },
|
||
Compressor: { fluid: "R410A", speed_rpm: 2900, displacement_m3: 0.000025, efficiency: 0.82, m1: 0.88, m2: 2.2, m3: 450, m4: 1400, m5: -2, m6: 1.5, m7: 550, m8: 1500, m9: -2.5, m10: 1.8 },
|
||
CondenserWater: { ua: 5000, t_sat_cond_c: 45, ref_pressure_bar: 26, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 30, sec_pressure_bar: 2, sec_mass_flow_kg_s: 1.2 },
|
||
CondenserAir: { ua: 5000, t_sat_cond_c: 45, ref_pressure_bar: 26, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 35, sec_pressure_bar: 1.01, sec_mass_flow_kg_s: 2.5, fan_speed: 1, n_air_exponent: 0.5 },
|
||
EvaporatorWater: { ua: 6000, t_sat_evap_c: 5, superheat_k: 5, ref_pressure_bar: 7, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 12, sec_pressure_bar: 2, sec_mass_flow_kg_s: 1.0 },
|
||
EvaporatorAir: { ua: 6000, t_sat_evap_c: 5, superheat_k: 5, ref_pressure_bar: 7, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 7, sec_pressure_bar: 1.01, sec_mass_flow_kg_s: 2.0 },
|
||
ExpansionValve: { fluid: "R410A", opening: 1 },
|
||
HeatExchanger: { ua: 6000, hot_fluid: "Air", hot_t_inlet_c: 7, hot_pressure_bar: 1.01, hot_mass_flow_kg_s: 0.8, cold_fluid: "R410A", cold_t_inlet_c: 0, cold_pressure_bar: 7, cold_mass_flow_kg_s: 0.04 },
|
||
Pump: {}
|
||
};
|
||
const EXPORT_AS_PLACEHOLDER = ["WaterSource", "WaterSink", "AirSource", "AirSink", "RefrigerantSource", "RefrigerantSink", "FlowSplitter", "FlowMerger"];
|
||
const NODE_LABEL = {
|
||
WaterSource: "Src eau", WaterSink: "Puits eau", AirSource: "Src air", AirSink: "Puits air",
|
||
RefrigerantSource: "Src réf.", RefrigerantSink: "Puits réf.",
|
||
FlowSplitter: "Split", FlowMerger: "Merge",
|
||
CondenserWater: "Cond. eau", CondenserAir: "Cond. air", EvaporatorWater: "Évap. eau", EvaporatorAir: "Évap. air"
|
||
};
|
||
const PARAM_META = {
|
||
n_equations: { type: "number", label: "n_equations" },
|
||
ua: { type: "number", label: "UA (W/K)" },
|
||
t_sat_k: { type: "number", label: "t_sat_k" },
|
||
t_sat_cond_c: { type: "number", label: "T sat cond. (°C)" },
|
||
t_sat_evap_c: { type: "number", label: "T sat évap. (°C)" },
|
||
superheat_k: { type: "number", label: "Surchauffe (K)" },
|
||
fluid: { type: "text", label: "Réfrigérant" },
|
||
opening: { type: "number", label: "Ouverture" },
|
||
speed_rpm: { type: "number", label: "Vitesse (tr/min)" },
|
||
displacement_m3: { type: "number", label: "Cylindrée (m³)" },
|
||
efficiency: { type: "number", label: "Rendement" },
|
||
ref_pressure_bar: { type: "number", label: "P réfrig. (bar)" },
|
||
ref_mass_flow_kg_s: { type: "number", label: "Débit réfrig. (kg/s)" },
|
||
sec_t_inlet_c: { type: "number", label: "T entrée secondaire (°C)" },
|
||
sec_pressure_bar: { type: "number", label: "P secondaire (bar)" },
|
||
sec_mass_flow_kg_s: { type: "number", label: "Débit secondaire (kg/s)" },
|
||
fan_speed: { type: "number", label: "Vitesse ventilateur (0–1)" },
|
||
n_air_exponent: { type: "number", label: "Exposant n_air (loi débit)" },
|
||
t_air_c: { type: "number", label: "T air (°C)" },
|
||
pressure_bar: { type: "number", label: "P (bar)" },
|
||
rh_percent: { type: "number", label: "Humidité relative (%)" },
|
||
m1: { type: "number", label: "m1" }, m2: { type: "number", label: "m2" },
|
||
m3: { type: "number", label: "m3" }, m4: { type: "number", label: "m4" },
|
||
m5: { type: "number", label: "m5" }, m6: { type: "number", label: "m6" },
|
||
m7: { type: "number", label: "m7" }, m8: { type: "number", label: "m8" },
|
||
m9: { type: "number", label: "m9" }, m10: { type: "number", label: "m10" },
|
||
hot_fluid: { type: "text", label: "Fluide chaud" }, cold_fluid: { type: "text", label: "Fluide froid" },
|
||
hot_t_inlet_c: { type: "number", label: "T entrée chaud (°C)" }, cold_t_inlet_c: { type: "number", label: "T entrée froid (°C)" },
|
||
hot_pressure_bar: { type: "number", label: "P chaud (bar)" }, cold_pressure_bar: { type: "number", label: "P froid (bar)" },
|
||
hot_mass_flow_kg_s: { type: "number", label: "Débit chaud (kg/s)" }, cold_mass_flow_kg_s: { type: "number", label: "Débit froid (kg/s)" }
|
||
};
|
||
const PARAM_ORDER = {
|
||
CondenserWater: ["ua", "t_sat_cond_c", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"],
|
||
CondenserAir: ["ua", "t_sat_cond_c", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s", "fan_speed", "n_air_exponent"],
|
||
EvaporatorWater: ["ua", "t_sat_evap_c", "superheat_k", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"],
|
||
EvaporatorAir: ["ua", "t_sat_evap_c", "superheat_k", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"]
|
||
};
|
||
|
||
let state = { mode: "move", nodes: [], edges: [], selectedId: null, connectFrom: null, dragNode: null, dragOffset: null, addType: null, mouseX: null, mouseY: null };
|
||
let nodeIdSeq = 0;
|
||
const NODE_W = 100, NODE_H = 52;
|
||
|
||
function getNode(id) { return state.nodes.find(n => n.id === id); }
|
||
function getNodeByName(name) { return state.nodes.find(n => n.name === name); }
|
||
|
||
function addNode(type, x, y) {
|
||
const name = type.toLowerCase().slice(0, 3) + "_" + (++nodeIdSeq);
|
||
const params = { ...COMPONENT_DEFAULTS[type] };
|
||
state.nodes.push({ id: "n" + nodeIdSeq, type, name, x, y, params });
|
||
render();
|
||
}
|
||
|
||
function deleteNode(id) {
|
||
state.nodes = state.nodes.filter(n => n.id !== id);
|
||
state.edges = state.edges.filter(e => e.from !== id && e.to !== id);
|
||
if (state.selectedId === id) state.selectedId = null;
|
||
render();
|
||
showProps();
|
||
}
|
||
|
||
function addEdge(fromId, toId) {
|
||
const from = getNode(fromId), to = getNode(toId);
|
||
if (!from || !to || fromId === toId) return;
|
||
if (state.edges.some(e => e.from === fromId && e.to === toId)) return;
|
||
state.edges.push({ from: fromId, to: toId });
|
||
state.connectFrom = null;
|
||
setModeHint();
|
||
render();
|
||
}
|
||
|
||
function buildConfig() {
|
||
const fluid = document.getElementById("fluid").value.trim() || "R410A";
|
||
const name = document.getElementById("projectName").value.trim() || "Machine";
|
||
const circuit = {
|
||
id: 0,
|
||
components: state.nodes.map(n => {
|
||
if (EXPORT_AS_PLACEHOLDER.includes(n.type)) {
|
||
return { type: "Placeholder", name: n.name, ...n.params };
|
||
}
|
||
const p = n.params;
|
||
if (n.type === "CondenserWater") {
|
||
return {
|
||
type: "HeatExchanger",
|
||
name: n.name,
|
||
ua: p.ua,
|
||
hot_fluid: fluid,
|
||
hot_t_inlet_c: (p.t_sat_cond_c != null ? p.t_sat_cond_c : 45) + 5,
|
||
hot_pressure_bar: p.ref_pressure_bar,
|
||
hot_mass_flow_kg_s: p.ref_mass_flow_kg_s,
|
||
cold_fluid: "Water",
|
||
cold_t_inlet_c: p.sec_t_inlet_c,
|
||
cold_pressure_bar: p.sec_pressure_bar,
|
||
cold_mass_flow_kg_s: p.sec_mass_flow_kg_s
|
||
};
|
||
}
|
||
if (n.type === "CondenserAir") {
|
||
return {
|
||
type: "MchxCondenserCoil",
|
||
name: n.name,
|
||
ua_nominal_kw_k: (p.ua != null ? p.ua : 5000) / 1000,
|
||
n_air_exponent: p.n_air_exponent != null ? p.n_air_exponent : 0.5,
|
||
air_inlet_temp_c: p.sec_t_inlet_c != null ? p.sec_t_inlet_c : 35,
|
||
fan_speed: p.fan_speed != null ? p.fan_speed : 1,
|
||
coil_index: 0
|
||
};
|
||
}
|
||
if (n.type === "EvaporatorWater") {
|
||
return {
|
||
type: "HeatExchanger",
|
||
name: n.name,
|
||
ua: p.ua,
|
||
hot_fluid: "Water",
|
||
hot_t_inlet_c: p.sec_t_inlet_c,
|
||
hot_pressure_bar: p.sec_pressure_bar,
|
||
hot_mass_flow_kg_s: p.sec_mass_flow_kg_s,
|
||
cold_fluid: fluid,
|
||
cold_t_inlet_c: p.t_sat_evap_c != null ? p.t_sat_evap_c : 5,
|
||
cold_pressure_bar: p.ref_pressure_bar,
|
||
cold_mass_flow_kg_s: p.ref_mass_flow_kg_s
|
||
};
|
||
}
|
||
if (n.type === "EvaporatorAir") {
|
||
return {
|
||
type: "HeatExchanger",
|
||
name: n.name,
|
||
ua: p.ua,
|
||
hot_fluid: "Air",
|
||
hot_t_inlet_c: p.sec_t_inlet_c,
|
||
hot_pressure_bar: p.sec_pressure_bar,
|
||
hot_mass_flow_kg_s: p.sec_mass_flow_kg_s,
|
||
cold_fluid: fluid,
|
||
cold_t_inlet_c: p.t_sat_evap_c != null ? p.t_sat_evap_c : 5,
|
||
cold_pressure_bar: p.ref_pressure_bar,
|
||
cold_mass_flow_kg_s: p.ref_mass_flow_kg_s
|
||
};
|
||
}
|
||
return { type: n.type, name: n.name, ...n.params };
|
||
}),
|
||
edges: state.edges.map(e => {
|
||
const a = getNode(e.from), b = getNode(e.to);
|
||
return a && b ? { from: a.name + ":outlet", to: b.name + ":inlet" } : null;
|
||
}).filter(Boolean)
|
||
};
|
||
return { name, fluid, circuits: [circuit], solver: { strategy: "fallback", max_iterations: 100, tolerance: 1e-6 } };
|
||
}
|
||
|
||
function render() {
|
||
const svg = document.getElementById("canvas");
|
||
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||
defs.innerHTML = '<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#58a6ff"/></marker>';
|
||
svg.innerHTML = "";
|
||
svg.appendChild(defs);
|
||
|
||
const gEdges = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||
state.edges.forEach(e => {
|
||
const a = getNode(e.from), b = getNode(e.to);
|
||
if (!a || !b) return;
|
||
const x1 = a.x + NODE_W; const y1 = a.y + NODE_H / 2;
|
||
const x2 = b.x; const y2 = b.y + NODE_H / 2;
|
||
const mid = (x1 + x2) / 2;
|
||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||
path.setAttribute("d", `M ${x1} ${y1} C ${mid} ${y1} ${mid} ${y2} ${x2} ${y2}`);
|
||
path.setAttribute("fill", "none");
|
||
path.setAttribute("stroke", "#58a6ff");
|
||
path.setAttribute("stroke-width", "2.5");
|
||
path.setAttribute("marker-end", "url(#arrow)");
|
||
gEdges.appendChild(path);
|
||
});
|
||
if (state.connectFrom && state.mouseX != null && state.mouseY != null) {
|
||
const a = getNode(state.connectFrom);
|
||
if (a) {
|
||
const x1 = a.x + NODE_W; const y1 = a.y + NODE_H / 2;
|
||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||
const mid = (x1 + state.mouseX) / 2;
|
||
path.setAttribute("d", `M ${x1} ${y1} C ${mid} ${y1} ${mid} ${state.mouseY} ${state.mouseX} ${state.mouseY}`);
|
||
path.setAttribute("fill", "none");
|
||
path.setAttribute("stroke", "#58a6ff");
|
||
path.setAttribute("stroke-width", "2");
|
||
path.setAttribute("stroke-dasharray", "6 4");
|
||
path.setAttribute("marker-end", "url(#arrow)");
|
||
gEdges.appendChild(path);
|
||
}
|
||
}
|
||
svg.appendChild(gEdges);
|
||
|
||
const gNodes = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||
state.nodes.forEach(n => {
|
||
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||
g.setAttribute("data-id", n.id);
|
||
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||
rect.setAttribute("x", n.x);
|
||
rect.setAttribute("y", n.y);
|
||
rect.setAttribute("width", NODE_W);
|
||
rect.setAttribute("height", NODE_H);
|
||
rect.setAttribute("rx", "8");
|
||
rect.setAttribute("fill", state.selectedId === n.id ? "#388bfd44" : "#21262d");
|
||
rect.setAttribute("stroke", state.connectFrom === n.id ? "#58a6ff" : "#30363d");
|
||
rect.setAttribute("stroke-width", state.selectedId === n.id || state.connectFrom === n.id ? "2" : "1");
|
||
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||
label.setAttribute("x", n.x + NODE_W / 2);
|
||
label.setAttribute("y", n.y + 20);
|
||
label.setAttribute("text-anchor", "middle");
|
||
label.setAttribute("fill", "#e6edf3");
|
||
label.setAttribute("font-size", "12");
|
||
label.setAttribute("pointer-events", "none");
|
||
label.textContent = NODE_LABEL[n.type] || n.type.slice(0, 2);
|
||
const nameText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||
nameText.setAttribute("x", n.x + NODE_W / 2);
|
||
nameText.setAttribute("y", n.y + 38);
|
||
nameText.setAttribute("text-anchor", "middle");
|
||
nameText.setAttribute("fill", "#8b949e");
|
||
nameText.setAttribute("font-size", "10");
|
||
nameText.setAttribute("pointer-events", "none");
|
||
nameText.textContent = n.name;
|
||
g.appendChild(rect);
|
||
g.appendChild(label);
|
||
g.appendChild(nameText);
|
||
const hitRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||
hitRect.setAttribute("x", n.x);
|
||
hitRect.setAttribute("y", n.y);
|
||
hitRect.setAttribute("width", NODE_W);
|
||
hitRect.setAttribute("height", NODE_H);
|
||
hitRect.setAttribute("fill", "transparent");
|
||
hitRect.setAttribute("cursor", state.mode === "connect" ? "pointer" : state.mode === "move" ? "grab" : "default");
|
||
hitRect.setAttribute("data-id", n.id);
|
||
g.appendChild(hitRect);
|
||
gNodes.appendChild(g);
|
||
});
|
||
svg.appendChild(gNodes);
|
||
if (state.mode === "connect") setModeHint();
|
||
}
|
||
|
||
function showProps() {
|
||
const empty = document.getElementById("propsEmpty");
|
||
const form = document.getElementById("propsForm");
|
||
if (state.selectedId) {
|
||
const n = getNode(state.selectedId);
|
||
if (n) {
|
||
empty.style.display = "none";
|
||
form.style.display = "block";
|
||
document.getElementById("propName").value = n.name;
|
||
document.getElementById("propType").value = n.type;
|
||
const container = document.getElementById("propParams");
|
||
container.innerHTML = "";
|
||
const order = PARAM_ORDER[n.type];
|
||
const keys = order ? order.filter(k => n.params.hasOwnProperty(k)).concat(Object.keys(n.params).filter(k => !order.includes(k))) : Object.keys(n.params);
|
||
let didRefHeader = false;
|
||
keys.forEach((k) => {
|
||
if (PARAM_ORDER[n.type]) {
|
||
if (!didRefHeader && (k === "ua" || k === "t_sat_cond_c" || k === "t_sat_evap_c")) {
|
||
didRefHeader = true;
|
||
const refHeader = document.createElement("div");
|
||
refHeader.className = "prop-section";
|
||
refHeader.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin-bottom:0.2rem;";
|
||
refHeader.textContent = "Côté réfrigérant";
|
||
container.appendChild(refHeader);
|
||
}
|
||
if (k === "sec_t_inlet_c") {
|
||
const sep = document.createElement("div");
|
||
sep.className = "prop-section";
|
||
sep.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin:0.4rem 0 0.2rem;padding-top:0.3rem;border-top:1px solid var(--border);";
|
||
sep.textContent = "Côté secondaire (eau / air)";
|
||
container.appendChild(sep);
|
||
}
|
||
if (n.type === "CondenserAir" && k === "fan_speed") {
|
||
const fanSep = document.createElement("div");
|
||
fanSep.className = "prop-section";
|
||
fanSep.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin:0.4rem 0 0.2rem;padding-top:0.3rem;border-top:1px solid var(--border);";
|
||
fanSep.textContent = "Ventilateur (export MchxCondenserCoil)";
|
||
container.appendChild(fanSep);
|
||
}
|
||
}
|
||
const meta = PARAM_META[k] || { type: "number", label: k };
|
||
const label = document.createElement("label");
|
||
label.textContent = meta.label;
|
||
const input = document.createElement("input");
|
||
input.type = meta.type === "number" ? "number" : "text";
|
||
input.dataset.key = k;
|
||
input.value = n.params[k];
|
||
input.addEventListener("input", () => { n.params[k] = meta.type === "number" ? parseFloat(input.value) || 0 : input.value; });
|
||
container.appendChild(label);
|
||
container.appendChild(input);
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
empty.style.display = "block";
|
||
form.style.display = "none";
|
||
}
|
||
|
||
function pt(svg, e) {
|
||
const p = svg.createSVGPoint();
|
||
p.x = e.clientX;
|
||
p.y = e.clientY;
|
||
const ctm = svg.getScreenCTM();
|
||
if (!ctm) return { x: 0, y: 0 };
|
||
const tp = p.matrixTransform(ctm.inverse());
|
||
return { x: tp.x, y: tp.y };
|
||
}
|
||
|
||
function nodeAtPoint(x, y) {
|
||
for (let i = state.nodes.length - 1; i >= 0; i--) {
|
||
const n = state.nodes[i];
|
||
if (x >= n.x && x <= n.x + NODE_W && y >= n.y && y <= n.y + NODE_H) return n.id;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const canvas = document.getElementById("canvas");
|
||
const wrap = document.getElementById("canvasWrap");
|
||
|
||
canvas.addEventListener("mousedown", e => {
|
||
e.preventDefault();
|
||
const p = pt(canvas, e);
|
||
const id = nodeAtPoint(p.x, p.y);
|
||
|
||
if (state.mode === "connect") {
|
||
if (id) {
|
||
if (state.connectFrom) {
|
||
if (state.connectFrom === id) {
|
||
state.connectFrom = null;
|
||
} else {
|
||
addEdge(state.connectFrom, id);
|
||
}
|
||
} else {
|
||
state.connectFrom = id;
|
||
}
|
||
} else {
|
||
state.connectFrom = null;
|
||
}
|
||
setModeHint();
|
||
render();
|
||
return;
|
||
}
|
||
|
||
if (state.mode === "add" && state.addType) {
|
||
if (!id) addNode(state.addType, Math.max(0, p.x - NODE_W / 2), Math.max(0, p.y - NODE_H / 2));
|
||
return;
|
||
}
|
||
|
||
if (id) {
|
||
state.selectedId = id;
|
||
state.dragNode = id;
|
||
const n = getNode(id);
|
||
state.dragOffset = { x: p.x - n.x, y: p.y - n.y };
|
||
showProps();
|
||
render();
|
||
} else {
|
||
state.selectedId = null;
|
||
showProps();
|
||
render();
|
||
}
|
||
});
|
||
|
||
function onMouseMove(e) {
|
||
const p = pt(canvas, e);
|
||
state.mouseX = p.x;
|
||
state.mouseY = p.y;
|
||
if (state.connectFrom) {
|
||
render();
|
||
return;
|
||
}
|
||
if (!state.dragNode) return;
|
||
const n = getNode(state.dragNode);
|
||
n.x = Math.max(0, p.x - state.dragOffset.x);
|
||
n.y = Math.max(0, p.y - state.dragOffset.y);
|
||
render();
|
||
}
|
||
canvas.addEventListener("mousemove", onMouseMove);
|
||
window.addEventListener("mousemove", onMouseMove);
|
||
|
||
canvas.addEventListener("mouseup", () => { state.dragNode = null; });
|
||
window.addEventListener("mouseup", () => { state.dragNode = null; });
|
||
|
||
canvas.addEventListener("dragover", e => { e.preventDefault(); });
|
||
canvas.addEventListener("drop", e => {
|
||
e.preventDefault();
|
||
const type = e.dataTransfer.getData("type");
|
||
if (!type) return;
|
||
const p = pt(canvas, e);
|
||
addNode(type, Math.max(0, p.x - NODE_W / 2), Math.max(0, p.y - NODE_H / 2));
|
||
});
|
||
|
||
document.querySelectorAll(".palette-item").forEach(el => {
|
||
el.addEventListener("click", () => {
|
||
state.addType = el.dataset.type;
|
||
document.querySelectorAll(".palette-item").forEach(x => x.style.borderColor = "transparent");
|
||
el.style.borderColor = "var(--accent)";
|
||
});
|
||
el.addEventListener("dragstart", e => { e.dataTransfer.setData("type", el.dataset.type); });
|
||
});
|
||
|
||
function setModeHint() {
|
||
const h = document.getElementById("modeHint");
|
||
const s = document.getElementById("connectStatus");
|
||
if (state.mode === "move") {
|
||
h.textContent = "Déplacer : glissez les blocs.";
|
||
s.textContent = "";
|
||
} else if (state.mode === "add") {
|
||
h.textContent = "Ajouter : choisissez un composant ci-dessus, puis cliquez sur le schéma.";
|
||
s.textContent = "";
|
||
} else {
|
||
h.textContent = "Relier : cliquez sur un bloc (sortie), puis sur un autre (entrée).";
|
||
s.textContent = state.connectFrom ? "→ Maintenant cliquez sur le bloc cible (entrée)." : "Cliquez sur le 1er bloc.";
|
||
}
|
||
}
|
||
document.getElementById("modeMove").onclick = () => { state.mode = "move"; state.connectFrom = null; wrap.className = "canvas-wrap"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeMove").classList.add("active"); setModeHint(); render(); };
|
||
document.getElementById("modeAdd").onclick = () => { state.mode = "add"; state.connectFrom = null; wrap.className = "canvas-wrap mode-add"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeAdd").classList.add("active"); setModeHint(); };
|
||
document.getElementById("modeConnect").onclick = () => { state.mode = "connect"; wrap.className = "canvas-wrap mode-connect"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeConnect").classList.add("active"); state.connectFrom = null; setModeHint(); render(); };
|
||
|
||
document.getElementById("propName").addEventListener("input", e => {
|
||
const n = getNode(state.selectedId);
|
||
if (n) n.name = e.target.value.trim() || n.name;
|
||
render();
|
||
});
|
||
document.getElementById("btnDeleteNode").onclick = () => { if (state.selectedId) deleteNode(state.selectedId); };
|
||
|
||
document.getElementById("exampleSelect").addEventListener("change", async function() {
|
||
const path = this.value;
|
||
if (!path) return;
|
||
const res = await fetch(path);
|
||
const config = await res.json();
|
||
state.nodes = [];
|
||
state.edges = [];
|
||
state.selectedId = null;
|
||
nodeIdSeq = 0;
|
||
const c = config.circuits[0];
|
||
if (!c) return;
|
||
const nameToId = {};
|
||
(c.components || []).forEach((comp, i) => {
|
||
const id = "n" + (++nodeIdSeq);
|
||
nameToId[comp.name] = id;
|
||
const params = { ...comp };
|
||
delete params.type;
|
||
delete params.name;
|
||
state.nodes.push({
|
||
id,
|
||
type: comp.type,
|
||
name: comp.name,
|
||
x: 150 + (i % 4) * 220,
|
||
y: 80 + Math.floor(i / 4) * 120,
|
||
params
|
||
});
|
||
});
|
||
(c.edges || []).forEach(edge => {
|
||
const fromName = (edge.from || "").split(":")[0];
|
||
const toName = (edge.to || "").split(":")[0];
|
||
const fromId = nameToId[fromName];
|
||
const toId = nameToId[toName];
|
||
if (fromId && toId) state.edges.push({ from: fromId, to: toId });
|
||
});
|
||
document.getElementById("projectName").value = config.name || "";
|
||
document.getElementById("fluid").value = config.fluid || "R410A";
|
||
this.value = "";
|
||
render();
|
||
showProps();
|
||
});
|
||
|
||
async function postCli(endpoint) {
|
||
const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildConfig()) });
|
||
const data = await res.json();
|
||
const box = document.getElementById("resultBox");
|
||
box.style.display = "block";
|
||
box.className = "result " + (data.ok ? "ok" : "err");
|
||
box.textContent = (data.stdout || "") + (data.stderr ? "\n" + data.stderr : "");
|
||
}
|
||
|
||
document.getElementById("btnValidate").onclick = () => postCli("/validate");
|
||
document.getElementById("btnRun").onclick = () => postCli("/run");
|
||
document.getElementById("btnExport").onclick = () => {
|
||
const blob = new Blob([JSON.stringify(buildConfig(), null, 2)], { type: "application/json" });
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = "entropyk-config.json";
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
};
|
||
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|