Compare commits

...

7 commits

Author SHA1 Message Date
77fa30d75f Fix non existing route
All checks were successful
Build & Deploy / Validation, build et push (push) Successful in 7m22s
2026-07-07 18:10:11 +02:00
a746069e4f Fix invalid php extension in php8.5
All checks were successful
Build & Deploy / Validation, build et push (push) Successful in 10m53s
2026-07-07 16:22:20 +02:00
029c93e866 Fix invalid composer requierement
Some checks failed
Build & Deploy / Validation, build et push (push) Failing after 4m22s
2026-07-07 15:48:13 +02:00
01ecc29800 Fix composer.lock missing
Some checks failed
Build & Deploy / Validation, build et push (push) Failing after 41s
2026-07-07 15:14:00 +02:00
10976f6145 Add Create mod calculator
Some checks failed
Build & Deploy / Validation, build et push (push) Failing after 5m8s
2026-07-07 14:49:38 +02:00
c38193dc7f fix available dependances field issue and change constraintes
All checks were successful
Build & Deploy / Validation, build et push (push) Successful in 8m16s
2026-06-14 10:40:27 +02:00
ee08bcecf0 fix dependances bug
All checks were successful
Build & Deploy / Validation, build et push (push) Successful in 10m12s
2026-06-12 23:15:07 +02:00
14 changed files with 8426 additions and 6983 deletions

10
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1,443 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
#canvas;
#tip;
#resizeHandler;
connect() {
const root = this.element;
const $ = id => root.querySelector("#" + id);
// debug
try{ console.debug && console.debug('calculateur: connect', root); }catch(e){}
// Store references for cleanup
this.#canvas = $("aero-chart");
this.#tip = $("aero-tip");
const C = {};
const readConsts = () => {
try {
const ct = $("aero-ct"); if(ct) C.CT=parseFloat(ct.value);
const sair = $("aero-sair"); if(sair) C.sAir=parseFloat(sair.value);
const g = $("aero-g"); if(g) C.g=parseFloat(g.value);
const sea = $("aero-sea"); if(sea) C.sea=parseFloat(sea.value);
const k = $("aero-k"); if(k) C.k=parseFloat(k.value);
const pmax = $("aero-pmax"); if(pmax) C.pMax=parseFloat(pmax.value);
const ytop = $("aero-ytop"); if(ytop) C.yTop=parseFloat(ytop.value);
const ybot = $("aero-ybot"); if(ybot) C.yBot=parseFloat(ybot.value);
} catch(err) {
console.error('calculateur:readConsts error:', err);
}
};
const pressure = (y) => {
let p = Math.exp(C.k * (y - C.sea));
if (p > C.pMax) p = C.pMax;
return p < 0 ? 0 : p;
};
const Y_OFFSET = 0.5;
const propThrust = (P, N) => C.CT * Math.pow(Math.max(P, 0), 1.5) * N;
const balloonForce = (V, y) => Math.max(V, 0) * C.sAir * C.g * pressure(y);
const balloonLift = (V, y) => Math.max(V, 0) * C.sAir * pressure(y);
const maxAltitude = (V, m) => {
if (V <= 0 || m <= 0) return { y: C.yTop, status: "ceiling" };
const t = m / (V * C.sAir);
if (t > C.pMax) return { y: C.yBot, status: "grounded" };
let y = C.sea + Math.log(t) / C.k;
if (y >= C.yTop) return { y: C.yTop, status: "ceiling" };
if (y <= C.yBot) return { y: C.yBot, status: "bottom" };
return { y, status: "ok" };
};
const requiredVolume = (m, y) => m / (C.sAir * pressure(y));
const nf = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0 });
const nf2 = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 2 });
const nf3 = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 3 });
const big = (x) => {
if (!isFinite(x)) return "n/a";
return Math.abs(x) >= 1000 ? nf.format(x) : nf2.format(x);
};
const updateProp = () => {
try {
const sails = $("aero-sails");
const rpm = $("aero-rpm");
const palt = $("aero-palt");
const P = sails ? parseFloat(sails.value) || 0 : 0;
const N = rpm ? parseFloat(rpm.value) || 0 : 0;
const y = palt ? parseFloat(palt.value) || 0 : 0;
const T = propThrust(P, N);
const thrust = $("aero-othrust");
const thrustapp = $("aero-othrustapp");
if (thrust) thrust.innerHTML = big(Math.abs(T)) + ' <small>pN</small>';
if (thrustapp) thrustapp.innerHTML = big(Math.abs(T) * pressure(y + Y_OFFSET)) + ' <small>pN</small>';
try { console.debug && console.debug('calculateur:updateProp', { P, N, y, T }); } catch (e) {}
} catch(err) {
console.error('calculateur:updateProp error:', err);
}
};
const updateBalloon = () => {
try {
const vol = $("aero-vol");
const balt = $("aero-balt");
const V = vol ? parseFloat(vol.value) || 0 : 0;
const y = balt ? parseFloat(balt.value) || 0 : 0;
const force = $("aero-oforce");
const lift = $("aero-olift");
const press = $("aero-opress");
if (force) force.innerHTML = big(balloonForce(V, y)) + ' <small>pN</small>';
if (lift) lift.innerHTML = big(balloonLift(V, y)) + ' <small>kpg</small>';
if (press) press.textContent = nf3.format(pressure(y));
try { console.debug && console.debug('calculateur:updateBalloon', { V, y }); } catch (e) {}
} catch(err) {
console.error('calculateur:updateBalloon error:', err);
}
};
const updateVolume = () => {
try {
const vmass = $("aero-vmass");
const valt = $("aero-valt");
const m = vmass ? parseFloat(vmass.value) || 0 : 0;
const y = valt ? parseFloat(valt.value) || 0 : 0;
const V = requiredVolume(m, y);
const volreq = $("aero-ovolreq");
const cube = $("aero-ocube");
const warn = $("aero-vwarn");
if (volreq) volreq.innerHTML = big(V) + ' <small>m³</small>';
if (cube) cube.textContent = (isFinite(V) && V > 0)
? "≈ cube de " + nf2.format(Math.cbrt(V)) + " blocs de côté · " + nf.format(Math.ceil(V)) + " blocs d'air"
: "";
if (warn) warn.innerHTML = (pressure(y) <= 0)
? "<b>Altitude hors atmosphère :</b> pression nulle, aucun volume fini ne suffit." : "";
try { console.debug && console.debug('calculateur:updateVolume', { m, y, V }); } catch (e) {}
} catch(err) {
console.error('calculateur:updateVolume error:', err);
}
};
// ---- graphe ----
let mode = "mass";
// prefer document-level IDs for canvas/tip (unique across page)
let canvas = null;
let tip = null;
let ctx = null;
let geom = null;
try {
canvas = document.getElementById("aero-chart");
tip = document.getElementById("aero-tip");
if (!canvas || !tip) {
throw new Error('canvas or tip element not found');
}
this.#canvas = canvas;
this.#tip = tip;
ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error('2d context not available');
}
} catch (err) {
console.error('calculateur: graph init failed:', err.message);
try{ root.dataset.calculateurConnected = '1'; }catch(e){}
// mark as connected but disable graph features
canvas = null;
tip = null;
ctx = null;
}
const tv = (n) => getComputedStyle(root).getPropertyValue(n).trim() || "#888";
const niceStep = (raw) => {
const p = Math.pow(10, Math.floor(Math.log10(raw)));
const n = raw / p;
return (n < 1.5 ? 1 : n < 3 ? 2 : n < 7 ? 5 : 10) * p;
};
const abbr = (v) => {
const a = Math.abs(v);
if (a >= 1e6) return nf2.format(v / 1e6) + "M";
if (a >= 1e3) return nf2.format(v / 1e3) + "k";
return nf.format(v);
};
const drawChart = () => {
// skip if graph not available
if (!canvas || !ctx) return;
const dpr = window.devicePixelRatio || 1;
const cssW = canvas.parentElement.clientWidth;
const cssH = Math.max(260, Math.round(cssW * 0.44));
canvas.style.height = cssH + "px";
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssW, cssH);
const padL = 54, padR = 16, padT = 14, padB = 38, pw = cssW - padL - padR, ph = cssH - padT - padB;
const muted = tv("--aero-muted"), border = tv("--aero-border"), accent = tv("--aero-accent"), warn = tv("--aero-warn");
let xMin, xMax, xLabel, fixed;
if (mode === "mass") {
const fixvol = $("aero-fixvol");
fixed = Math.max(1, fixvol ? parseFloat(fixvol.value) || 1 : 1);
xMin = 1;
xMax = Math.max(2, fixed * C.sAir * C.pMax * 1.15);
xLabel = "poids m (kpg)";
} else {
const fixmass = $("aero-fixmass");
fixed = Math.max(1, fixmass ? parseFloat(fixmass.value) || 1 : 1);
xMin = Math.max(1, fixed / (C.sAir * C.pMax) * 0.5);
xMax = Math.max(xMin * 1.5, 4 * fixed / C.sAir);
xLabel = "volume V (m³)";
}
const yMin = C.yBot, yMax = C.yTop;
const X = x => padL + (x - xMin) / (xMax - xMin) * pw;
const Y = y => padT + (1 - (y - yMin) / (yMax - yMin)) * ph;
ctx.font = '11px ' + (getComputedStyle(root).fontFamily || "sans-serif");
// grille
ctx.strokeStyle = border;
ctx.fillStyle = muted;
ctx.lineWidth = 1;
ctx.textBaseline = "middle";
ctx.textAlign = "right";
const yStep = niceStep((yMax - yMin) / 6);
for (let yy = Math.ceil(yMin / yStep) * yStep; yy <= yMax; yy += yStep) {
const py = Y(yy);
ctx.beginPath();
ctx.moveTo(padL, py);
ctx.lineTo(padL + pw, py);
ctx.stroke();
ctx.fillText(String(yy), padL - 7, py);
}
ctx.textAlign = "center";
ctx.textBaseline = "top";
const xStep = niceStep((xMax - xMin) / 6);
for (let xx = Math.ceil(xMin / xStep) * xStep; xx <= xMax; xx += xStep) {
const px = X(xx);
ctx.beginPath();
ctx.moveTo(px, padT);
ctx.lineTo(px, padT + ph);
ctx.stroke();
ctx.fillText(abbr(xx), px, padT + ph + 7);
}
ctx.fillStyle = muted;
ctx.fillText(xLabel, padL + pw / 2, padT + ph + 22);
ctx.save();
ctx.translate(13, padT + ph / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText("altitude max y", 0, 0);
ctx.restore();
const refLine = (yv, label) => {
if (yv < yMin || yv > yMax) return;
const py = Y(yv);
ctx.strokeStyle = muted;
ctx.setLineDash([4, 4]);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padL, py);
ctx.lineTo(padL + pw, py);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = muted;
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText(label, padL + 5, py - 2);
};
refLine(C.sea, "niveau mer " + C.sea);
refLine(C.yTop, "limite " + C.yTop);
// courbe
const steps = 240, pts = [];
for (let i = 0; i <= steps; i++) {
const x = xMin + (xMax - xMin) * i / steps;
const r = (mode === "mass") ? maxAltitude(fixed, x) : maxAltitude(x, fixed);
pts.push({ x, y: r.y, status: r.status });
}
// zone grounded
ctx.fillStyle = warn;
ctx.globalAlpha = .10;
let gs = null;
for (let i = 0; i <= steps; i++) {
const g = pts[i].status === "grounded";
if (g && gs === null) gs = pts[i].x;
if ((!g || i === steps) && gs !== null) {
ctx.fillRect(X(gs), padT, X(pts[i].x) - X(gs), ph);
gs = null;
}
}
ctx.globalAlpha = 1;
ctx.lineWidth = 2;
ctx.strokeStyle = accent;
ctx.beginPath();
let started = false;
for (const p of pts) {
if (p.status === "grounded") {
started = false;
continue;
}
const px = X(p.x), py = Y(p.y);
if (!started) {
ctx.moveTo(px, py);
started = true;
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
geom = { padL, pw, padT, ph, xMin, xMax, X, Y, fixed };
};
// Event listeners (only if canvas available)
if (canvas && tip) {
canvas.addEventListener("mousemove", e => {
if (!geom) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
if (mx < geom.padL || mx > geom.padL + geom.pw) {
tip.style.opacity = 0;
return;
}
const x = geom.xMin + (mx - geom.padL) / geom.pw * (geom.xMax - geom.xMin);
const r = (mode === "mass") ? maxAltitude(geom.fixed, x) : maxAltitude(x, geom.fixed);
tip.textContent = r.status === "grounded" ? abbr(x) + " : ne décolle pas"
: r.status === "ceiling" ? abbr(x) + " : plafond (y " + Math.round(r.y) + ")"
: abbr(x) + " : y " + Math.round(r.y);
tip.style.left = geom.X(x) + "px";
tip.style.top = geom.Y(r.y) + "px";
tip.style.opacity = 1;
});
canvas.addEventListener("mouseleave", () => {
tip.style.opacity = 0;
});
} else {
if (!canvas) console.warn('calculateur: canvas not available, graph interactions disabled');
if (!tip) console.warn('calculateur: tip not available, graph tooltip disabled');
}
// Affiche uniquement l'input correspondant au mode courant
const applyModeVisibility = () => {
const fixvolwrap = $("aero-fixvolwrap");
const fixmasswrap = $("aero-fixmasswrap");
if (fixvolwrap) fixvolwrap.style.display = mode === "mass" ? "" : "none";
if (fixmasswrap) fixmasswrap.style.display = mode === "vol" ? "" : "none";
};
// ---- Partage par URL (paramètres GET, tous optionnels) ----
// Clé = id de l'input sans le préfixe "aero-". On ne sérialise que les
// valeurs qui diffèrent de leur valeur par défaut → URLs courtes.
const paramInputs = Array.from(root.querySelectorAll("input, select"))
.filter(el => el.id && el.id.startsWith("aero-"));
const keyOf = el => el.id.slice(5);
const defaults = new Map(paramInputs.map(el => [el, el.value]));
// Applique les paramètres présents dans l'URL aux inputs (au chargement)
const applyUrlParams = () => {
const params = new URLSearchParams(window.location.search);
paramInputs.forEach(el => {
const k = keyOf(el);
if (params.has(k)) el.value = params.get(k);
});
};
// Reflète l'état courant des inputs dans l'URL (sans polluer l'historique)
const syncUrl = () => {
try {
const params = new URLSearchParams();
paramInputs.forEach(el => {
if (el.value !== defaults.get(el)) params.set(keyOf(el), el.value);
});
const qs = params.toString();
const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
window.history.replaceState(null, "", url);
} catch(err) {
console.error('calculateur:syncUrl error:', err);
}
};
// Store event handlers for cleanup
const handleModeChange = e => {
try {
mode = e.target.value;
applyModeVisibility();
drawChart();
syncUrl();
} catch(err) {
console.error('calculateur:handleModeChange error:', err);
}
};
const handleInput = () => {
try {
readConsts();
updateProp();
updateBalloon();
updateVolume();
drawChart();
syncUrl();
} catch(err) {
console.error('calculateur:handleInput error:', err);
}
};
this.#resizeHandler = () => {
if (geom) drawChart();
};
const modeEl = $("aero-mode");
if (modeEl) modeEl.addEventListener("change", handleModeChange);
root.querySelectorAll("input,select").forEach(el => el.addEventListener("input", handleInput));
window.addEventListener("resize", this.#resizeHandler);
// Store handlers for cleanup
this.handleModeChange = handleModeChange;
this.handleInput = handleInput;
this.root = root;
this.$ = $;
try {
// Charge l'état partagé depuis l'URL, puis synchronise le mode
applyUrlParams();
if (modeEl) mode = modeEl.value;
applyModeVisibility();
readConsts();
updateProp();
updateBalloon();
updateVolume();
drawChart();
} catch(err) {
console.error('calculateur: error during initialization:', err);
}
try{ root.dataset.calculateurConnected = '1'; }catch(e){}
}
disconnect() {
// Clean up event listeners
if (this.root && this.$) {
const $ = this.$;
if (this.handleModeChange) {
const modeEl = $("aero-mode");
if (modeEl) modeEl.removeEventListener("change", this.handleModeChange);
}
if (this.handleInput) {
this.root.querySelectorAll("input,select").forEach(el =>
el.removeEventListener("input", this.handleInput)
);
}
}
if (this.#resizeHandler) {
window.removeEventListener("resize", this.#resizeHandler);
}
}
}

View file

@ -0,0 +1,233 @@
"use strict";
(function(){
const root = document.querySelector(".aero-calc");
if (!root) {
console.error('calculateur legacy: .aero-calc container not found');
return;
}
const $ = id => {
const el = root.querySelector("#"+id);
// silencieusement retourner null si l'élément n'existe pas
return el;
};
const C = {};
function readConsts(){
try {
const ct = $("aero-ct"); if(ct) C.CT=parseFloat(ct.value); else console.warn('aero-ct not found');
const sair = $("aero-sair"); if(sair) C.sAir=parseFloat(sair.value); else console.warn('aero-sair not found');
const g = $("aero-g"); if(g) C.g=parseFloat(g.value); else console.warn('aero-g not found');
const sea = $("aero-sea"); if(sea) C.sea=parseFloat(sea.value); else console.warn('aero-sea not found');
const k = $("aero-k"); if(k) C.k=parseFloat(k.value); else console.warn('aero-k not found');
const pmax = $("aero-pmax"); if(pmax) C.pMax=parseFloat(pmax.value); else console.warn('aero-pmax not found');
const ytop = $("aero-ytop"); if(ytop) C.yTop=parseFloat(ytop.value); else console.warn('aero-ytop not found');
const ybot = $("aero-ybot"); if(ybot) C.yBot=parseFloat(ybot.value); else console.warn('aero-ybot not found');
} catch(e) { console.error('readConsts error:', e); }
}
function pressure(y){ let p=Math.exp(C.k*(y-C.sea)); if(p>C.pMax)p=C.pMax; return p<0?0:p; }
// Le mod echantillonne la pression au CENTRE du bloc : getBlockPos().getCenter() -> y+0.5
const Y_OFFSET = 0.5;
const propThrust = (P,N)=>C.CT*Math.pow(Math.max(P,0),1.5)*N;
const balloonForce = (V,y)=>Math.max(V,0)*C.sAir*C.g*pressure(y);
const balloonLift = (V,y)=>Math.max(V,0)*C.sAir*pressure(y);
function maxAltitude(V,m){
if(V<=0||m<=0) return {y:C.yTop,status:"ceiling"};
const t=m/(V*C.sAir);
if(t>C.pMax) return {y:C.yBot,status:"grounded"};
let y=C.sea+Math.log(t)/C.k;
if(y>=C.yTop) return {y:C.yTop,status:"ceiling"};
if(y<=C.yBot) return {y:C.yBot,status:"bottom"};
return {y,status:"ok"};
}
const requiredVolume=(m,y)=>m/(C.sAir*pressure(y));
const nf=new Intl.NumberFormat("fr-FR",{maximumFractionDigits:0});
const nf2=new Intl.NumberFormat("fr-FR",{maximumFractionDigits:2});
const nf3=new Intl.NumberFormat("fr-FR",{maximumFractionDigits:3});
function big(x){ if(!isFinite(x)) return "n/a"; return Math.abs(x)>=1000?nf.format(x):nf2.format(x); }
function updateProp(){
try {
const sails = $("aero-sails"); const P = sails ? parseFloat(sails.value)||0 : 0;
const rpm = $("aero-rpm"); const N = rpm ? parseFloat(rpm.value)||0 : 0;
const palt = $("aero-palt"); const y = palt ? parseFloat(palt.value)||0 : 0;
const T=propThrust(P,N);
const thrust = $("aero-othrust"); if(thrust) thrust.innerHTML=big(Math.abs(T))+' <small>pN</small>';
const thrustapp = $("aero-othrustapp"); if(thrustapp) thrustapp.innerHTML=big(Math.abs(T)*pressure(y+Y_OFFSET))+' <small>pN</small>';
} catch(e) { console.error('updateProp error:', e); }
}
function updateBalloon(){
try {
const vol = $("aero-vol"); const V = vol ? parseFloat(vol.value)||0 : 0;
const balt = $("aero-balt"); const y = balt ? parseFloat(balt.value)||0 : 0;
const force = $("aero-oforce"); if(force) force.innerHTML=big(balloonForce(V,y))+' <small>pN</small>';
const lift = $("aero-olift"); if(lift) lift.innerHTML=big(balloonLift(V,y))+' <small>kpg</small>';
const press = $("aero-opress"); if(press) press.textContent=nf3.format(pressure(y));
} catch(e) { console.error('updateBalloon error:', e); }
}
function updateVolume(){
try {
const vmass = $("aero-vmass"); const m = vmass ? parseFloat(vmass.value)||0 : 0;
const valt = $("aero-valt"); const y = valt ? parseFloat(valt.value)||0 : 0;
const V=requiredVolume(m,y);
const volreq = $("aero-ovolreq"); if(volreq) volreq.innerHTML=big(V)+' <small>m³</small>';
const cube = $("aero-ocube"); if(cube) cube.textContent=(isFinite(V)&&V>0)
? "≈ cube de "+nf2.format(Math.cbrt(V))+" blocs de côté · "+nf.format(Math.ceil(V))+" blocs d'air"
: "";
const warn = $("aero-vwarn"); if(warn) warn.innerHTML=(pressure(y)<=0)
? "<b>Altitude hors atmosphère :</b> pression nulle, aucun volume fini ne suffit." : "";
} catch(e) { console.error('updateVolume error:', e); }
}
// ---- graphe ----
let mode="mass";
const canvas=$("aero-chart"), tip=$("aero-tip");
if (!canvas) {
console.error('calculateur legacy: canvas not found, graph disabled');
}
const ctx = canvas ? canvas.getContext("2d") : null;
let geom=null;
const tv=n=>getComputedStyle(root).getPropertyValue(n).trim()||"#888";
function niceStep(raw){ const p=Math.pow(10,Math.floor(Math.log10(raw))); const n=raw/p; return (n<1.5?1:n<3?2:n<7?5:10)*p; }
function abbr(v){ const a=Math.abs(v); if(a>=1e6)return nf2.format(v/1e6)+"M"; if(a>=1e3)return nf2.format(v/1e3)+"k"; return nf.format(v); }
function drawChart(){
if(!canvas||!ctx) { console.warn('calculateur legacy: graph not available'); return; }
const dpr=window.devicePixelRatio||1;
const cssW=canvas.parentElement.clientWidth;
const cssH=Math.max(260,Math.round(cssW*0.44));
canvas.style.height=cssH+"px";
canvas.width=Math.round(cssW*dpr); canvas.height=Math.round(cssH*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,cssW,cssH);
const padL=54,padR=16,padT=14,padB=38, pw=cssW-padL-padR, ph=cssH-padT-padB;
const fg=tv("--aero-fg"), muted=tv("--aero-muted"), border=tv("--aero-border"), accent=tv("--aero-accent"), warn=tv("--aero-warn");
let xMin,xMax,xLabel,fixed;
if(mode==="mass"){
const fixvol = $("aero-fixvol");
fixed=Math.max(1, fixvol ? parseFloat(fixvol.value)||1 : 1);
xMin=1; xMax=Math.max(2,fixed*C.sAir*C.pMax*1.15); xLabel="poids m (kpg)";
} else {
const fixmass = $("aero-fixmass");
fixed=Math.max(1, fixmass ? parseFloat(fixmass.value)||1 : 1);
xMin=Math.max(1,fixed/(C.sAir*C.pMax)*0.5); xMax=Math.max(xMin*1.5,4*fixed/C.sAir); xLabel="volume V (m³)";
}
const yMin=C.yBot,yMax=C.yTop;
const X=x=>padL+(x-xMin)/(xMax-xMin)*pw;
const Y=y=>padT+(1-(y-yMin)/(yMax-yMin))*ph;
ctx.font='11px '+(getComputedStyle(root).fontFamily||"sans-serif");
// grille
ctx.strokeStyle=border; ctx.fillStyle=muted; ctx.lineWidth=1;
ctx.textBaseline="middle"; ctx.textAlign="right";
const yStep=niceStep((yMax-yMin)/6);
for(let yy=Math.ceil(yMin/yStep)*yStep; yy<=yMax; yy+=yStep){
const py=Y(yy); ctx.beginPath();ctx.moveTo(padL,py);ctx.lineTo(padL+pw,py);ctx.stroke();
ctx.fillText(String(yy),padL-7,py);
}
ctx.textAlign="center"; ctx.textBaseline="top";
const xStep=niceStep((xMax-xMin)/6);
for(let xx=Math.ceil(xMin/xStep)*xStep; xx<=xMax; xx+=xStep){
const px=X(xx); ctx.beginPath();ctx.moveTo(px,padT);ctx.lineTo(px,padT+ph);ctx.stroke();
ctx.fillText(abbr(xx),px,padT+ph+7);
}
ctx.fillStyle=muted; ctx.fillText(xLabel,padL+pw/2,padT+ph+22);
ctx.save();ctx.translate(13,padT+ph/2);ctx.rotate(-Math.PI/2);ctx.fillText("altitude max y",0,0);ctx.restore();
function refLine(yv,label){
if(yv<yMin||yv>yMax) return;
const py=Y(yv);
ctx.strokeStyle=muted; ctx.setLineDash([4,4]); ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(padL,py);ctx.lineTo(padL+pw,py);ctx.stroke(); ctx.setLineDash([]);
ctx.fillStyle=muted; ctx.textAlign="left"; ctx.textBaseline="bottom"; ctx.fillText(label,padL+5,py-2);
}
refLine(C.sea,"niveau mer "+C.sea);
refLine(C.yTop,"limite "+C.yTop);
// courbe
const steps=240,pts=[];
for(let i=0;i<=steps;i++){ const x=xMin+(xMax-xMin)*i/steps; const r=(mode==="mass")?maxAltitude(fixed,x):maxAltitude(x,fixed); pts.push({x,y:r.y,status:r.status}); }
// zone grounded
ctx.fillStyle=warn; ctx.globalAlpha=.10; let gs=null;
for(let i=0;i<=steps;i++){
const g=pts[i].status==="grounded";
if(g&&gs===null)gs=pts[i].x;
if((!g||i===steps)&&gs!==null){ ctx.fillRect(X(gs),padT,X(pts[i].x)-X(gs),ph); gs=null; }
}
ctx.globalAlpha=1;
ctx.lineWidth=2; ctx.strokeStyle=accent; ctx.beginPath(); let started=false;
for(const p of pts){ if(p.status==="grounded"){started=false;continue;} const px=X(p.x),py=Y(p.y); if(!started){ctx.moveTo(px,py);started=true;}else ctx.lineTo(px,py); }
ctx.stroke();
geom={padL,pw,padT,ph,xMin,xMax,X,Y,fixed};
}
if(canvas && tip) {
canvas.addEventListener("mousemove",e=>{
if(!geom)return; const rect=canvas.getBoundingClientRect(); const mx=e.clientX-rect.left;
if(mx<geom.padL||mx>geom.padL+geom.pw){tip.style.opacity=0;return;}
const x=geom.xMin+(mx-geom.padL)/geom.pw*(geom.xMax-geom.xMin);
const r=(mode==="mass")?maxAltitude(geom.fixed,x):maxAltitude(x,geom.fixed);
let txt = r.status==="grounded" ? abbr(x)+" : ne décolle pas"
: r.status==="ceiling" ? abbr(x)+" : plafond (y "+Math.round(r.y)+")"
: abbr(x)+" : y "+Math.round(r.y);
tip.textContent=txt; tip.style.left=geom.X(x)+"px"; tip.style.top=geom.Y(r.y)+"px"; tip.style.opacity=1;
});
canvas.addEventListener("mouseleave",()=>{tip.style.opacity=0;});
}
const modeSelector = $("aero-mode");
// Affiche uniquement l'input correspondant au mode courant
function applyModeVisibility(){
const fixVolWrap = $("aero-fixvolwrap");
const fixMassWrap = $("aero-fixmasswrap");
if(fixVolWrap) fixVolWrap.style.display = mode==="mass"?"":"none";
if(fixMassWrap) fixMassWrap.style.display= mode==="vol"?"":"none";
}
// ---- Partage par URL (paramètres GET, tous optionnels) ----
const paramInputs = Array.from(root.querySelectorAll("input, select")).filter(el=>el.id&&el.id.startsWith("aero-"));
const keyOf = el => el.id.slice(5);
const defaults = new Map(paramInputs.map(el=>[el, el.value]));
function applyUrlParams(){
const params = new URLSearchParams(window.location.search);
paramInputs.forEach(el=>{ const k=keyOf(el); if(params.has(k)) el.value=params.get(k); });
}
function syncUrl(){
try {
const params = new URLSearchParams();
paramInputs.forEach(el=>{ if(el.value!==defaults.get(el)) params.set(keyOf(el), el.value); });
const qs = params.toString();
window.history.replaceState(null, "", window.location.pathname + (qs?"?"+qs:"") + window.location.hash);
} catch(e){ console.error('calculateur legacy: syncUrl error:', e); }
}
if(modeSelector) {
modeSelector.addEventListener("change",e=>{
mode=e.target.value;
applyModeVisibility();
drawChart();
syncUrl();
});
}
root.querySelectorAll("input,select").forEach(el=>el.addEventListener("input",()=>{
try {
readConsts(); updateProp(); updateBalloon(); updateVolume(); drawChart(); syncUrl();
} catch(e) {
console.error('calculateur legacy: error during input event:', e);
}
}));
window.addEventListener("resize",()=>{ if(geom) drawChart(); });
try {
applyUrlParams();
if(modeSelector) mode = modeSelector.value;
applyModeVisibility();
readConsts(); updateProp(); updateBalloon(); updateVolume(); drawChart();
} catch(e) {
console.error('calculateur legacy: error during initialization:', e);
}
})();

View file

@ -0,0 +1,534 @@
/* ================================================================
assets/styles/calculator/index.css
Styles du calculateur Create Aeronautics
Cohérent avec le système « bulle » + tokens globaux (app.css).
Lisible en thème clair ET sombre.
================================================================ */
/* TOKENS DU GRAPHE
Lus en JS via getComputedStyle(.aero-calc) pour dessiner le canvas
(accent, warn, muted, border, fg). Doivent exister pour les 2 thèmes. */
.aero-calc {
--aero-fg: var(--text-light);
--aero-muted: var(--muted-light);
--aero-border: rgba(0, 0, 0, 0.14);
--aero-accent: var(--violet);
--aero-warn: #ef4444;
}
[data-theme="dark"] .aero-calc {
--aero-fg: var(--text-dark);
--aero-muted: var(--muted-dark);
--aero-border: rgba(255, 255, 255, 0.16);
--aero-accent: var(--violet-light);
--aero-warn: #f87171;
}
/* Couleur de texte par défaut des bulles en thème clair
(les bulles ne fixent la couleur qu'en sombre) */
.aero-calc .bulle {
color: var(--text-light);
}
[data-theme="dark"] .aero-calc .bulle {
color: var(--text-dark);
}
/* ── EN-TÊTE ────────────────────────────────────────────────────── */
.aero-calc .bulle-header {
text-align: center;
gap: 0.5rem;
}
.aero-calc h1 {
font-family: 'Syne', sans-serif;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 800;
letter-spacing: -0.01em;
color: var(--text-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc h1 {
color: var(--text-dark);
}
.aero-calc .ac-sub {
max-width: 62ch;
font-size: 0.9rem;
line-height: 1.5;
color: var(--muted-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-sub {
color: var(--muted-dark);
}
/* ── TITRES DE SECTION ──────────────────────────────────────────── */
.aero-calc .ac-h2,
.aero-calc .ac-h3 {
font-family: 'Syne', sans-serif;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-light);
transition: color var(--duration-slow);
}
.aero-calc .ac-h2 { font-size: 1.25rem; }
.aero-calc .ac-h3 { font-size: 1.05rem; }
[data-theme="dark"] .aero-calc .ac-h2,
[data-theme="dark"] .aero-calc .ac-h3 {
color: var(--text-dark);
}
/* Filet accent sous les titres de section principaux */
.aero-calc .ac-h2 {
display: inline-block;
padding-bottom: 0.35rem;
border-bottom: 2px solid var(--violet);
margin-bottom: 0.35rem;
}
/* ── TEXTES D'AIDE (hint / desc / note) ─────────────────────────── */
.aero-calc .ac-hint,
.aero-calc .ac-desc,
.aero-calc .ac-note,
.aero-calc .ac-cube {
font-size: 0.82rem;
line-height: 1.5;
color: var(--muted-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-hint,
[data-theme="dark"] .aero-calc .ac-desc,
[data-theme="dark"] .aero-calc .ac-note,
[data-theme="dark"] .aero-calc .ac-cube {
color: var(--muted-dark);
}
.aero-calc .ac-desc { margin-bottom: 1rem; }
.aero-calc .ac-hint { margin-bottom: 0.85rem; }
.aero-calc .ac-note { margin-top: 0.85rem; }
.aero-calc .ac-cube { margin-top: 0.4rem; font-style: italic; }
.aero-calc .ac-note b { color: var(--violet); font-weight: 600; }
[data-theme="dark"] .aero-calc .ac-note b { color: var(--violet-light); }
/* ── CONSTANTES (details / summary) ─────────────────────────────── */
.aero-calc .ac-consts {
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 0.25rem 1rem;
transition: border-color var(--duration-slow), background var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-consts {
border-color: var(--border-dark);
}
.aero-calc .ac-consts summary {
cursor: pointer;
list-style: none;
padding: 0.75rem 0;
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 0.95rem;
color: var(--text-light);
display: flex;
align-items: center;
gap: 0.5rem;
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-consts summary {
color: var(--text-dark);
}
.aero-calc .ac-consts summary::-webkit-details-marker { display: none; }
/* Chevron custom qui pivote à l'ouverture */
.aero-calc .ac-consts summary::before {
content: '';
width: 0.5rem;
height: 0.5rem;
border-right: 2px solid var(--violet);
border-bottom: 2px solid var(--violet);
transform: rotate(-45deg);
transition: transform var(--duration-normal);
flex-shrink: 0;
}
[data-theme="dark"] .aero-calc .ac-consts summary::before {
border-color: var(--violet-light);
}
.aero-calc .ac-consts[open] summary::before {
transform: rotate(45deg);
}
.aero-calc .ac-consts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.85rem;
padding: 0.5rem 0 1rem;
}
/* ── CHAMPS (label + input) ─────────────────────────────────────── */
.aero-calc .ac-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.aero-calc .ac-field label {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.5rem;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--text-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-field label {
color: var(--text-dark);
}
/* Unité affichée à droite du label */
.aero-calc .ac-field label .u {
font-weight: 500;
font-size: 0.72rem;
color: var(--muted-light);
white-space: nowrap;
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-field label .u {
color: var(--muted-dark);
}
.aero-calc .ac-field input,
.aero-calc .ac-field select,
.aero-calc .ac-controls select {
width: 100%;
padding: 0.55rem 0.8rem;
border-radius: 10px;
border: 1px solid var(--border-light);
background-color: var(--nav-bg-light);
color: var(--text-light);
font-family: 'DM Sans', sans-serif;
font-size: 0.9rem;
outline: none;
appearance: none;
transition: border-color var(--duration-normal), box-shadow var(--duration-normal),
background var(--duration-slow), color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-field input,
[data-theme="dark"] .aero-calc .ac-field select,
[data-theme="dark"] .aero-calc .ac-controls select {
background-color: var(--nav-bg-dark);
border-color: var(--border-dark);
color: var(--text-dark);
}
.aero-calc .ac-field input:focus,
.aero-calc .ac-field select:focus {
border-color: var(--violet);
box-shadow: 0 0 0 3px var(--violet-glow);
}
/* Flèche des <select> (reprise de form.css) */
.aero-calc select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%237C3AED' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.7rem center;
background-size: 0.7rem;
padding-right: 2.2rem;
cursor: pointer;
}
[data-theme="dark"] .aero-calc select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23A78BFA' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
}
/* ── OUTILS (Hélice / Ballon) ───────────────────────────────────── */
.aero-calc .ac-tool {
display: flex;
flex-direction: column;
gap: 0.7rem;
}
/* ── READOUTS (clé → valeur) ────────────────────────────────────── */
.aero-calc .ac-readout {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.55rem 0.75rem;
border-radius: 10px;
background: var(--pill-bg-light);
transition: background var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-readout {
background: var(--pill-bg-dark);
}
.aero-calc .ac-rk {
font-size: 0.8rem;
color: var(--muted-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-rk {
color: var(--muted-dark);
}
.aero-calc .ac-rv {
font-variant-numeric: tabular-nums;
font-weight: 600;
font-size: 0.95rem;
text-align: right;
white-space: nowrap;
color: var(--text-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-rv {
color: var(--text-dark);
}
/* Valeur mise en avant */
.aero-calc .ac-rv.key {
font-size: 1.15rem;
font-weight: 700;
color: var(--violet);
}
[data-theme="dark"] .aero-calc .ac-rv.key {
color: var(--violet-light);
}
.aero-calc .ac-rv small {
font-size: 0.72rem;
font-weight: 500;
color: var(--muted-light);
margin-left: 0.15rem;
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-rv small {
color: var(--muted-dark);
}
/* ── CONTRÔLES DU GRAPHE ────────────────────────────────────────── */
.aero-calc .ac-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.85rem;
margin-bottom: 1.1rem;
}
/* ── ZONE GRAPHE ────────────────────────────────────────────────── */
.aero-calc .ac-chart-shell {
position: relative;
width: 100%;
padding: 0.5rem;
border-radius: 12px;
border: 1px solid var(--border-light);
background: rgba(255, 255, 255, 0.35);
transition: background var(--duration-slow), border-color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-chart-shell {
border-color: var(--border-dark);
background: rgba(0, 0, 0, 0.18);
}
.aero-calc #aero-chart {
display: block;
width: 100%;
}
/* Infobulle suivant le curseur (position gérée en JS) */
.aero-calc .ac-tip {
position: absolute;
transform: translate(-50%, -130%);
padding: 0.3rem 0.55rem;
border-radius: 8px;
background: var(--violet);
color: #fff;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
pointer-events: none;
opacity: 0;
box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35);
transition: opacity var(--duration-fast);
z-index: 2;
}
/* ── LÉGENDE ────────────────────────────────────────────────────── */
.aero-calc .ac-legend {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
margin-top: 0.9rem;
}
.aero-calc .ac-legend span {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.8rem;
color: var(--muted-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-legend span {
color: var(--muted-dark);
}
.aero-calc .ac-sw {
width: 1.1rem;
height: 0.3rem;
border-radius: 999px;
flex-shrink: 0;
background: var(--aero-muted);
}
/* Correspondance avec les couleurs tracées sur le canvas */
.aero-calc .ac-legend span:nth-child(1) .ac-sw {
background: var(--aero-accent);
}
.aero-calc .ac-legend span:nth-child(2) .ac-sw {
background: var(--aero-muted);
height: 0;
border-top: 2px dashed var(--aero-muted);
}
.aero-calc .ac-legend span:nth-child(3) .ac-sw {
background: color-mix(in srgb, var(--aero-warn) 45%, transparent);
height: 0.7rem;
border-radius: 3px;
}
/* ── VOLUME REQUIS ──────────────────────────────────────────────── */
/* Inputs en ligne, résultat en dessous (comme les sections Hélice/Ballon) */
.aero-calc .ac-vol {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.aero-calc .ac-vol-inputs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 1rem;
}
/* ── FORMULES ───────────────────────────────────────────────────── */
.aero-calc .ac-formulas {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 0.9rem;
}
.aero-calc .ac-formulas > div {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0.85rem;
border-radius: 10px;
border: 1px solid var(--border-light);
background: rgba(255, 255, 255, 0.3);
transition: border-color var(--duration-slow), background var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-formulas > div {
border-color: var(--border-dark);
background: rgba(0, 0, 0, 0.16);
}
.aero-calc .ac-fk {
font-size: 0.78rem;
font-weight: 600;
color: var(--muted-light);
transition: color var(--duration-slow);
}
[data-theme="dark"] .aero-calc .ac-fk {
color: var(--muted-dark);
}
.aero-calc .ac-formulas code {
font-family: 'DM Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
font-size: 0.85rem;
color: var(--violet);
word-break: break-word;
}
[data-theme="dark"] .aero-calc .ac-formulas code {
color: var(--violet-light);
}
/* ── RESPONSIVE ─────────────────────────────────────────────────── */
@media screen and (max-width: 768px) {
/* Conteneur + bulles plus compacts pour gagner de la largeur utile */
.aero-calc.bulle-container {
padding: 0.5rem;
gap: 0.75rem;
}
.aero-calc .bulle {
padding: 1rem;
}
/* Titres un cran plus petits */
.aero-calc h1 { font-size: 1.35rem; }
.aero-calc .ac-h2 { font-size: 1.1rem; }
.aero-calc .ac-h3 { font-size: 1rem; }
/* Grilles denses : on resserre les colonnes minimales */
.aero-calc .ac-consts-grid {
grid-template-columns: repeat(auto-fit, minmax(115px, 1fr));
gap: 0.6rem;
}
.aero-calc .ac-controls {
grid-template-columns: 1fr;
}
/* Readout empilé : libellé au-dessus, valeur en dessous */
.aero-calc .ac-readout {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.aero-calc .ac-rv,
.aero-calc .ac-rv.key {
text-align: left;
}
.aero-calc .ac-legend {
gap: 0.4rem 1rem;
}
.aero-calc .ac-chart-shell {
padding: 0.35rem;
}
}
/* Très petits écrans : formules et constantes sur une seule colonne */
@media screen and (max-width: 420px) {
.aero-calc .ac-consts-grid,
.aero-calc .ac-formulas {
grid-template-columns: 1fr;
}
}

View file

@ -7,10 +7,10 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^2.18",
"doctrine/doctrine-bundle": "^2.18.3",
"doctrine/doctrine-migrations-bundle": "^3.7",
"doctrine/orm": "^3.6",
"phpdocumentor/reflection-docblock": "^6.0",
"doctrine/orm": "^3.6.7",
"phpdocumentor/reflection-docblock": "^6.0.3",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "7.4.*",
"symfony/asset-mapper": "7.4.*",
@ -18,14 +18,14 @@
"symfony/doctrine-messenger": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/expression-language": "7.4.*",
"symfony/flex": "^2",
"symfony/flex": "^2.11",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/http-client": "7.4.*",
"symfony/intl": "7.4.*",
"symfony/mailer": "7.4.*",
"symfony/mime": "7.4.*",
"symfony/monolog-bundle": "^3.0|^4.0",
"symfony/monolog-bundle": "^3.0|^4.0.2",
"symfony/notifier": "7.4.*",
"symfony/process": "7.4.*",
"symfony/property-access": "7.4.*",
@ -33,16 +33,16 @@
"symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/stimulus-bundle": "^2.32",
"symfony/stimulus-bundle": "^2.36",
"symfony/string": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/ux-turbo": "^2.32",
"symfony/ux-turbo": "^2.36",
"symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*",
"symfony/yaml": "7.4.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"twig/extra-bundle": "^2.12|^3.24",
"twig/twig": "^2.12|^3.27.1",
"ext-zip": "*"
},
"config": {
@ -51,6 +51,9 @@
"symfony/flex": true,
"symfony/runtime": true
},
"platform": {
"php": "8.5"
},
"bump-after-update": true,
"sort-packages": true
},
@ -97,11 +100,11 @@
}
},
"require-dev": {
"phpunit/phpunit": "^12.5",
"phpunit/phpunit": "^12.5.30",
"symfony/browser-kit": "7.4.*",
"symfony/css-selector": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/maker-bundle": "^1.0",
"symfony/maker-bundle": "^1.67",
"symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.4.*"
}

13846
app/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -128,7 +128,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* @psalm-type FrameworkConfig = array{
* secret?: scalar|Param|null,
* http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false
* allowed_http_method_override?: list<string|Param>|null,
* allowed_http_method_override?: null|list<string|Param>,
* trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%"
* ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%"
* test?: bool|Param,
@ -136,9 +136,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false
* set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false
* enabled_locales?: list<scalar|Param|null>,
* trusted_hosts?: list<scalar|Param|null>,
* trusted_hosts?: string|list<scalar|Param|null>,
* trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"]
* trusted_headers?: list<scalar|Param|null>,
* trusted_headers?: string|list<scalar|Param|null>,
* error_controller?: scalar|Param|null, // Default: "error_controller"
* handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true
* csrf_protection?: bool|array{
@ -202,23 +202,23 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* property?: scalar|Param|null,
* service?: scalar|Param|null,
* },
* supports?: list<scalar|Param|null>,
* supports?: string|list<scalar|Param|null>,
* definition_validators?: list<scalar|Param|null>,
* support_strategy?: scalar|Param|null,
* initial_marking?: list<scalar|Param|null>,
* events_to_dispatch?: list<string|Param>|null,
* places?: list<array{ // Default: []
* initial_marking?: \BackedEnum|string|list<scalar|Param|null>,
* events_to_dispatch?: null|list<string|Param>,
* places?: string|list<array{ // Default: []
* name?: scalar|Param|null,
* metadata?: array<string, mixed>,
* }>,
* transitions?: list<array{ // Default: []
* name?: string|Param,
* guard?: string|Param, // An expression to block the transition.
* from?: list<array{ // Default: []
* from?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* to?: list<array{ // Default: []
* to?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
@ -271,7 +271,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* version_format?: scalar|Param|null, // Default: "%%s?%%s"
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: list<scalar|Param|null>,
* base_urls?: string|list<scalar|Param|null>,
* packages?: array<string, array{ // Default: []
* strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|Param|null, // Default: null
@ -279,12 +279,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* version_format?: scalar|Param|null, // Default: null
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: list<scalar|Param|null>,
* base_urls?: string|list<scalar|Param|null>,
* }>,
* },
* asset_mapper?: bool|array{ // Asset Mapper configuration
* enabled?: bool|Param, // Default: true
* paths?: array<string, scalar|Param|null>,
* paths?: string|array<string, scalar|Param|null>,
* excluded_patterns?: list<scalar|Param|null>,
* exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
* server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true
@ -303,7 +303,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* translator?: bool|array{ // Translator configuration
* enabled?: bool|Param, // Default: true
* fallbacks?: list<scalar|Param|null>,
* fallbacks?: string|list<scalar|Param|null>,
* logging?: bool|Param, // Default: false
* formatter?: scalar|Param|null, // Default: "translator.formatter.default"
* cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations"
@ -333,7 +333,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enabled?: bool|Param, // Default: true
* cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0.
* enable_attributes?: bool|Param, // Default: true
* static_method?: list<scalar|Param|null>,
* static_method?: string|list<scalar|Param|null>,
* translation_domain?: scalar|Param|null, // Default: "validators"
* email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5"
* mapping?: array{
@ -396,7 +396,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection"
* default_pdo_provider?: scalar|Param|null, // Default: null
* pools?: array<string, array{ // Default: []
* adapters?: list<scalar|Param|null>,
* adapters?: string|list<scalar|Param|null>,
* tags?: scalar|Param|null, // Default: null
* public?: bool|Param, // Default: false
* default_lifetime?: scalar|Param|null, // Default lifetime of the pool.
@ -419,11 +419,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* lock?: bool|string|array{ // Lock configuration
* enabled?: bool|Param, // Default: false
* resources?: array<string, string|list<scalar|Param|null>>,
* resources?: string|array<string, string|list<scalar|Param|null>>,
* },
* semaphore?: bool|string|array{ // Semaphore configuration
* enabled?: bool|Param, // Default: false
* resources?: array<string, scalar|Param|null>,
* resources?: string|array<string, scalar|Param|null>,
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool|Param, // Default: true
@ -453,7 +453,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null
* }>,
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* stop_worker_on_signals?: list<scalar|Param|null>,
* stop_worker_on_signals?: int|string|list<scalar|Param|null>,
* default_bus?: scalar|Param|null, // Default: null
* buses?: array<string, array{ // Default: {"messenger.bus.default":{"default_middleware":{"enabled":true,"allow_no_handlers":false,"allow_no_senders":true},"middleware":[]}}
* default_middleware?: bool|string|array{
@ -461,7 +461,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* allow_no_handlers?: bool|Param, // Default: false
* allow_no_senders?: bool|Param, // Default: true
* },
* middleware?: list<string|array{ // Default: []
* middleware?: string|list<string|array{ // Default: []
* id?: scalar|Param|null,
* arguments?: list<mixed>,
* }>,
@ -510,9 +510,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: list<string|Param>,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
@ -563,9 +563,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: list<string|Param>,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
@ -582,8 +582,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* transports?: array<string, scalar|Param|null>,
* envelope?: array{ // Mailer Envelope configuration
* sender?: scalar|Param|null,
* recipients?: list<scalar|Param|null>,
* allowed_recipients?: list<scalar|Param|null>,
* recipients?: string|list<scalar|Param|null>,
* allowed_recipients?: string|list<scalar|Param|null>,
* },
* headers?: array<string, string|array{ // Default: []
* value?: mixed,
@ -635,7 +635,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
* storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
* policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
* limiters?: list<scalar|Param|null>,
* limiters?: string|list<scalar|Param|null>,
* limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
* interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket".
@ -658,20 +658,20 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
* allow_elements?: array<string, mixed>,
* block_elements?: list<string|Param>,
* drop_elements?: list<string|Param>,
* block_elements?: string|list<string|Param>,
* drop_elements?: string|list<string|Param>,
* allow_attributes?: array<string, mixed>,
* drop_attributes?: array<string, mixed>,
* force_attributes?: array<string, array<string, string|Param>>,
* force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false
* allowed_link_schemes?: list<string|Param>,
* allowed_link_hosts?: list<string|Param>|null,
* allowed_link_schemes?: string|list<string|Param>,
* allowed_link_hosts?: null|string|list<string|Param>,
* allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false
* allowed_media_schemes?: list<string|Param>,
* allowed_media_hosts?: list<string|Param>|null,
* allowed_media_schemes?: string|list<string|Param>,
* allowed_media_hosts?: null|string|list<string|Param>,
* allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false
* with_attribute_sanitizers?: list<string|Param>,
* without_attribute_sanitizers?: list<string|Param>,
* with_attribute_sanitizers?: string|list<string|Param>,
* without_attribute_sanitizers?: string|list<string|Param>,
* max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0
* }>,
* },
@ -718,7 +718,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -769,7 +769,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -801,7 +801,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -967,7 +967,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* auto_reload?: scalar|Param|null,
* optimizations?: int|Param,
* default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates"
* file_name_pattern?: list<scalar|Param|null>,
* file_name_pattern?: string|list<scalar|Param|null>,
* paths?: array<string, mixed>,
* date?: array{ // The default format options used by the date filter.
* format?: scalar|Param|null, // Default: "F j, Y H:i"
@ -1049,7 +1049,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* use_underscore?: bool|Param, // Default: true
* unordered_list_markers?: list<scalar|Param|null>,
* },
* ...<mixed>
* ...<string, mixed>
* },
* }
* @psalm-type SecurityConfig = array{
@ -1067,7 +1067,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* password_hashers?: array<string, string|array{ // Default: []
* algorithm?: scalar|Param|null,
* migrate_from?: list<scalar|Param|null>,
* migrate_from?: string|list<scalar|Param|null>,
* hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512"
* key_length?: scalar|Param|null, // Default: 40
* ignore_case?: bool|Param, // Default: false
@ -1081,7 +1081,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* providers?: array<string, array{ // Default: []
* id?: scalar|Param|null,
* chain?: array{
* providers?: list<scalar|Param|null>,
* providers?: string|list<scalar|Param|null>,
* },
* entity?: array{
* class?: scalar|Param|null, // The full entity class name of your user class.
@ -1091,7 +1091,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* memory?: array{
* users?: array<string, array{ // Default: []
* password?: scalar|Param|null, // Default: null
* roles?: list<scalar|Param|null>,
* roles?: string|list<scalar|Param|null>,
* }>,
* },
* ldap?: array{
@ -1100,7 +1100,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* search_dn?: scalar|Param|null, // Default: null
* search_password?: scalar|Param|null, // Default: null
* extra_fields?: list<scalar|Param|null>,
* default_roles?: list<scalar|Param|null>,
* default_roles?: string|list<scalar|Param|null>,
* role_fetcher?: scalar|Param|null, // Default: null
* uid_key?: scalar|Param|null, // Default: "sAMAccountName"
* filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})"
@ -1110,7 +1110,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* firewalls?: array<string, array{ // Default: []
* pattern?: scalar|Param|null,
* host?: scalar|Param|null,
* methods?: list<scalar|Param|null>,
* methods?: string|list<scalar|Param|null>,
* security?: bool|Param, // Default: true
* user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker"
* request_matcher?: scalar|Param|null,
@ -1129,8 +1129,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* path?: scalar|Param|null, // Default: "/logout"
* target?: scalar|Param|null, // Default: "/"
* invalidate_session?: bool|Param, // Default: true
* clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>,
* delete_cookies?: array<string, array{ // Default: []
* clear_site_data?: string|list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>,
* delete_cookies?: string|array<string, array{ // Default: []
* path?: scalar|Param|null, // Default: null
* domain?: scalar|Param|null, // Default: null
* secure?: scalar|Param|null, // Default: false
@ -1268,7 +1268,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* success_handler?: scalar|Param|null,
* failure_handler?: scalar|Param|null,
* realm?: scalar|Param|null, // Default: null
* token_extractors?: list<scalar|Param|null>,
* token_extractors?: string|list<scalar|Param|null>,
* token_handler?: string|array{
* id?: scalar|Param|null,
* oidc_user_info?: string|array{
@ -1283,7 +1283,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* oidc?: array{
* discovery?: array{ // Enable the OIDC discovery.
* base_uri?: list<scalar|Param|null>,
* base_uri?: string|list<scalar|Param|null>,
* cache?: array{
* id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.
* },
@ -1326,7 +1326,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* remember_me?: array{
* secret?: scalar|Param|null, // Default: "%kernel.secret%"
* service?: scalar|Param|null,
* user_providers?: list<scalar|Param|null>,
* user_providers?: string|list<scalar|Param|null>,
* catch_exceptions?: bool|Param, // Default: true
* signature_properties?: list<scalar|Param|null>,
* token_provider?: string|array{
@ -1354,12 +1354,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* path?: scalar|Param|null, // Use the urldecoded format. // Default: null
* host?: scalar|Param|null, // Default: null
* port?: int|Param, // Default: null
* ips?: list<scalar|Param|null>,
* ips?: string|list<scalar|Param|null>,
* attributes?: array<string, scalar|Param|null>,
* route?: scalar|Param|null, // Default: null
* methods?: list<scalar|Param|null>,
* methods?: string|list<scalar|Param|null>,
* allow_if?: scalar|Param|null, // Default: null
* roles?: list<scalar|Param|null>,
* roles?: string|list<scalar|Param|null>,
* }>,
* role_hierarchy?: array<string, string|list<scalar|Param|null>>,
* }
@ -1440,7 +1440,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* delay_between_messages?: bool|Param, // Default: false
* topic?: int|Param, // Default: null
* factor?: int|Param, // Default: 1
* tags?: list<scalar|Param|null>,
* tags?: string|list<scalar|Param|null>,
* console_formatter_options?: mixed, // Default: []
* formatter?: scalar|Param|null,
* nested?: bool|Param, // Default: false
@ -1484,7 +1484,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* host?: scalar|Param|null,
* },
* from_email?: scalar|Param|null,
* to_email?: list<scalar|Param|null>,
* to_email?: string|list<scalar|Param|null>,
* subject?: scalar|Param|null,
* content_type?: scalar|Param|null, // Default: null
* headers?: list<scalar|Param|null>,

View file

@ -0,0 +1,18 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/calculator')]
final class CalculatorController extends AbstractController
{
#[Route('/', name: 'app_calculator')]
public function index(): Response
{
return $this->render('calculator/index.html.twig', [
]);
}
}

View file

@ -25,11 +25,17 @@ class ModType extends AbstractType
'JEI',
];
$isEdit = $options['data']?->getId() !== null;
$mods = $this->modRepository->findAvailableDependances($builder->getData());
$mods = [];
if ($isEdit) {
$mods = $this->modRepository->findAvailableDependances($builder->getData());
}
else{
$mods = $this->modRepository->findBy(array(), array('nom' => 'ASC'));
}
$builder
->add('nom', TextType::class, [
'disabled' => $isEdit,
'label' => 'Nom du mod',
'required' => true,
'attr' => [

View file

@ -24,7 +24,7 @@ class ModRepository extends ServiceEntityRepository
{
$excludedIds = [];
if ($mod !== null) {
$excludedIds = $this->collectDependencyIds($mod, []);
$excludedIds = $this->collectDependentIds($mod, []);
$excludedIds[] = $mod->getId();
}
$qb = $this->createQueryBuilder('m');
@ -63,6 +63,32 @@ class ModRepository extends ServiceEntityRepository
return $ids;
}
/**
* Collecte récursivement les IDs de tous les mods qui dépendent du mod donné
* (dépendances inverses, directes ou indirectes), via la relation `soumis`.
*
* @param int[] $visited IDs déjà visités (évite les boucles infinies)
* @return int[]
*/
private function collectDependentIds(Mod $mod, array $visited): array
{
$ids = [];
foreach ($mod->getSoumis() as $dependent) {
$depId = $dependent->getId();
// Evite les boucles infinies en cas de dépendants cycliques
if (in_array($depId, $visited, true)) {
continue;
}
$visited[] = $depId;
$ids[] = $depId;
$ids = array_merge($ids, $this->collectDependentIds($dependent, $visited));
}
return $ids;
}
// /**
// * @return Mod[] Returns an array of Mod objects

View file

@ -26,8 +26,9 @@
{% block stylesheets %}{% endblock %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>

View file

@ -0,0 +1,140 @@
{% extends 'base.html.twig' %}
{% block title %}Calcul{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('styles/globals/bulles.css') }}">
{# Styles spécifiques au calculateur (inputs, readouts, tip, chart) #}
<link rel="stylesheet" href="{{ asset('styles/calculator/index.css') }}">
{% endblock %}
{% block main %}
<div class="bulle-container aero-calc" data-controller="calculateur">
<div class="bulle bulle-all-col bulle-header">
<h1>Calulateur pour Create Aeronautics.</h1>
<p class="ac-sub">Calculs dérivés des mods Aeronautics et Sable. Le ballon est supposé pleinement chauffé (volume rempli = capacité).</p>
</div>
<div class="bulle bulle-all-col">
<details class="ac-consts">
<summary>Constantes (config par défaut)</summary>
<div class="ac-consts-grid">
<div class="ac-field"><label>C_T <span class="u">poussée</span></label><input id="aero-ct" type="number" step="any" value="0.2"></div>
<div class="ac-field"><label>s_air <span class="u">kpg/m³</span></label><input id="aero-sair" type="number" step="any" value="1.5"></div>
<div class="ac-field"><label>g <span class="u">gravité</span></label><input id="aero-g" type="number" step="any" value="11"></div>
<div class="ac-field"><label>y_mer <span class="u">niveau mer</span></label><input id="aero-sea" type="number" step="any" value="63"></div>
<div class="ac-field"><label>k <span class="u">pente pression</span></label><input id="aero-k" type="number" step="any" value="-0.004"></div>
<div class="ac-field"><label>P_max <span class="u">plafond</span></label><input id="aero-pmax" type="number" step="any" value="1.5"></div>
<div class="ac-field"><label>y_top <span class="u">limite haute</span></label><input id="aero-ytop" type="number" step="any" value="320"></div>
<div class="ac-field"><label>y_bas <span class="u">limite basse</span></label><input id="aero-ybot" type="number" step="any" value="-64"></div>
</div>
</details>
</div>
<div class="bulle bulle-all-col">
<div class="ac-tool">
<h2 class="ac-h3">Hélice</h2>
<p class="ac-hint">Poussée le long de l'axe du bearing.</p>
<div class="ac-field"><label>Voiles (sails) <span class="u">P</span></label><input id="aero-sails" type="number" min="0" step="1" value="24"></div>
<div class="ac-field"><label>Vitesse de rotation <span class="u">N · RPM</span></label><input id="aero-rpm" type="number" step="any" value="256"></div>
<div class="ac-field"><label>Altitude <span class="u">y</span></label><input id="aero-palt" type="number" step="any" value="63"></div>
<div class="ac-readout"><div class="ac-rk">Poussée brute</div><div class="ac-rv key" id="aero-othrust">0 <small>pN</small></div></div>
<div class="ac-readout"><div class="ac-rk">Appliquée à l'arrêt (× P_air)</div><div class="ac-rv" id="aero-othrustapp">0 <small>pN</small></div></div>
</div>
</div>
<div class="bulle bulle-all-col">
<div class="ac-tool">
<h2 class="ac-h3">Ballon chauffé</h2>
<p class="ac-hint">Pleinement chauffé : volume rempli = capacité V.</p>
<div class="ac-field"><label>Volume intérieur <span class="u">V · m³</span></label><input id="aero-vol" type="number" min="0" step="any" value="512"></div>
<div class="ac-field"><label>Altitude <span class="u">y</span></label><input id="aero-balt" type="number" step="any" value="63"></div>
<div class="ac-readout"><div class="ac-rk">Force ascensionnelle brute</div><div class="ac-rv key" id="aero-oforce">0 <small>pN</small></div></div>
<div class="ac-readout"><div class="ac-rk">Portance (masse soulevable)</div><div class="ac-rv" id="aero-olift">0 <small>kpg</small></div></div>
<div class="ac-readout"><div class="ac-rk">Pression locale</div><div class="ac-rv" id="aero-opress">1.000</div></div>
</div>
</div>
<div class="bulle bulle-all-col">
<h2 class="ac-h2">Altitude maximale du ballon</h2>
<p class="ac-desc">Altitude de flottaison neutre où la portance compense le poids.</p>
<div class="ac-controls">
<div class="ac-field">
<label>Tracer en fonction</label>
<select id="aero-mode">
<option value="mass">du poids (volume fixé)</option>
<option value="vol">du volume (poids fixé)</option>
</select>
</div>
<div class="ac-field" id="aero-fixvolwrap"><label>Volume fixé <span class="u">V · m³</span></label><input id="aero-fixvol" type="number" min="1" step="any" value="512"></div>
<div class="ac-field" id="aero-fixmasswrap"><label>Poids fixé <span class="u">m · kpg</span></label><input id="aero-fixmass" type="number" min="1" step="any" value="400"></div>
</div>
<div class="ac-chart-shell">
<canvas id="aero-chart"></canvas>
<div class="ac-tip" id="aero-tip"></div>
</div>
<div class="ac-legend">
<span><i class="ac-sw"></i> altitude max</span>
<span><i class="ac-sw"></i> repères (mer, limite)</span>
<span><i class="ac-sw"></i> ne décolle pas</span>
</div>
<p class="ac-note"><b>m</b> kpg = <b>m</b> blocs standard (masse défaut 1 kpg/bloc). L'atténuation de pression sur les 40 derniers mètres sous la limite haute n'est pas modélisée.</p>
</div>
<section class="bulle bulle-all-col">
<h2 class="ac-h2">Volume requis</h2>
<p class="ac-desc">Volume d'air chaud minimal pour flotter à une altitude visée.</p>
<div class="ac-vol">
<div class="ac-vol-inputs">
<div class="ac-field"><label>Poids total <span class="u">m · kpg</span></label><input id="aero-vmass" type="number" min="0" step="any" value="400"></div>
<div class="ac-field"><label>Altitude visée <span class="u">y</span></label><input id="aero-valt" type="number" step="any" value="150"></div>
</div>
<div class="ac-vol-out">
<div class="ac-readout"><div class="ac-rk">Volume requis</div><div class="ac-rv key" id="aero-ovolreq">0 <small>m³</small></div></div>
<p class="ac-cube" id="aero-ocube"></p>
</div>
</div>
<p class="ac-note" id="aero-vwarn"></p>
</section>
<section class="bulle bulle-all-col">
<h2 class="ac-h2">Formules</h2>
<div class="ac-formulas">
<div><div class="ac-fk">Poussée hélice brute</div><code>T = C_T · P^(3/2) · N</code></div>
<div><div class="ac-fk">Appliquée (à l'arrêt, A_flux = 1)</div><code>F = T · A_flux · P_air</code></div>
<div><div class="ac-fk">Force ballon brute (pleinement chauffé)</div><code>F = V · s_air · g · P_air(y)</code></div>
<div><div class="ac-fk">Portance (masse)</div><code>L = V · s_air · P_air(y)</code></div>
<div><div class="ac-fk">Pression atmosphérique</div><code>P_air(y) = min( e^(k·(y y_mer)) , P_max )</code></div>
<div><div class="ac-fk">Altitude max (flottaison neutre)</div><code>y_max = y_mer + ln( V·s_air / m ) / k</code></div>
<div><div class="ac-fk">Volume requis</div><code>V = m / ( s_air · P_air(y) )</code></div>
</div>
</section>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
// Fallback: si Stimulus n'a pas attaché le controller (par ex. problème de chargement),
// on charge l'ancien script autonome qui cible la class .aero-calc.
setTimeout(function(){
try{
const root = document.querySelector('.aero-calc');
if(root && root.dataset.calculateurConnected !== '1'){
const s = document.createElement('script');
s.type = 'module';
s.src = '{{ asset('js/calcultateur_create.js') }}';
document.head.appendChild(s);
console.warn('calculateur: fallback legacy script injected');
}
}catch(e){ console.error(e); }
}, 300);
</script>
{% endblock %}

View file

@ -40,11 +40,17 @@
Statistiques
</a>
<a href="{{ path('app_accueil') }}"
class="nav-link {{ current_route == 'app_home' ? 'active' }}"
class="nav-link {{ current_route == 'app_news' ? 'active' }}"
data-action="click->header-mobile#close"
>
Actualité
</a>
<a href="{{ path('app_calculator') }}"
class="nav-link {{ current_route == 'app_calculator' ? 'active' }}"
data-action="click->header-mobile#close"
>
Calculateur
</a>
<a href="{{ path('app_maps') }}"
class="nav-link {{ current_route == 'app_maps' ? 'active' }}"
data-action="click->header-mobile#close"

View file

@ -3,7 +3,7 @@
# Le code source est copié dans l'image au moment du build
# ============================================================
FROM php:8.3-apache
FROM php:8.5-apache
# ── Dépendances système ──────────────────────────────────────
RUN apt-get update && apt-get install -y \
@ -17,7 +17,6 @@ RUN apt-get update && apt-get install -y \
pdo \
pdo_pgsql \
zip \
opcache \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*