Files
Sepehr ab5dc7e568 chore: remove BMAD framework files and IDE configuration artifacts
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>
2026-04-25 15:01:09 +02:00

641 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 01). Circuit air = Source air → … → Puits air (export Placeholder tant que le CLI na 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 &amp; 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 (01)" },
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>