Buckets:
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>SimSite - Applications de Simulation</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', sans-serif; margin: 0; padding: 0; background: #0a0a1a; color: #eee; min-height: 100vh; } | |
| /* Navigation */ | |
| .navbar { | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
| padding: 15px 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-bottom: 1px solid #0f3460; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .navbar-brand { | |
| font-size: 1.5em; | |
| font-weight: bold; | |
| color: #00d4ff; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .navbar-brand:hover { opacity: 0.8; } | |
| .nav-breadcrumb { | |
| color: #888; | |
| font-size: 0.9em; | |
| } | |
| .nav-breadcrumb span { color: #00d4ff; } | |
| /* Views */ | |
| .view { display: none; padding: 30px; max-width: 1400px; margin: 0 auto; } | |
| .view.active { display: block; } | |
| /* Home Page */ | |
| .home-header { | |
| text-align: center; | |
| padding: 60px 20px; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #0f3460 100%); | |
| margin: -30px -30px 30px -30px; | |
| border-bottom: 2px solid #00d4ff; | |
| } | |
| .home-header h1 { | |
| font-size: 3em; | |
| margin: 0; | |
| background: linear-gradient(135deg, #00d4ff, #00ff88); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .home-header p { | |
| color: #aaa; | |
| font-size: 1.2em; | |
| margin-top: 10px; | |
| } | |
| /* App Cards */ | |
| .apps-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 30px; | |
| padding: 20px 0; | |
| } | |
| .app-card { | |
| background: linear-gradient(145deg, #1a1a2e, #16213e); | |
| border: 1px solid #0f3460; | |
| border-radius: 16px; | |
| padding: 30px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .app-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: linear-gradient(90deg, var(--card-color), transparent); | |
| } | |
| .app-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 40px rgba(0, 212, 255, 0.2); | |
| border-color: var(--card-color); | |
| } | |
| .app-card-icon { | |
| font-size: 3em; | |
| margin-bottom: 15px; | |
| } | |
| .app-card h2 { | |
| color: #fff; | |
| margin: 0 0 10px 0; | |
| font-size: 1.5em; | |
| } | |
| .app-card p { | |
| color: #aaa; | |
| margin: 0 0 20px 0; | |
| line-height: 1.6; | |
| } | |
| .app-card-tags { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .app-card-tag { | |
| background: rgba(0, 212, 255, 0.1); | |
| color: #00d4ff; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 0.8em; | |
| } | |
| .app-card-arrow { | |
| position: absolute; | |
| bottom: 30px; | |
| right: 30px; | |
| font-size: 1.5em; | |
| color: var(--card-color); | |
| opacity: 0; | |
| transform: translateX(-10px); | |
| transition: all 0.3s ease; | |
| } | |
| .app-card:hover .app-card-arrow { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| /* App-specific styles */ | |
| .section-title { | |
| color: #00d4ff; | |
| border-bottom: 2px solid #00d4ff; | |
| padding-bottom: 10px; | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .panel { | |
| background: #16213e; | |
| padding: 25px; | |
| border-radius: 12px; | |
| border: 1px solid #0f3460; | |
| margin-bottom: 20px; | |
| } | |
| .form-group { margin-bottom: 15px; } | |
| .form-group label { display: block; margin-bottom: 5px; color: #00d4ff; font-weight: 500; } | |
| .form-group input[type="text"], | |
| .form-group input[type="number"], | |
| .form-group textarea { | |
| width: 100%; | |
| padding: 12px; | |
| border-radius: 8px; | |
| border: 1px solid #0f3460; | |
| background: #1a1a2e; | |
| color: #fff; | |
| font-size: 1em; | |
| transition: border-color 0.3s; | |
| } | |
| .form-group input:focus, .form-group textarea:focus { | |
| outline: none; | |
| border-color: #00d4ff; | |
| } | |
| .form-group input[type="range"] { width: 100%; accent-color: #00d4ff; } | |
| .form-group input[type="file"] { color: #00d4ff; } | |
| .form-group textarea { font-family: 'Consolas', monospace; } | |
| button, .btn { | |
| background: linear-gradient(135deg, #00d4ff, #0099cc); | |
| color: #1a1a2e; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| font-size: 1em; | |
| transition: all 0.2s; | |
| } | |
| button:hover, .btn:hover { transform: scale(1.02); box-shadow: 0 5px 20px rgba(0, 212, 255, 0.3); } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } | |
| .btn-secondary { background: #0f3460; color: #00d4ff; } | |
| .btn-danger { background: #ff1744; color: #fff; } | |
| /* Grid layouts */ | |
| .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } | |
| .grid-2-1 { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; } | |
| @media (max-width: 900px) { | |
| .grid-2, .grid-2-1 { grid-template-columns: 1fr; } | |
| } | |
| /* Table */ | |
| table { width: 100%; border-collapse: collapse; } | |
| th, td { padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; } | |
| th { color: #00d4ff; font-weight: 600; } | |
| tr:hover { background: rgba(0, 212, 255, 0.05); } | |
| /* Status badges */ | |
| .badge { padding: 4px 12px; border-radius: 20px; font-weight: bold; font-size: 0.85em; } | |
| .badge-success { background: #00c853; color: #1a1a2e; } | |
| .badge-warning { background: #ffab00; color: #1a1a2e; } | |
| .badge-danger { background: #ff1744; color: #fff; } | |
| /* Results */ | |
| .result-card { | |
| background: #1a1a2e; | |
| padding: 20px; | |
| border-radius: 12px; | |
| border: 1px solid #0f3460; | |
| } | |
| .result-card h3 { color: #00d4ff; margin: 0 0 15px 0; } | |
| .result-value { font-size: 2.5em; color: #00d4ff; font-weight: bold; } | |
| .chart-container { height: 350px; position: relative; } | |
| /* Alerts */ | |
| .alert { padding: 15px 20px; border-radius: 8px; margin-bottom: 15px; } | |
| .alert-error { background: rgba(255, 23, 68, 0.2); border: 1px solid #ff1744; color: #ff6b6b; } | |
| .alert-success { background: rgba(0, 200, 83, 0.2); border: 1px solid #00c853; color: #69f0ae; } | |
| .alert-info { background: rgba(0, 212, 255, 0.1); border: 1px solid #0f3460; color: #00d4ff; } | |
| /* Tabs */ | |
| .tabs { display: flex; gap: 5px; margin-bottom: 20px; } | |
| .tab { padding: 10px 20px; background: #0f3460; color: #00d4ff; border: none; border-radius: 8px 8px 0 0; cursor: pointer; transition: all 0.2s; } | |
| .tab.active { background: #00d4ff; color: #1a1a2e; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| /* DV specific */ | |
| .dv-safe { color: #00c853; font-weight: bold; } | |
| .dv-unsafe { color: #ff1744; font-weight: bold; } | |
| /* Equation box */ | |
| .equation-box { | |
| background: #0f3460; | |
| padding: 20px; | |
| border-radius: 12px; | |
| text-align: center; | |
| font-size: 1.1em; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #00d4ff; | |
| } | |
| .equation-box .equation { font-size: 1.3em; margin: 10px 0; color: #00d4ff; } | |
| .equation-box small { color: #888; } | |
| /* Animation */ | |
| .animation-container { text-align: center; } | |
| .animation-controls { margin-top: 15px; display: flex; justify-content: center; gap: 10px; } | |
| .animation-controls button { width: auto; } | |
| /* Hidden utility */ | |
| .hidden { display: none ; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Navigation Bar --> | |
| <nav class="navbar"> | |
| <div class="navbar-brand" onclick="navigateTo('home')"> | |
| <span>SimSite</span> | |
| </div> | |
| <div class="nav-breadcrumb" id="breadcrumb"></div> | |
| </nav> | |
| <!-- HOME VIEW --> | |
| <div id="view-home" class="view active"> | |
| <div class="home-header"> | |
| <h1>SimSite</h1> | |
| <p>Plateforme de simulation numerique et d'analyse</p> | |
| </div> | |
| <div class="apps-grid"> | |
| <!-- FEniCS App Card --> | |
| <div class="app-card" style="--card-color: #00d4ff;" onclick="navigateTo('fenics')"> | |
| <div class="app-card-icon">🔬</div> | |
| <h2>Simulation FEniCS</h2> | |
| <p>Resolution d'equations aux derivees partielles (EDP). Simulez la diffusion thermique sur un domaine 2D avec visualisation en temps reel.</p> | |
| <div class="app-card-tags"> | |
| <span class="app-card-tag">EDP</span> | |
| <span class="app-card-tag">Diffusion</span> | |
| <span class="app-card-tag">Elements Finis</span> | |
| </div> | |
| <div class="app-card-arrow">→</div> | |
| </div> | |
| <!-- Dang Van App Card --> | |
| <div class="app-card" style="--card-color: #ff6b6b;" onclick="navigateTo('dangvan')"> | |
| <div class="app-card-icon">⚙️</div> | |
| <h2>Analyse Dang Van</h2> | |
| <p>Critere de fatigue multiaxiale pour l'analyse de la tenue en fatigue des pieces mecaniques soumises a des chargements complexes.</p> | |
| <div class="app-card-tags"> | |
| <span class="app-card-tag">Fatigue</span> | |
| <span class="app-card-tag">Multiaxial</span> | |
| <span class="app-card-tag">Mecanique</span> | |
| </div> | |
| <div class="app-card-arrow">→</div> | |
| </div> | |
| <!-- Placeholder for future apps --> | |
| <div class="app-card" style="--card-color: #888; opacity: 0.5; cursor: default;"> | |
| <div class="app-card-icon">🚀</div> | |
| <h2>Prochainement...</h2> | |
| <p>D'autres applications de simulation seront ajoutees. Restez connectes pour decouvrir de nouveaux outils d'analyse.</p> | |
| <div class="app-card-tags"> | |
| <span class="app-card-tag">A venir</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- FENICS VIEW --> | |
| <div id="view-fenics" class="view"> | |
| <h1 class="section-title">🔬 Simulation FEniCS</h1> | |
| <div class="equation-box"> | |
| <strong>Equation de diffusion transitoire</strong> | |
| <div class="equation">∂u/∂t = D · ∇²u + Q</div> | |
| <small> | |
| u(x,y,t) = champ de temperature | D = coefficient de diffusion | Q = terme source<br> | |
| Conditions: u(x,y,0) = 0 | u = 0 sur ∂Ω (bords) | |
| </small> | |
| </div> | |
| <div class="grid-2"> | |
| <!-- Form Panel --> | |
| <div class="panel"> | |
| <h2 class="section-title">Nouvelle Simulation</h2> | |
| <form id="simForm"> | |
| <div class="form-group"> | |
| <label>Nom de la simulation</label> | |
| <input type="text" id="simName" placeholder="Ma simulation"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Resolution du maillage: <span id="meshVal">32</span></label> | |
| <input type="range" id="meshResolution" min="8" max="64" step="8" value="32" oninput="document.getElementById('meshVal').textContent = this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Coefficient de diffusion D: <span id="diffVal">0.1</span></label> | |
| <input type="range" id="diffCoef" min="0.01" max="0.5" step="0.01" value="0.1" oninput="document.getElementById('diffVal').textContent = this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Terme source Q: <span id="sourceVal">1.0</span></label> | |
| <input type="range" id="sourceTerm" min="0.1" max="3.0" step="0.1" value="1.0" oninput="document.getElementById('sourceVal').textContent = this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Temps final: <span id="timeVal">1.0</span>s</label> | |
| <input type="range" id="timeFinal" min="0.1" max="2.0" step="0.1" value="1.0" oninput="document.getElementById('timeVal').textContent = this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Nombre de pas de temps: <span id="stepsVal">20</span></label> | |
| <input type="range" id="numSteps" min="10" max="50" step="5" value="20" oninput="document.getElementById('stepsVal').textContent = this.value"> | |
| </div> | |
| <button type="submit" id="submitBtn">Lancer la simulation</button> | |
| </form> | |
| <div id="formError" class="alert alert-error hidden"></div> | |
| </div> | |
| <!-- Simulations List --> | |
| <div class="panel"> | |
| <h2 class="section-title"> | |
| Simulations recentes | |
| <button class="btn-secondary" style="margin-left: auto; padding: 8px 16px;" onclick="loadSimulations()">Rafraichir</button> | |
| </h2> | |
| <table> | |
| <thead> | |
| <tr><th>ID</th><th>Nom</th><th>Status</th><th>Date</th></tr> | |
| </thead> | |
| <tbody id="simTable"> | |
| <tr><td colspan="4">Chargement...</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Results Section (hidden by default) --> | |
| <div id="resultSection" class="panel hidden"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <h2 class="section-title" style="margin: 0; border: none;">Resultats - Simulation #<span id="resultId"></span></h2> | |
| <button class="btn-danger" onclick="closeResults()">Fermer</button> | |
| </div> | |
| <div class="grid-2" style="margin-top: 20px;"> | |
| <div class="result-card"> | |
| <h3>Valeur maximale</h3> | |
| <div class="result-value" id="resultMax">-</div> | |
| </div> | |
| <div class="result-card"> | |
| <h3>Valeur moyenne</h3> | |
| <div class="result-value" id="resultMean">-</div> | |
| </div> | |
| </div> | |
| <div class="grid-2" style="margin-top: 20px;"> | |
| <div class="result-card"> | |
| <h3>Evolution temporelle</h3> | |
| <div class="chart-container"><canvas id="timeChart"></canvas></div> | |
| </div> | |
| <div class="result-card animation-container"> | |
| <h3>Animation de la solution</h3> | |
| <canvas id="animationCanvas" width="300" height="300"></canvas> | |
| <div class="animation-controls"> | |
| <button onclick="playAnimation()">Play</button> | |
| <button onclick="pauseAnimation()">Pause</button> | |
| <button onclick="resetAnimation()">Reset</button> | |
| </div> | |
| <p>Temps: <span id="animTime">0.00</span> / <span id="animTotal">1.00</span>s</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- DANG VAN VIEW --> | |
| <div id="view-dangvan" class="view"> | |
| <h1 class="section-title">⚙️ Analyse de Fatigue - Critere de Dang Van</h1> | |
| <div class="equation-box"> | |
| <strong>Critere de Dang Van</strong> | |
| <div class="equation">τ_max + a · σ_H ≤ b</div> | |
| <small> | |
| τ_max = contrainte de cisaillement maximale (balayage des facettes)<br> | |
| σ_H = pression hydrostatique = (σ_xx + σ_yy + σ_zz) / 3<br> | |
| a = pente du critere | b = limite en cisaillement alterne | |
| </small> | |
| </div> | |
| <div class="grid-2-1"> | |
| <div class="panel"> | |
| <h2 class="section-title">Donnees de contrainte</h2> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchDvTab('json')">Saisie JSON</button> | |
| <button class="tab" onclick="switchDvTab('file')">Import Fichier</button> | |
| </div> | |
| <div id="dvTabJson" class="tab-content active"> | |
| <div class="form-group"> | |
| <label>Serie de tenseurs de contrainte (format Voigt 6 composantes)</label> | |
| <textarea id="dvStress" rows="8" placeholder='Exemple: | |
| [[100, 0, 0, 0, 0, 0], | |
| [50, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0], | |
| [-50, 0, 0, 0, 0, 0], | |
| [-100, 0, 0, 0, 0, 0]] | |
| Format: [σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz] en MPa'></textarea> | |
| </div> | |
| </div> | |
| <div id="dvTabFile" class="tab-content"> | |
| <div class="form-group"> | |
| <label>Fichier CSV ou TXT</label> | |
| <input type="file" id="dvFile" accept=".csv,.txt,.dat"> | |
| </div> | |
| <div class="alert alert-info"> | |
| <strong>Format attendu:</strong><br> | |
| - 6 colonnes: σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz<br> | |
| - Une ligne = un pas de temps<br> | |
| - Separateurs: virgule, point-virgule, tabulation ou espace<br> | |
| - Lignes commencant par # ou // ignorees | |
| </div> | |
| <div id="dvFilePreview" class="hidden" style="margin-top: 15px;"> | |
| <label>Apercu (<span id="dvFileCount">0</span> pas de temps charges)</label> | |
| <textarea id="dvFileData" rows="4" readonly style="background: #0f3460;"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2 class="section-title">Parametres du critere</h2> | |
| <div class="form-group"> | |
| <label>Parametre a (pente)</label> | |
| <input type="number" id="dvA" value="0.3" step="0.01"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Parametre b (limite en MPa)</label> | |
| <input type="number" id="dvB" value="120.0" step="1"> | |
| </div> | |
| <button id="dvBtn" onclick="calculateDangVan()" style="width: 100%; margin-top: 20px;"> | |
| Calculer | |
| </button> | |
| </div> | |
| </div> | |
| <div id="dvError" class="alert alert-error hidden"></div> | |
| <!-- Results --> | |
| <div id="dvResults" class="hidden"> | |
| <div class="grid-2" style="margin-top: 20px;"> | |
| <div class="result-card"> | |
| <h3>Resultat de l'analyse</h3> | |
| <table> | |
| <tr><td>DV_max</td><td><strong id="dvMax">-</strong> MPa</td></tr> | |
| <tr><td>Marge (b - DV_max)</td><td><strong id="dvMargin">-</strong> MPa</td></tr> | |
| <tr><td>Verdict</td><td><span id="dvSafe">-</span></td></tr> | |
| <tr><td>Point critique</td><td>Pas de temps #<span id="dvCritical">-</span></td></tr> | |
| </table> | |
| </div> | |
| <div class="result-card"> | |
| <h3>Interpretation</h3> | |
| <div id="dvInterpretation" style="color: #aaa; line-height: 1.8;"></div> | |
| </div> | |
| </div> | |
| <div class="panel" style="margin-top: 20px;"> | |
| <h3 class="section-title">Diagramme de Dang Van</h3> | |
| <div class="chart-container" style="height: 450px;"> | |
| <canvas id="dvChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ============================================ | |
| // NAVIGATION | |
| // ============================================ | |
| const views = { | |
| home: { title: '', breadcrumb: '' }, | |
| fenics: { title: 'Simulation FEniCS', breadcrumb: '<span>Accueil</span> / Simulation FEniCS' }, | |
| dangvan: { title: 'Analyse Dang Van', breadcrumb: '<span>Accueil</span> / Analyse Dang Van' } | |
| }; | |
| function navigateTo(viewId) { | |
| // Hide all views | |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); | |
| // Show target view | |
| document.getElementById('view-' + viewId).classList.add('active'); | |
| // Update breadcrumb | |
| document.getElementById('breadcrumb').innerHTML = views[viewId].breadcrumb; | |
| // Scroll to top | |
| window.scrollTo(0, 0); | |
| // Load data if needed | |
| if (viewId === 'fenics') loadSimulations(); | |
| } | |
| // ============================================ | |
| // GLOBALS | |
| // ============================================ | |
| const API_URL = '/api'; | |
| let timeChart = null; | |
| let dvChart = null; | |
| let animationId = null; | |
| let animFrame = 0; | |
| let animData = []; | |
| let isAnimating = false; | |
| let animFrameImages = []; | |
| let useImageFrames = false; | |
| let dvStressFromFile = null; | |
| // ============================================ | |
| // FENICS SIMULATION | |
| // ============================================ | |
| async function loadSimulations() { | |
| try { | |
| const res = await fetch(API_URL + '/simulations/'); | |
| if (!res.ok) throw new Error('Erreur serveur: ' + res.status); | |
| const data = await res.json(); | |
| const tbody = document.getElementById('simTable'); | |
| if (data.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="4">Aucune simulation</td></tr>'; | |
| } else { | |
| tbody.innerHTML = data.slice(0, 10).map(sim => { | |
| const badge = sim.status === 'completed' ? 'badge-success' : | |
| sim.status === 'running' ? 'badge-warning' : 'badge-danger'; | |
| return `<tr onclick="showResults(${sim.id})" style="cursor:pointer"> | |
| <td>#${sim.id}</td> | |
| <td>${sim.name || 'Sans nom'}</td> | |
| <td><span class="badge ${badge}">${sim.status}</span></td> | |
| <td>${new Date(sim.created_at).toLocaleDateString()}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| } catch (err) { | |
| document.getElementById('simTable').innerHTML = `<tr><td colspan="4" class="alert-error">${err.message}</td></tr>`; | |
| } | |
| } | |
| async function showResults(id) { | |
| try { | |
| const res = await fetch(API_URL + '/simulations/' + id + '/'); | |
| if (!res.ok) throw new Error('Erreur serveur'); | |
| const sim = await res.json(); | |
| if (sim.status !== 'completed') { | |
| alert('Simulation non terminee (status: ' + sim.status + ')'); | |
| return; | |
| } | |
| if (!sim.result_summary) { | |
| alert('Pas de resultats disponibles'); | |
| return; | |
| } | |
| document.getElementById('resultSection').classList.remove('hidden'); | |
| document.getElementById('resultId').textContent = sim.id; | |
| document.getElementById('resultMax').textContent = (sim.result_summary.final_max || 0).toFixed(4); | |
| document.getElementById('resultMean').textContent = (sim.result_summary.final_mean || 0).toFixed(4); | |
| animData = sim.result_summary.time_series || []; | |
| updateTimeChart(animData); | |
| resetAnimation(); | |
| document.getElementById('resultSection').scrollIntoView({ behavior: 'smooth' }); | |
| } catch (err) { | |
| alert('Erreur: ' + err.message); | |
| } | |
| } | |
| function closeResults() { | |
| document.getElementById('resultSection').classList.add('hidden'); | |
| stopAnimation(); | |
| } | |
| function updateTimeChart(data) { | |
| const ctx = document.getElementById('timeChart').getContext('2d'); | |
| if (timeChart) timeChart.destroy(); | |
| if (!data || data.length === 0) return; | |
| timeChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: data.map(d => (d.time || 0).toFixed(2)), | |
| datasets: [{ | |
| label: 'Maximum', | |
| data: data.map(d => d.max || 0), | |
| borderColor: '#00d4ff', | |
| backgroundColor: 'rgba(0, 212, 255, 0.1)', | |
| fill: true, | |
| tension: 0.3 | |
| }, { | |
| label: 'Moyenne', | |
| data: data.map(d => d.mean || 0), | |
| borderColor: '#ff6b6b', | |
| backgroundColor: 'rgba(255, 107, 107, 0.1)', | |
| fill: true, | |
| tension: 0.3 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { legend: { labels: { color: '#fff' } } }, | |
| scales: { | |
| x: { ticks: { color: '#888' }, grid: { color: 'rgba(255,255,255,0.1)' }, title: { display: true, text: 'Temps (s)', color: '#00d4ff' } }, | |
| y: { ticks: { color: '#888' }, grid: { color: 'rgba(255,255,255,0.1)' }, title: { display: true, text: 'Valeur', color: '#00d4ff' } } | |
| } | |
| } | |
| }); | |
| } | |
| // Animation | |
| function stopAnimation() { | |
| if (animationId) cancelAnimationFrame(animationId); | |
| isAnimating = false; | |
| } | |
| function playAnimation() { | |
| if (animData.length === 0) return; | |
| isAnimating = true; | |
| animate(); | |
| } | |
| function pauseAnimation() { isAnimating = false; } | |
| function resetAnimation() { | |
| stopAnimation(); | |
| animFrame = 0; | |
| drawHeatmap(null); | |
| document.getElementById('animTime').textContent = '0.00'; | |
| document.getElementById('animTotal').textContent = animData.length > 0 ? (animData[animData.length-1].time || 0).toFixed(2) : '0.00'; | |
| } | |
| function animate() { | |
| if (!isAnimating || animFrame >= animData.length) { | |
| isAnimating = false; | |
| return; | |
| } | |
| const frameData = animData[animFrame]; | |
| drawHeatmap(frameData); | |
| document.getElementById('animTime').textContent = (frameData.time || 0).toFixed(2); | |
| animFrame++; | |
| animationId = requestAnimationFrame(() => setTimeout(animate, 100)); | |
| } | |
| function drawHeatmap(frameData) { | |
| const canvas = document.getElementById('animationCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const size = canvas.width; | |
| const gridSize = 32; | |
| const cellSize = size / gridSize; | |
| ctx.fillStyle = '#1a1a2e'; | |
| ctx.fillRect(0, 0, size, size); | |
| if (!frameData) { | |
| ctx.fillStyle = '#888'; | |
| ctx.font = '14px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Cliquez Play pour demarrer', size/2, size/2); | |
| return; | |
| } | |
| const maxVal = Math.max(...animData.map(d => d.max || 0.001)); | |
| for (let i = 0; i < gridSize; i++) { | |
| for (let j = 0; j < gridSize; j++) { | |
| const distance = Math.sqrt((i - gridSize/2)**2 + (j - gridSize/2)**2); | |
| const normalizedDist = Math.min(distance / (gridSize/2), 1); | |
| const heatValue = Math.max(0, 1 - normalizedDist) * ((frameData.max || 0) / maxVal); | |
| const r = Math.floor(255 * heatValue); | |
| const g = Math.floor(100 * heatValue); | |
| const b = Math.floor(255 * (1 - heatValue)); | |
| ctx.fillStyle = `rgb(${r},${g},${b})`; | |
| ctx.fillRect(j * cellSize, (gridSize - 1 - i) * cellSize, cellSize, cellSize); | |
| } | |
| } | |
| } | |
| // Form submission | |
| document.getElementById('simForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const btn = document.getElementById('submitBtn'); | |
| const errDiv = document.getElementById('formError'); | |
| errDiv.classList.add('hidden'); | |
| btn.disabled = true; | |
| btn.textContent = 'Calcul en cours...'; | |
| const params = { | |
| mesh_resolution: parseInt(document.getElementById('meshResolution').value), | |
| diffusion_coefficient: parseFloat(document.getElementById('diffCoef').value), | |
| source_term: parseFloat(document.getElementById('sourceTerm').value), | |
| time_final: parseFloat(document.getElementById('timeFinal').value), | |
| num_steps: parseInt(document.getElementById('numSteps').value) | |
| }; | |
| try { | |
| const res = await fetch(API_URL + '/simulations/', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name: document.getElementById('simName').value || 'Simulation', parameters: params }) | |
| }); | |
| if (!res.ok) throw new Error('Erreur creation: ' + res.status); | |
| btn.textContent = 'Simulation lancee!'; | |
| setTimeout(() => { | |
| loadSimulations(); | |
| btn.disabled = false; | |
| btn.textContent = 'Lancer la simulation'; | |
| }, 1500); | |
| } catch (err) { | |
| errDiv.textContent = err.message; | |
| errDiv.classList.remove('hidden'); | |
| btn.disabled = false; | |
| btn.textContent = 'Lancer la simulation'; | |
| } | |
| }); | |
| // ============================================ | |
| // DANG VAN | |
| // ============================================ | |
| function switchDvTab(tab) { | |
| document.querySelectorAll('.tabs .tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| if (tab === 'json') { | |
| document.querySelector('.tabs .tab:first-child').classList.add('active'); | |
| document.getElementById('dvTabJson').classList.add('active'); | |
| dvStressFromFile = null; | |
| } else { | |
| document.querySelector('.tabs .tab:last-child').classList.add('active'); | |
| document.getElementById('dvTabFile').classList.add('active'); | |
| } | |
| } | |
| function parseStressFile(content) { | |
| const lines = content.trim().split('\n'); | |
| const data = []; | |
| for (let line of lines) { | |
| line = line.trim(); | |
| if (!line || line.startsWith('#') || line.startsWith('//') || line.match(/^[a-zA-Z]/)) continue; | |
| let values; | |
| if (line.includes(',')) values = line.split(','); | |
| else if (line.includes(';')) values = line.split(';'); | |
| else if (line.includes('\t')) values = line.split('\t'); | |
| else values = line.split(/\s+/); | |
| const nums = values.map(v => parseFloat(v.trim())).filter(n => !isNaN(n)); | |
| if (nums.length >= 6) data.push(nums.slice(0, 6)); | |
| } | |
| return data; | |
| } | |
| document.getElementById('dvFile').addEventListener('change', async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| try { | |
| const content = await file.text(); | |
| const parsed = parseStressFile(content); | |
| if (parsed.length === 0) { | |
| alert('Aucune donnee valide trouvee'); | |
| return; | |
| } | |
| dvStressFromFile = parsed; | |
| document.getElementById('dvFilePreview').classList.remove('hidden'); | |
| document.getElementById('dvFileCount').textContent = parsed.length; | |
| document.getElementById('dvFileData').value = parsed.slice(0, 5).map(r => r.map(v => v.toFixed(1)).join(', ')).join('\n') + (parsed.length > 5 ? '\n...' : ''); | |
| } catch (err) { | |
| alert('Erreur lecture: ' + err.message); | |
| } | |
| }); | |
| async function calculateDangVan() { | |
| const btn = document.getElementById('dvBtn'); | |
| const errDiv = document.getElementById('dvError'); | |
| errDiv.classList.add('hidden'); | |
| btn.disabled = true; | |
| btn.textContent = 'Calcul en cours...'; | |
| let stress; | |
| const jsonTab = document.getElementById('dvTabJson').classList.contains('active'); | |
| if (jsonTab) { | |
| try { | |
| const raw = document.getElementById('dvStress').value.trim(); | |
| if (!raw) throw new Error('Entrez des donnees JSON'); | |
| stress = JSON.parse(raw); | |
| } catch (e) { | |
| errDiv.textContent = 'JSON invalide: ' + e.message; | |
| errDiv.classList.remove('hidden'); | |
| btn.disabled = false; | |
| btn.textContent = 'Calculer'; | |
| return; | |
| } | |
| } else { | |
| if (!dvStressFromFile || dvStressFromFile.length === 0) { | |
| errDiv.textContent = 'Chargez un fichier CSV/TXT valide'; | |
| errDiv.classList.remove('hidden'); | |
| btn.disabled = false; | |
| btn.textContent = 'Calculer'; | |
| return; | |
| } | |
| stress = dvStressFromFile; | |
| } | |
| const a = parseFloat(document.getElementById('dvA').value); | |
| const b = parseFloat(document.getElementById('dvB').value); | |
| try { | |
| const res = await fetch(API_URL + '/dangvan/', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ stress_series: stress, a, b }) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || 'Erreur serveur'); | |
| displayDangVanResults(data); | |
| } catch (err) { | |
| errDiv.textContent = err.message; | |
| errDiv.classList.remove('hidden'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = 'Calculer'; | |
| } | |
| } | |
| function displayDangVanResults(data) { | |
| document.getElementById('dvResults').classList.remove('hidden'); | |
| document.getElementById('dvMax').textContent = data.dv_max.toFixed(2); | |
| document.getElementById('dvMargin').textContent = data.margin.toFixed(2); | |
| document.getElementById('dvCritical').textContent = data.index_max; | |
| const safeEl = document.getElementById('dvSafe'); | |
| const interpEl = document.getElementById('dvInterpretation'); | |
| if (data.safe) { | |
| safeEl.innerHTML = '<span class="dv-safe">SECURITE OK</span>'; | |
| interpEl.innerHTML = ` | |
| <p>La piece est <strong style="color:#00c853">securisee</strong> vis-a-vis de la fatigue multiaxiale.</p> | |
| <p>Tous les points de chargement se situent sous la droite de Dang Van.</p> | |
| <p>Marge de securite: <strong>${data.margin.toFixed(2)} MPa</strong></p> | |
| `; | |
| } else { | |
| safeEl.innerHTML = '<span class="dv-unsafe">RISQUE DE FATIGUE</span>'; | |
| interpEl.innerHTML = ` | |
| <p>La piece presente un <strong style="color:#ff1744">risque de fatigue</strong>.</p> | |
| <p>Un ou plusieurs points de chargement depassent la limite de Dang Van.</p> | |
| <p>Depassement maximal: <strong>${Math.abs(data.margin).toFixed(2)} MPa</strong> au pas de temps #${data.index_max}</p> | |
| `; | |
| } | |
| updateDvChart(data); | |
| document.getElementById('dvResults').scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function updateDvChart(data) { | |
| const ctx = document.getElementById('dvChart').getContext('2d'); | |
| if (dvChart) dvChart.destroy(); | |
| const points = data.points || []; | |
| const criterionLine = data.criterion_line || []; | |
| const scatterData = points.map(p => ({ x: p.sigma_H, y: p.tau_max })); | |
| const lineData = criterionLine.map(p => ({ x: p.sigma_H, y: p.tau })); | |
| // Find safe/unsafe points | |
| const safePoints = points.filter(p => p.dv <= data.b).map(p => ({ x: p.sigma_H, y: p.tau_max })); | |
| const unsafePoints = points.filter(p => p.dv > data.b).map(p => ({ x: p.sigma_H, y: p.tau_max })); | |
| dvChart = new Chart(ctx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [ | |
| { | |
| label: 'Points securises', | |
| data: safePoints, | |
| backgroundColor: '#00c853', | |
| borderColor: '#00c853', | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| }, | |
| { | |
| label: 'Points critiques', | |
| data: unsafePoints, | |
| backgroundColor: '#ff1744', | |
| borderColor: '#ff1744', | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| }, | |
| { | |
| label: `Limite: τ = ${data.b} - ${data.a}·σH`, | |
| data: lineData, | |
| type: 'line', | |
| borderColor: '#ffab00', | |
| borderWidth: 3, | |
| borderDash: [8, 4], | |
| fill: false, | |
| pointRadius: 0 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { labels: { color: '#fff', font: { size: 12 } } }, | |
| title: { display: true, text: 'Diagramme de Dang Van (τ_max vs σ_H)', color: '#00d4ff', font: { size: 16 } } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| ticks: { color: '#888' }, | |
| grid: { color: 'rgba(255,255,255,0.1)' }, | |
| title: { display: true, text: 'Pression hydrostatique σH (MPa)', color: '#00d4ff', font: { size: 14 } } | |
| }, | |
| y: { | |
| ticks: { color: '#888' }, | |
| grid: { color: 'rgba(255,255,255,0.1)' }, | |
| title: { display: true, text: 'Cisaillement max τ_max (MPa)', color: '#00d4ff', font: { size: 14 } } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ============================================ | |
| // INIT | |
| // ============================================ | |
| console.log('SimSite loaded'); | |
| </script> | |
| </body> | |
| </html> | |
Xet Storage Details
- Size:
- 37.6 kB
- Xet hash:
- 0ab486f87a8aa7a5da99587360da5f82341daf749289d1e2795034a1936177bb
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.