Files
SPA-landing/geo-calc/index.html

2909 lines
118 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rechtwinkliges Dreieck - 3D Geometrie-Rechner</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1600px;
margin: 0 auto;
padding: 20px;
background-color: #f0f2f5;
}
h1 {
text-align: center;
color: #1a1a1a;
margin-bottom: 30px;
font-size: 2em;
}
.main-container {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
margin-bottom: 20px;
}
.left-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.right-panel {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
}
h2 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007acc;
padding-bottom: 8px;
font-size: 1.2em;
}
h3 {
color: #555;
font-size: 1em;
margin-top: 15px;
margin-bottom: 10px;
background: #f5f5f5;
padding: 5px 10px;
border-radius: 4px;
}
.input-group {
margin: 10px 0;
display: grid;
grid-template-columns: 120px 1fr 40px;
gap: 8px;
align-items: center;
}
label {
font-weight: 500;
color: #555;
font-size: 0.9em;
}
input {
padding: 6px 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}
input:focus {
outline: none;
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
input:disabled {
background-color: #f5f5f5;
color: #999;
}
select {
padding: 6px 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
}
select:focus {
outline: none;
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
.unit {
color: #777;
font-size: 0.85em;
text-align: center;
}
.ratio-input {
display: flex;
gap: 5px;
align-items: center;
}
.ratio-input input {
text-align: center;
width: 60px;
}
.ratio-separator {
color: #555;
flex-shrink: 0;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
button {
background-color: #007acc;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
button:hover {
background-color: #005a9e;
}
.clear-button {
background-color: #dc3545;
}
.clear-button:hover {
background-color: #c82333;
}
.export-button {
background-color: #28a745;
}
.export-button:hover {
background-color: #218838;
}
/* Plotly Container */
#plot3d {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Koordinaten-Tabelle */
.coordinates-table {
margin-top: 20px;
}
.coord-table {
width: 100%;
border-collapse: collapse;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
.coord-table th,
.coord-table td {
padding: 8px;
text-align: left;
border: 1px solid #ddd;
}
.coord-table th {
background: #f5f5f5;
font-weight: 600;
position: sticky;
top: 0;
}
.coord-table tr:nth-child(even) {
background: #fafafa;
}
.coord-table tr:hover {
background: #e8f4ff;
}
/* Kaskadenrechner Tabelle */
#cascade-results table {
width: 100%;
border-collapse: collapse;
}
#cascade-results th,
#cascade-results td {
padding: 8px;
text-align: center;
border: 1px solid #ddd;
}
#cascade-results th {
background: #f5f5f5;
font-weight: 600;
}
#cascade-results tr:hover {
background: #e8f4ff;
cursor: pointer;
}
.add-mulde-placeholder {
text-align: center;
padding: 8px;
border: 2px dashed #ccc;
border-radius: 4px;
cursor: pointer;
color: #666;
font-size: 0.85em;
transition: all 0.2s ease-in-out;
}
.add-mulde-placeholder:hover {
background: #e8f4ff;
border-color: #007acc;
color: #007acc;
}
.dark-mode .add-mulde-placeholder {
border-color: #555;
color: #999;
}
.dark-mode .add-mulde-placeholder:hover {
background: #3a3a3a;
border-color: #0096ff;
color: #0096ff;
}
.point-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.color-main { background-color: #007acc; }
.color-inner { background-color: #28a745; }
.color-trapez { background-color: #dc3545; }
.color-extended { background-color: #6f42c1; }
.color-rect { background-color: #ffc107; }
/* Ergebnisse */
.result-grid {
display: grid;
gap: 10px;
}
.result-group {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.result-group h4 {
margin: 0 0 8px 0;
color: #495057;
font-size: 0.9em;
font-weight: 600;
}
.result {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 0.85em;
}
.result-label {
color: #666;
}
.result-value {
font-weight: 600;
color: #007acc;
font-family: 'Consolas', 'Monaco', monospace;
}
/* Converter - Kompakter */
.converter-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.converter-section h2 {
margin-bottom: 15px;
}
.converter-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 10px;
}
.converter-main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.converter-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.converter-item label {
font-size: 0.8em;
margin: 0;
}
.converter-item input {
padding: 5px 8px;
font-size: 13px;
}
.converter-ratio {
display: flex;
gap: 3px;
align-items: center;
}
.converter-ratio input {
text-align: center;
padding: 5px 4px;
width: 100%;
}
.converter-ratio .separator {
font-size: 0.9em;
}
.multiplier-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.multiplier-title {
margin-top: 0 !important;
margin-bottom: 10px !important;
font-size: 0.9em !important;
background: none !important;
padding: 0 !important;
border: none !important;
font-weight: 600;
}
.multiplier-grid {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1.2fr auto;
gap: 8px;
align-items: center;
}
.multiplier-grid input {
width: 100%;
}
.multiplier-grid button {
padding: 5px 10px;
justify-self: start;
}
.multiplier-op {
font-weight: 500;
font-size: 1.1em;
text-align: center;
}
.bottom-layout-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-top: 20px;
align-items: start;
}
#coordinates-section {
grid-column: 1;
}
.converter-section {
grid-column: 2;
}
/* Dialog */
.dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 999;
}
.dialog {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 1000;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.dialog h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007acc;
padding-bottom: 10px;
}
.dialog textarea {
width: 100%;
height: 400px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
white-space: pre;
}
/* Plot Controls */
.plot-controls {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
font-size: 0.85em;
}
.plot-controls label {
margin: 0;
display: flex;
align-items: center;
gap: 5px;
font-weight: normal;
}
.plot-controls input[type="checkbox"] {
width: auto;
}
/* Dark Mode Styles */
body.dark-mode {
background-color: #1a1a1a;
color: #e0e0e0;
}
.dark-mode .section {
background: #2a2a2a;
border-color: #404040;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.dark-mode h1, .dark-mode h2 {
color: #e0e0e0;
}
.dark-mode h3 {
background: #333;
color: #e0e0e0;
}
.dark-mode label {
color: #b0b0b0;
}
.dark-mode input {
background: #333;
color: #e0e0e0;
border-color: #555;
}
.dark-mode input:disabled {
background-color: #252525;
color: #666;
}
.dark-mode select {
background: #333;
color: #e0e0e0;
border-color: #555;
}
.dark-mode .unit {
color: #999;
}
.dark-mode .result-group {
background: #333;
border-color: #404040;
}
.dark-mode .converter-section {
background: #333;
border-color: #404040;
}
.dark-mode .plot-controls {
background: #333;
}
.dark-mode .coord-table th {
background: #333;
}
.dark-mode .coord-table tr:nth-child(even) {
background: #2a2a2a;
}
.dark-mode .coord-table tr:hover {
background: #3a3a3a;
}
/* Dark Mode Toggle */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: #007acc;
color: white;
border: none;
padding: 8px 12px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
z-index: 100;
}
.theme-toggle:hover {
background: #005a9e;
}
.dark-mode .theme-toggle {
background: #ffc107;
color: #333;
}
.dark-mode .theme-toggle:hover {
background: #ffb300;
}
.dark-mode .dialog {
background: #2a2a2a;
color: #e0e0e0;
}
.dark-mode .dialog h3 {
color: #e0e0e0;
}
.dark-mode .dialog textarea {
background: #333;
color: #e0e0e0;
border-color: #555;
}
/* Dark Mode für Kaskadenrechner */
.dark-mode #cascade-results,
.dark-mode #cascade-detail-content,
.dark-mode #cascade-lengths,
.dark-mode #cascade-config-container,
.dark-mode #cascade-summary-content {
background: #333;
color: #e0e0e0;
}
.dark-mode #cascade-config-container > div > div {
background: #2a2a2a;
border-color: #555;
}
.dark-mode #cascade-canvas {
background: #2a2a2a;
border-color: #555;
}
.dark-mode #cascade-results table tr:hover {
background: #3a3a3a;
}
.dark-mode .multiplier-section {
border-top-color: #404040;
}
.dark-mode .multiplier-title {
color: #b0b0b0 !important;
}
.dark-mode .multiplier-op,
.dark-mode .converter-ratio .separator {
color: #b0b0b0;
}
@media (max-width: 1200px) {
.main-container {
grid-template-columns: 1fr;
}
.right-panel > div:first-child {
grid-template-columns: 1fr;
}
.converter-grid {
grid-template-columns: repeat(2, 1fr);
}
#cascade-calculator > div {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.bottom-layout-grid {
grid-template-columns: 1fr;
}
.bottom-layout-grid > .section {
grid-column: 1;
}
}
</style>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
</head>
<body>
<button class="theme-toggle" onclick="toggleDarkMode()">🌓</button>
<h1>Rechtwinkliges Dreieck - 3D Geometrie-Rechner</h1>
<div class="main-container">
<!-- Linke Spalte: Eingabe -->
<div class="left-panel">
<div class="section">
<h2>Eingabe-Parameter</h2>
<h3>Hauptwinkel β (BN_Berg)</h3>
<div class="input-group">
<label for="beta-degrees">β (Grad)</label>
<input type="number" id="beta-degrees" onchange="convertBeta(this.id); calculate()" step="0.01">
<span class="unit">°</span>
</div>
<div class="input-group">
<label for="beta-percent">β (Prozent)</label>
<input type="number" id="beta-percent" onchange="convertBeta(this.id); calculate()" step="0.01">
<span class="unit">%</span>
</div>
<div class="input-group">
<label for="beta-ratio">β (Verhältnis)</label>
<div class="ratio-input">
<input type="number" id="beta-ratio-left" value="1" onchange="convertBeta('beta-ratio'); calculate()" step="0.01">
<span class="ratio-separator">:</span>
<input type="number" id="beta-ratio-right" onchange="convertBeta('beta-ratio'); calculate()" step="0.001">
</div>
<span class="unit"></span>
</div>
<h3>Abstand BS2 (SHmax)</h3>
<div class="input-group">
<label for="bs2">Vertikaler Abstand</label>
<input type="number" id="bs2" step="0.001" onchange="calculate()">
<span class="unit">m</span>
</div>
<h3>Innerer Winkel (Straßenneigung)</h3>
<div class="input-group">
<label for="zeta-degrees">ζ (Grad)</label>
<input type="number" id="zeta-degrees" onchange="convertZeta(this.id); calculate()" step="0.01">
<span class="unit">°</span>
</div>
<div class="input-group">
<label for="zeta-percent">ζ (Prozent)</label>
<input type="number" id="zeta-percent" onchange="convertZeta(this.id); calculate()" step="0.01">
<span class="unit">%</span>
</div>
<div class="input-group">
<label for="zeta-ratio">ζ (Verhältnis)</label>
<div class="ratio-input">
<input type="number" id="zeta-ratio-left" value="1" onchange="convertZeta('zeta-ratio'); calculate()" step="0.01">
<span class="ratio-separator">:</span>
<input type="number" id="zeta-ratio-right" onchange="convertZeta('zeta-ratio'); calculate()" step="0.001">
</div>
<span class="unit"></span>
</div>
<h3>Trapez-Erweiterung (SHmin)</h3>
<div class="input-group">
<label for="trapezoid-be">Seite BE</label>
<input type="number" id="trapezoid-be" step="0.01" onchange="handleTrapezoidInput(this.id); calculate()">
<span class="unit">m</span>
</div>
<div class="input-group">
<label for="trapezoid-ef">Seite EF</label>
<input type="number" id="trapezoid-ef" step="0.01" onchange="handleTrapezoidInput(this.id); calculate()">
<span class="unit">m</span>
</div>
<div class="input-group">
<label for="trapezoid-s2f">Seite S2F</label>
<input type="number" id="trapezoid-s2f" step="0.01" onchange="handleTrapezoidInput(this.id); calculate()">
<span class="unit">m</span>
</div>
<h3>Erweiterte Konstruktion (BN_Tal)</h3>
<div class="input-group">
<label for="angle-e-degrees">Winkel E (Grad)</label>
<input type="number" id="angle-e-degrees" onchange="convertAngleE(this.id); calculate()" step="0.01">
<span class="unit">°</span>
</div>
<div class="input-group">
<label for="angle-e-percent">Winkel E (Prozent)</label>
<input type="number" id="angle-e-percent" onchange="convertAngleE(this.id); calculate()" step="0.01">
<span class="unit">%</span>
</div>
<div class="input-group">
<label for="angle-e-ratio">Winkel E (Verhältnis)</label>
<div class="ratio-input">
<input type="number" id="angle-e-ratio-left" value="1" onchange="convertAngleE('angle-e-ratio'); calculate()" step="0.01">
<span class="ratio-separator">:</span>
<input type="number" id="angle-e-ratio-right" onchange="convertAngleE('angle-e-ratio'); calculate()" step="0.001">
</div>
<span class="unit"></span>
</div>
<div class="input-group">
<label for="distance-ag">Strecke AG</label>
<input type="number" id="distance-ag" step="0.01" onchange="handleExtendedInput(this.id); calculate()">
<span class="unit">m</span>
</div>
<div class="button-group">
<button onclick="calculate()">Berechnen</button>
<button class="clear-button" onclick="clearAll()">Zurücksetzen</button>
<button class="export-button" onclick="exportToRhino()">Rhino Export</button>
</div>
</div>
</div>
<!-- Rechte Spalte: Visualisierung und Ergebnisse -->
<div class="right-panel">
<div style="display: grid; grid-template-columns: 1fr 400px; gap: 20px;">
<!-- 3D Visualisierung -->
<div class="section">
<h2>3D Geometrie-Visualisierung</h2>
<div class="plot-controls">
<label><input type="checkbox" id="showGrid" checked onchange="updatePlot()"> Gitter</label>
<label><input type="checkbox" id="showLabels" checked onchange="updatePlot()"> Beschriftungen</label>
<label><input type="checkbox" id="showTriangle" checked onchange="updatePlot()"> Hauptdreieck</label>
<label><input type="checkbox" id="showRect" checked onchange="updatePlot()"> Rechteck</label>
<label><input type="checkbox" id="showInner" checked onchange="updatePlot()"> Innere Konstruktion</label>
<label><input type="checkbox" id="showTrapez" checked onchange="updatePlot()"> Trapez</label>
<label><input type="checkbox" id="showExtended" checked onchange="updatePlot()"> Erweitert</label>
</div>
<div id="plot3d"></div>
</div>
<!-- Ergebnisse -->
<div class="section">
<h2>Berechnungsergebnisse</h2>
<div class="result-grid">
<div class="result-group">
<h4>Berechnete Grundwerte</h4>
<div class="result">
<span class="result-label">Seite a (BC):</span>
<span class="result-value" id="result-calc-a">-</span>
</div>
<div class="result">
<span class="result-label">Seite b (AC):</span>
<span class="result-value" id="result-calc-b">-</span>
</div>
<div class="result">
<span class="result-label">Seite c (AB):</span>
<span class="result-value" id="result-calc-c">-</span>
</div>
<div class="result">
<span class="result-label">Winkel α:</span>
<span class="result-value" id="result-calc-alpha">-</span>
</div>
</div>
<div class="result-group">
<h4>Hauptdreieck ABC</h4>
<div class="result">
<span class="result-label">Fläche:</span>
<span class="result-value" id="result-area">-</span>
</div>
<div class="result">
<span class="result-label">Umfang:</span>
<span class="result-value" id="result-perimeter">-</span>
</div>
<div class="result">
<span class="result-label">Höhe h_c:</span>
<span class="result-value" id="result-hc">-</span>
</div>
</div>
<div id="inner-results" class="result-group" style="display: none;">
<h4>Inneres Dreieck AS1S2</h4>
<div class="result">
<span class="result-label">Hypotenuse AS2:</span>
<span class="result-value" id="result-inner-f">-</span>
</div>
<div class="result">
<span class="result-label">Winkel ε:</span>
<span class="result-value" id="result-inner-epsilon">-</span>
</div>
<div class="result">
<span class="result-label">Winkel ζ:</span>
<span class="result-value" id="result-inner-zeta">-</span>
</div>
</div>
<div id="trapezoid-results" class="result-group" style="display: none;">
<h4>Trapez S2FEB</h4>
<div class="result">
<span class="result-label">Fläche:</span>
<span class="result-value" id="result-trapezoid-area">-</span>
</div>
<div class="result">
<span class="result-label">Seite S2F:</span>
<span class="result-value" id="result-trapezoid-fs2">-</span>
</div>
</div>
<div id="extended-results" class="result-group" style="display: none;">
<h4>Erweiterte Konstruktion</h4>
<div class="result">
<span class="result-label">Strecke AG:</span>
<span class="result-value" id="result-extended-ag">-</span>
</div>
<div class="result">
<span class="result-label">Seite EG:</span>
<span class="result-value" id="result-extended-eg">-</span>
</div>
<div class="result">
<span class="result-label">Seite GH:</span>
<span class="result-value" id="result-extended-gh">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Kaskadenrechner -->
<div class="section" id="cascade-calculator">
<h2>Kaskadenrechner - Muldenaufteilung</h2>
<div style="display: grid; grid-template-columns: 350px 1fr; gap: 20px;">
<!-- Linke Spalte: Grundparameter -->
<div>
<h3>Grundparameter</h3>
<div class="input-group">
<label for="cascade-total-length">Gesamtlänge L</label>
<input type="number" id="cascade-total-length" step="0.1" onchange="updateCascadePreview()">
<span class="unit">m</span>
</div>
<div class="input-group">
<label for="cascade-bankett-width">Bankettbreite</label>
<input type="number" id="cascade-bankett-width" step="0.1" value="1.0" onchange="updateCascadePreview()">
<span class="unit">m</span>
</div>
<h3>Berechnete Muldenlängen</h3>
<div id="cascade-lengths" style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 0.85em;">
<div class="result">
<span class="result-label">Typ AG (AS2+S2F+FG):</span>
<span class="result-value" id="cascade-length-ag">-</span>
</div>
<div class="result">
<span class="result-label">Typ S2F (vertikal):</span>
<span class="result-value" id="cascade-length-s2f">-</span>
</div>
<div class="result">
<span class="result-label">Typ K1 (AS2+S2F):</span>
<span class="result-value" id="cascade-length-k1">-</span>
</div>
<div class="result">
<span class="result-label">Typ K2 (S2F+FG):</span>
<span class="result-value" id="cascade-length-k2">-</span>
</div>
<div class="result" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #ddd;">
<span class="result-label">EF (aktuell):</span>
<span class="result-value" id="cascade-ef-current" style="color: #28a745;">-</span>
</div>
</div>
<h3>Schnellkonfiguration</h3>
<div style="margin: 10px 0;">
<label style="display: flex; align-items: center; gap: 5px; margin-bottom: 10px;">
<input type="checkbox" id="cascade-bankett-start" checked onchange="updateCascadePreview()">
Bankett am Anfang
</label>
<label style="display: flex; align-items: center; gap: 5px; margin-bottom: 10px;">
<input type="checkbox" id="cascade-bankett-end" checked onchange="updateCascadePreview()">
Bankett am Ende
</label>
<div style="margin-top: 10px;">
<label style="display: block; margin-bottom: 5px;">Standard-Muldentyp:</label>
<select id="cascade-default-type" style="width: 100%; padding: 6px;">
<option value="AG">Typ AG: AS2 + S2F + FG</option>
<option value="S2F">Typ S2F: Nur S2F (vertikal)</option>
<option value="K1">Typ K1: AS2 + S2F</option>
<option value="K2">Typ K2: S2F + FG</option>
</select>
</div>
</div>
</div>
<!-- Rechte Spalte: Muldenkonfiguration -->
<div>
<h3>Muldenkonfiguration</h3>
<div id="cascade-config-container" style="max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 4px;">
</div>
<div style="margin-top: 15px; display: flex; gap: 10px;">
<button onclick="autoFillMulden()">Automatisch auffüllen</button>
<button onclick="clearAllMulden()" class="clear-button">Alle entfernen</button>
<button onclick="exportCascadeConfig()" class="export-button">Konfiguration exportieren</button>
</div>
</div>
</div>
<!-- Visuelle Darstellung -->
<div id="cascade-visual" style="margin-top: 20px;">
<h3>Kaskadenvisualisierung</h3>
<canvas id="cascade-canvas" width="1200" height="250" style="width: 100%; border: 1px solid #ddd; border-radius: 4px; background: white;"></canvas>
</div>
<!-- Ergebnistabelle -->
<div id="cascade-summary" style="margin-top: 20px; display: none;">
<h3>Zusammenfassung</h3>
<div id="cascade-summary-content" style="background: #f8f9fa; padding: 15px; border-radius: 4px;">
</div>
<div id="cascade-suggestion" style="margin-top: 15px; background: #e3f2fd; padding: 15px; border-radius: 4px; display: none; border: 1px solid #bde0fe;">
<h4 style="margin-top: 0; color: #0c5460; font-size: 0.9em; font-weight: 600;">Optimierungsvorschlag</h4>
<p id="suggestion-text" style="font-size: 0.85em; margin-bottom: 10px;"></p>
<div style="font-size: 0.85em;">
<div class="result">
<span class="result-label">Ziel-Wert für S2F:</span>
<span class="result-value" id="suggestion-s2f" style="color: #dc3545;">-</span>
</div>
<div class="result">
<span class="result-label">Ziel-Wert für EF:</span>
<span class="result-value" id="suggestion-ef" style="color: #dc3545;">-</span>
</div>
</div>
<div class="button-group" style="margin-top: 10px; justify-content: flex-start;">
<button onclick="applySuggestion('s2f')" style="background-color: #28a745; font-size: 12px; padding: 5px 10px;">S2F anwenden</button>
<button onclick="applySuggestion('ef')" style="background-color: #28a745; font-size: 12px; padding: 5px 10px;">EF anwenden</button>
</div>
</div>
<div id="cascade-distribute-suggestion" style="margin-top: 15px; background: #d4edda; padding: 15px; border-radius: 4px; display: none; border: 1px solid #c3e6cb;">
<h4 style="margin-top: 0; color: #155724; font-size: 0.9em; font-weight: 600;">Differenz verteilen</h4>
<p id="distribute-suggestion-text" style="font-size: 0.85em; margin-bottom: 10px;"></p>
<div style="font-size: 0.85em;">
<div class="result">
<span class="result-label">Neuer Ziel-Wert für S2F:</span>
<span class="result-value" id="distribute-suggestion-s2f" style="color: #dc3545;">-</span>
</div>
<div class="result">
<span class="result-label">Daraus resultierender EF:</span>
<span class="result-value" id="distribute-suggestion-ef" style="color: #dc3545;">-</span>
</div>
</div>
<div class="button-group" style="margin-top: 10px; justify-content: flex-start;">
<button onclick="applyDistributeSuggestion('s2f')" style="background-color: #28a745; font-size: 12px; padding: 5px 10px;">S2F anwenden</button>
</div>
</div>
</div>
</div>
<div class="bottom-layout-grid">
<!-- Koordinaten-Tabelle -->
<div class="section" id="coordinates-section" style="display: none;">
<h2>Punktkoordinaten (A = Ursprung)</h2>
<div class="coordinates-table">
<table class="coord-table">
<thead>
<tr>
<th>Punkt</th>
<th>X</th>
<th>Y</th>
<th>Z</th>
<th>Gruppe</th>
</tr>
</thead>
<tbody id="coord-tbody">
</tbody>
</table>
<div class="button-group" style="margin-top: 10px;">
<button onclick="copyCoordinates()">Koordinaten kopieren</button>
<button onclick="copyAsCSV()">Als CSV kopieren</button>
</div>
</div>
</div>
<!-- Umrechner - Kompakter -->
<div class="section converter-section">
<h2>Neigungs-Umrechner</h2>
<div class="converter-main-grid">
<div class="converter-item">
<label>Prozent</label>
<input type="number" id="conv-percent" onchange="convertSlope(this.id)" step="0.01">
</div>
<div class="converter-item">
<label>Grad</label>
<input type="number" id="conv-degrees" onchange="convertSlope(this.id)" step="0.01">
</div>
<div class="converter-item">
<label>Verhältnis</label>
<div class="converter-ratio">
<input type="number" id="conv-ratio-left" value="1" onchange="convertSlope('conv-ratio')" step="0.1">
<span class="separator">:</span>
<input type="number" id="conv-ratio-right" onchange="convertSlope('conv-ratio')" step="0.01">
</div>
</div>
<div class="converter-item">
<label>Faktor</label>
<input type="number" id="conv-factor" disabled step="0.000001">
</div>
</div>
<div class="multiplier-section">
<h3 class="multiplier-title">Multiplikator</h3>
<div class="multiplier-grid">
<input type="number" id="mult-input" placeholder="Wert" step="0.01" onchange="calculateMultiplier()">
<span class="multiplier-op">×</span>
<input type="number" id="mult-ratio" placeholder="Verhältnis" step="0.01" onchange="calculateMultiplier()">
<span class="multiplier-op">=</span>
<input type="number" id="mult-result" disabled>
<button onclick="calculateMultiplier()">Berechnen</button>
</div>
</div>
</div>
</div>
<!-- Rhino Export Dialog -->
<div class="dialog-overlay" id="dialog-overlay" onclick="closeRhinoExport()"></div>
<div class="dialog" id="rhino-dialog">
<h3>Rhino Python Script Export</h3>
<textarea id="rhino-script"></textarea>
<div class="button-group">
<button onclick="copyRhinoScript()">Script kopieren</button>
<button onclick="downloadRhinoScript()">Als Datei speichern</button>
<button onclick="closeRhinoExport()">Schließen</button>
</div>
</div>
<script>
// Konstanten
const DEFAULTS = {
beta_ratio_right: 2.5,
bs2: 0.35,
zeta_percent: 10,
trapezoid_ef: 0.2,
angle_e_degrees: 78.2
};
// Globale Variablen
let currentResults = null;
let currentCameraPosition = null;
let lastModifiedInput = null;
// Hilfsfunktionen
const toRadians = (deg) => deg * Math.PI / 180;
const toDegrees = (rad) => rad * 180 / Math.PI;
// Hauptberechnung
function calculate() {
// Steuergrößen einlesen
let beta = parseFloat(document.getElementById('beta-degrees').value) || null;
let bs2 = parseFloat(document.getElementById('bs2').value) || null;
let zeta = parseFloat(document.getElementById('zeta-degrees').value) || null;
let be_input = parseFloat(document.getElementById('trapezoid-be').value) || null;
let ef_input = parseFloat(document.getElementById('trapezoid-ef').value) || null;
let s2f_input = parseFloat(document.getElementById('trapezoid-s2f').value) || null;
let angleE = parseFloat(document.getElementById('angle-e-degrees').value) || null;
let ag_input = parseFloat(document.getElementById('distance-ag').value) || null;
// Validierung
if (beta === null || bs2 === null || zeta === null) {
// alert("Bitte geben Sie Werte für β, BS2 und ζ an.");
return;
}
// Epsilon aus Zeta berechnen
if (zeta <= 0 || zeta >= 90) {
alert("Der Winkel ζ muss zwischen 0° und 90° liegen.");
return;
}
const epsilon = 90 - zeta;
if (bs2 <= 0) {
alert("Der Abstand BS2 muss positiv sein.");
return;
}
if (beta <= 0 || beta >= 90) {
alert("Der Winkel β muss zwischen 0° und 90° liegen.");
return;
}
// Grunddreieck berechnen
const tanBeta = Math.tan(toRadians(beta));
const tanEpsilon = Math.tan(toRadians(epsilon));
const denominator = tanBeta * tanEpsilon - 1;
if (denominator <= 0) {
alert("Diese Kombination von β und ε ist geometrisch nicht möglich.");
return;
}
const a = (bs2 * tanEpsilon) / denominator;
const b = a * tanBeta;
// Dreieck lösen
const triangle = solveTriangle({ a, b });
if (triangle) {
// Ergebnisse erweitern
const results = { ...triangle };
results.area = (results.a * results.b) / 2;
results.perimeter = results.a + results.b + results.c;
results.hc = (results.a * results.b) / results.c;
results.p = (results.b**2) / results.c;
results.q = (results.a**2) / results.c;
results.bs2 = bs2;
results.epsilon = epsilon;
// Inneres Dreieck
const h_as1 = bs2;
const h_cs1 = results.b - bs2;
results.innerTriangle = {
h_cs1: h_cs1,
h_as1: h_as1,
a: results.a,
f: Math.sqrt(h_as1**2 + results.a**2),
epsilon: results.epsilon,
zeta: 90 - results.epsilon
};
// Alle Punkte berechnen
const A = { x: 0, y: 0, z: 0 };
const B = { x: results.a, y: 0, z: -results.b };
const C = { x: 0, y: 0, z: -results.b };
const D = { x: results.a, y: 0, z: 0 };
const S1 = { x: 0, y: 0, z: -results.b + bs2 };
const S2 = { x: results.a, y: 0, z: -results.b + bs2 };
results.points = { A, B, C, D, S1, S2 };
// Trapezoid berechnen wenn Eingabe vorhanden
if (be_input !== null || ef_input !== null || ag_input !== null || s2f_input !== null) {
const m_as2 = (S2.z - A.z) / (S2.x - A.x);
let E, F, be_len, ef_len, s2f_len;
// Bestimme Priorität basierend auf lastModifiedInput
let useBE = false, useEF = false, useAG = false, useS2F = false;
if (lastModifiedInput === 'trapezoid-be' && be_input !== null) {
useBE = true;
} else if (lastModifiedInput === 'trapezoid-ef' && ef_input !== null) {
useEF = true;
} else if (lastModifiedInput === 'trapezoid-s2f' && s2f_input !== null) {
useS2F = true;
} else if (lastModifiedInput === 'distance-ag' && ag_input !== null) {
useAG = true;
} else {
// Fallback: Priorität BE > AG > EF > S2F
if (be_input !== null) useBE = true;
else if (ag_input !== null) useAG = true;
else if (ef_input !== null) useEF = true;
else if (s2f_input !== null) useS2F = true;
}
if (useBE) {
be_len = be_input;
const Ex = B.x + be_len;
E = { x: Ex, y: 0, z: B.z };
const Fx = Ex;
const Fz = m_as2 * Fx;
F = { x: Fx, y: 0, z: Fz };
ef_len = Math.abs(F.z - E.z);
} else if (useAG && angleE !== null) {
// G liegt auf der Linie AS2 (oder deren Verlängerung) mit Abstand AG von A
const as2_length = results.innerTriangle.f;
// Berechne G auf AS2 mit Abstand AG von A
const t = ag_input / as2_length;
const G = {
x: A.x + t * (S2.x - A.x),
y: 0,
z: A.z + t * (S2.z - A.z)
};
// E liegt auf Höhe B.z, berechne E.x aus Winkel bei E
const angleERad = toRadians(angleE);
const E_z = B.z;
// tan(angleE) = (G.z - E.z) / (G.x - E.x)
// E.x = G.x - (G.z - E.z) / tan(angleE)
const E_x = G.x - (G.z - E_z) / Math.tan(angleERad);
// Prüfe ob E rechts von B liegt
if (E_x <= B.x) {
alert("Mit dieser Kombination von AG und Winkel bei E würde E links von B liegen, was nicht zulässig ist.");
return;
}
E = { x: E_x, y: 0, z: E_z };
// F ist der Schnittpunkt der Vertikalen durch E mit AS2
const Fx = E.x;
const Fz = m_as2 * Fx;
F = { x: Fx, y: 0, z: Fz };
be_len = E.x - B.x;
ef_len = Math.abs(F.z - E.z);
// Speichere G für erweiterte Konstruktion
results.points.G = G;
} else if (useEF) {
ef_len = ef_input;
const x1 = (B.z + ef_len) / m_as2;
const x2 = (B.z - ef_len) / m_as2;
let Ex;
if (x1 > B.x) {
Ex = x1;
F = { x: Ex, y: 0, z: B.z + ef_len };
} else if (x2 > B.x) {
Ex = x2;
F = { x: Ex, y: 0, z: B.z - ef_len };
} else {
Ex = B.x + 0.1;
F = { x: Ex, y: 0, z: m_as2 * Ex };
}
E = { x: Ex, y: 0, z: B.z };
be_len = E.x - B.x;
} else if (useS2F) {
s2f_len = s2f_input;
// F.x aus der Länge S2F berechnen
// s2f_len^2 = (F.x - S2.x)^2 * (1 + m_as2^2)
const dx = s2f_len / Math.sqrt(1 + m_as2**2);
const Fx = S2.x + dx;
const Fz = m_as2 * Fx;
F = { x: Fx, y: 0, z: Fz };
// E ableiten
const Ex = F.x;
E = { x: Ex, y: 0, z: B.z };
be_len = E.x - B.x;
ef_len = Math.abs(F.z - E.z);
}
const side_s2b = Math.abs(S2.z - B.z);
const trapezoid_area = (side_s2b + ef_len) / 2 * be_len;
results.trapezoid = {
E: E, F: F,
be: be_len, ef: ef_len,
area: trapezoid_area,
fs2: s2f_len || Math.sqrt((F.x - S2.x)**2 + (F.z - S2.z)**2)
};
results.points.E = E;
results.points.F = F;
// Erweiterte Konstruktion
if (angleE && angleE > 0 && angleE < 180) {
if (!results.points.G) {
// G wurde noch nicht berechnet (AG war nicht "Chef")
const angleERad = toRadians(angleE);
const dx_eg = Math.cos(angleERad);
const dz_eg = Math.sin(angleERad);
const denominator = dz_eg - m_as2 * dx_eg;
if (Math.abs(denominator) > 0.0001) {
const t = (m_as2 * E.x - E.z) / denominator;
const G = {
x: E.x + t * dx_eg,
y: 0,
z: E.z + t * dz_eg
};
const H = { x: G.x, y: 0, z: E.z };
results.extended = {
G: G,
H: H,
angleE: angleE,
gh: Math.abs(G.z - H.z),
eg: Math.sqrt((G.x - E.x)**2 + (G.z - E.z)**2),
eh: Math.abs(H.x - E.x),
ag: Math.sqrt(G.x**2 + G.z**2)
};
results.points.G = G;
results.points.H = H;
}
} else {
// G wurde bereits berechnet (AG war "Chef")
const H = { x: results.points.G.x, y: 0, z: E.z };
results.extended = {
G: results.points.G,
H: H,
angleE: angleE,
gh: Math.abs(results.points.G.z - H.z),
eg: Math.sqrt((results.points.G.x - E.x)**2 + (results.points.G.z - E.z)**2),
eh: Math.abs(H.x - E.x),
ag: ag_input || Math.sqrt(results.points.G.x**2 + results.points.G.z**2)
};
results.points.H = H;
}
}
}
// Ergebnisse anzeigen
storeResults(results);
displayResults(results);
updateInputs(results);
displayCoordinatesTable(results);
createPlot(results);
// Kaskadenrechner-Längen aktualisieren
updateCascadeConfigDisplay();
updateCascadePreview();
}
}
function solveTriangle(known) {
let k = { ...known };
const MAX_ITERATIONS = 5;
for (let i = 0; i < MAX_ITERATIONS; i++) {
if (k.alpha && !k.beta) k.beta = 90 - k.alpha;
if (k.beta && !k.alpha) k.alpha = 90 - k.beta;
if (k.a && k.b && !k.c) k.c = Math.sqrt(k.a**2 + k.b**2);
if (k.a && k.c && !k.b) k.b = Math.sqrt(k.c**2 - k.a**2);
if (k.b && k.c && !k.a) k.a = Math.sqrt(k.c**2 - k.b**2);
if (k.a && k.b && !k.alpha) k.alpha = toDegrees(Math.atan(k.a / k.b));
if (k.a && k.c && !k.alpha) k.alpha = toDegrees(Math.asin(k.a / k.c));
if (k.b && k.c && !k.alpha) k.alpha = toDegrees(Math.acos(k.b / k.c));
if (k.c && k.alpha && !k.a) k.a = k.c * Math.sin(toRadians(k.alpha));
if (k.c && k.alpha && !k.b) k.b = k.c * Math.cos(toRadians(k.alpha));
if (k.a && k.alpha && !k.c) k.c = k.a / Math.sin(toRadians(k.alpha));
if (k.a && k.alpha && !k.b) k.b = k.a / Math.tan(toRadians(k.alpha));
if (k.b && k.alpha && !k.c) k.c = k.b / Math.cos(toRadians(k.alpha));
if (k.b && k.alpha && !k.a) k.a = k.b * Math.tan(toRadians(k.alpha));
if (k.a && k.b && k.c && k.alpha && k.beta) break;
}
if (k.a && k.b && k.c) {
return k;
}
return null;
}
function displayResults(res) {
document.getElementById('result-calc-a').textContent = `${res.a.toFixed(3)} m`;
document.getElementById('result-calc-b').textContent = `${res.b.toFixed(3)} m`;
document.getElementById('result-calc-c').textContent = `${res.c.toFixed(3)} m`;
document.getElementById('result-calc-alpha').textContent = `${res.alpha.toFixed(2)}°`;
document.getElementById('result-area').textContent = `${res.area.toFixed(3)}`;
document.getElementById('result-perimeter').textContent = `${res.perimeter.toFixed(3)} m`;
document.getElementById('result-hc').textContent = `${res.hc.toFixed(3)} m`;
const innerDiv = document.getElementById('inner-results');
if (res.innerTriangle) {
document.getElementById('result-inner-f').textContent = `${res.innerTriangle.f.toFixed(3)} m`;
document.getElementById('result-inner-epsilon').textContent = `${res.innerTriangle.epsilon.toFixed(2)}°`;
document.getElementById('result-inner-zeta').textContent = `${res.innerTriangle.zeta.toFixed(2)}°`;
innerDiv.style.display = 'block';
} else {
innerDiv.style.display = 'none';
}
const trapDiv = document.getElementById('trapezoid-results');
if (res.trapezoid) {
document.getElementById('result-trapezoid-area').textContent = `${res.trapezoid.area.toFixed(3)}`;
document.getElementById('result-trapezoid-fs2').textContent = `${res.trapezoid.fs2.toFixed(3)} m`;
trapDiv.style.display = 'block';
} else {
trapDiv.style.display = 'none';
}
const extDiv = document.getElementById('extended-results');
if (res.extended) {
document.getElementById('result-extended-ag').textContent = `${res.extended.ag.toFixed(3)} m`;
document.getElementById('result-extended-eg').textContent = `${res.extended.eg.toFixed(3)} m`;
document.getElementById('result-extended-gh').textContent = `${res.extended.gh.toFixed(3)} m`;
extDiv.style.display = 'block';
} else {
extDiv.style.display = 'none';
}
}
function displayCoordinatesTable(results) {
const tbody = document.getElementById('coord-tbody');
const section = document.getElementById('coordinates-section');
tbody.innerHTML = '';
const points = [
{ name: 'A', coord: results.points.A, group: 'Hauptdreieck', color: 'main' },
{ name: 'B', coord: results.points.B, group: 'Hauptdreieck', color: 'main' },
{ name: 'C', coord: results.points.C, group: 'Hauptdreieck', color: 'main' },
{ name: 'D', coord: results.points.D, group: 'Rechteck', color: 'rect' }
];
if (results.points.S1) {
points.push(
{ name: 'S1', coord: results.points.S1, group: 'Innere Konstruktion', color: 'inner' },
{ name: 'S2', coord: results.points.S2, group: 'Innere Konstruktion', color: 'inner' }
);
}
if (results.points.E) {
points.push(
{ name: 'E', coord: results.points.E, group: 'Trapez', color: 'trapez' },
{ name: 'F', coord: results.points.F, group: 'Trapez', color: 'trapez' }
);
}
if (results.points.G) {
points.push(
{ name: 'G', coord: results.points.G, group: 'Erweiterte Konstruktion', color: 'extended' },
{ name: 'H', coord: results.points.H, group: 'Erweiterte Konstruktion', color: 'extended' }
);
}
points.forEach(p => {
const row = tbody.insertRow();
row.innerHTML = `
<td><span class="point-indicator color-${p.color}"></span>${p.name}</td>
<td>${p.coord.x.toFixed(3)}</td>
<td>${p.coord.y.toFixed(3)}</td>
<td>${p.coord.z.toFixed(3)}</td>
<td>${p.group}</td>
`;
});
section.style.display = 'block';
}
function updateInputs(results) {
// Nur Grad-Feld aktualisieren, OHNE Konvertierung auszulösen
document.getElementById('zeta-degrees').value = results.innerTriangle.zeta.toFixed(2);
// Update abhängige Werte wenn sie nicht gerade aktiv eingegeben werden
const beInput = document.getElementById('trapezoid-be');
const efInput = document.getElementById('trapezoid-ef');
const s2fInput = document.getElementById('trapezoid-s2f');
const agInput = document.getElementById('distance-ag');
// Nur updaten wenn das Feld nicht im Fokus ist
if (results.trapezoid && document.activeElement !== beInput) {
beInput.value = results.trapezoid.be.toFixed(3);
}
if (results.trapezoid && document.activeElement !== efInput) {
efInput.value = results.trapezoid.ef.toFixed(3);
}
if (results.trapezoid && document.activeElement !== s2fInput) {
s2fInput.value = results.trapezoid.fs2.toFixed(3);
}
// AG kann entweder aus extended oder direkt aus G berechnet werden
if (document.activeElement !== agInput) {
if (results.extended && results.extended.ag) {
agInput.value = results.extended.ag.toFixed(3);
} else if (results.points && results.points.G) {
const ag = Math.sqrt(results.points.G.x**2 + results.points.G.z**2);
agInput.value = ag.toFixed(3);
}
}
}
// Plotly 3D Visualisierung
function createPlot(results) {
// Kamera-Position speichern falls vorhanden
const plotDiv = document.getElementById('plot3d');
if (plotDiv && plotDiv.layout && plotDiv.layout.scene && plotDiv.layout.scene.camera) {
currentCameraPosition = plotDiv.layout.scene.camera;
}
// Erst den alten Plot sauber entfernen
Plotly.purge('plot3d');
const traces = [];
const p = results.points;
// Hilfsfunktion für Linien
const createLine = (p1, p2, color, name, showlegend = false, width = 2) => ({
type: 'scatter3d',
mode: 'lines',
x: [p1.x, p2.x],
y: [p1.y, p2.y],
z: [p1.z, p2.z],
line: { color: color, width: width },
name: name,
showlegend: showlegend,
hoverinfo: 'name'
});
// Hauptdreieck ABC
if (document.getElementById('showTriangle').checked) {
traces.push({
type: 'scatter3d',
mode: 'lines',
x: [p.A.x, p.B.x, p.C.x, p.A.x],
y: [p.A.y, p.B.y, p.C.y, p.A.y],
z: [p.A.z, p.B.z, p.C.z, p.A.z],
line: { color: '#007acc', width: 4 },
name: 'Dreieck ABC',
showlegend: true
});
}
// Rechteck ABCD
if (document.getElementById('showRect').checked) {
traces.push(createLine(p.A, p.D, '#ffc107', 'AD'));
traces.push(createLine(p.B, p.D, '#ffc107', 'BD'));
traces.push(createLine(p.C, p.D, '#ffc107', 'CD'));
}
// Innere Konstruktion
if (document.getElementById('showInner').checked && p.S1 && p.S2) {
traces.push(createLine(p.A, p.S2, '#28a745', 'AS2', true, 3));
traces.push(createLine(p.S1, p.S2, '#28a745', 'S1S2'));
traces.push(createLine(p.B, p.S2, '#28a745', 'BS2'));
traces.push(createLine(p.S2, p.D, '#28a745', 'S2D'));
}
// Trapez
if (document.getElementById('showTrapez').checked && p.E && p.F) {
traces.push(createLine(p.B, p.E, '#dc3545', 'BE'));
traces.push(createLine(p.E, p.F, '#dc3545', 'EF'));
traces.push(createLine(p.S2, p.F, '#dc3545', 'S2F'));
}
// Erweiterte Konstruktion
if (document.getElementById('showExtended').checked && p.G && p.H) {
traces.push(createLine(p.E, p.G, '#6f42c1', 'EG', true, 4));
traces.push(createLine(p.G, p.H, '#6f42c1', 'GH'));
traces.push(createLine(p.F, p.G, '#6f42c1', 'FG'));
traces.push(createLine(p.A, p.G, '#ff6b6b', 'AG', true, 3));
traces.push({
type: 'scatter3d',
mode: 'lines',
x: [p.E.x, p.F.x, p.G.x, p.H.x, p.E.x],
y: [p.E.y, p.F.y, p.G.y, p.H.y, p.E.y],
z: [p.E.z, p.F.z, p.G.z, p.H.z, p.E.z],
line: { color: '#6f42c1', width: 2 },
name: 'EFGH',
showlegend: false
});
}
// Punkte hinzufügen
const pointColors = {
A: '#007acc', B: '#007acc', C: '#007acc', D: '#ffc107',
S1: '#28a745', S2: '#28a745',
E: '#dc3545', F: '#dc3545',
G: '#6f42c1', H: '#6f42c1'
};
const pointData = {
x: [], y: [], z: [], text: [], colors: []
};
Object.entries(p).forEach(([name, coord]) => {
pointData.x.push(coord.x);
pointData.y.push(coord.y);
pointData.z.push(coord.z);
pointData.text.push(name);
pointData.colors.push(pointColors[name] || '#000000');
});
traces.push({
type: 'scatter3d',
mode: 'markers+text',
x: pointData.x,
y: pointData.y,
z: pointData.z,
text: pointData.text,
textposition: 'top center',
textfont: { size: 14, color: '#333' },
marker: {
size: 8,
color: pointData.colors
},
name: 'Punkte',
showlegend: false,
hoverinfo: 'text',
hovertext: pointData.text.map((name, i) =>
`${name}: (${pointData.x[i].toFixed(3)}, ${pointData.y[i].toFixed(3)}, ${pointData.z[i].toFixed(3)})`)
});
// Layout
const isDarkMode = document.body.classList.contains('dark-mode');
const layout = {
title: {
text: 'Geometrische Konstruktion',
font: { size: 18, color: isDarkMode ? '#e0e0e0' : '#333' }
},
paper_bgcolor: isDarkMode ? '#2a2a2a' : 'white',
plot_bgcolor: isDarkMode ? '#2a2a2a' : 'white',
scene: {
aspectmode: 'data',
xaxis: {
title: 'X',
showspikes: false,
showgrid: document.getElementById('showGrid').checked,
gridcolor: isDarkMode ? '#444' : '#ddd',
color: isDarkMode ? '#e0e0e0' : '#333'
},
yaxis: {
title: 'Y',
showspikes: false,
showgrid: document.getElementById('showGrid').checked,
gridcolor: isDarkMode ? '#444' : '#ddd',
color: isDarkMode ? '#e0e0e0' : '#333'
},
zaxis: {
title: 'Z',
showspikes: false,
showgrid: document.getElementById('showGrid').checked,
gridcolor: isDarkMode ? '#444' : '#ddd',
color: isDarkMode ? '#e0e0e0' : '#333'
},
camera: currentCameraPosition || {
eye: { x: 1.5, y: 1.5, z: 1.5 }
}
},
showlegend: true,
legend: {
x: 0,
y: 1,
bgcolor: isDarkMode ? 'rgba(42,42,42,0.8)' : 'rgba(255,255,255,0.8)',
font: { color: isDarkMode ? '#e0e0e0' : '#333' }
},
margin: { l: 0, r: 0, t: 40, b: 0 }
};
const config = {
responsive: true,
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
toImageButtonOptions: {
format: 'png',
filename: 'dreieck_3d',
height: 800,
width: 1200,
scale: 1
}
};
Plotly.newPlot('plot3d', traces, layout, config);
}
function updatePlot() {
if (currentResults) {
createPlot(currentResults);
}
}
// Hilfsfunktionen
function clearAll() {
// Reset tracking
lastModifiedInput = null;
currentCameraPosition = null;
// Inputs zurücksetzen
document.getElementById('beta-degrees').value = '';
document.getElementById('beta-percent').value = '';
document.getElementById('beta-ratio-left').value = '1';
document.getElementById('beta-ratio-right').value = '';
document.getElementById('bs2').value = DEFAULTS.bs2;
document.getElementById('zeta-degrees').value = '';
document.getElementById('zeta-percent').value = '';
document.getElementById('zeta-ratio-left').value = '1';
document.getElementById('zeta-ratio-right').value = '';
document.getElementById('trapezoid-be').value = '';
document.getElementById('trapezoid-ef').value = DEFAULTS.trapezoid_ef;
document.getElementById('trapezoid-s2f').value = '';
document.getElementById('angle-e-degrees').value = '';
document.getElementById('angle-e-percent').value = '';
document.getElementById('angle-e-ratio-left').value = '1';
document.getElementById('angle-e-ratio-right').value = '';
document.getElementById('distance-ag').value = '';
// Umrechner zurücksetzen
document.getElementById('conv-percent').value = '';
document.getElementById('conv-degrees').value = '';
document.getElementById('conv-ratio-left').value = '1';
document.getElementById('conv-ratio-right').value = '';
document.getElementById('conv-factor').value = '';
// Ergebnisse
document.querySelectorAll('.result-value').forEach(el => el.textContent = '-');
document.getElementById('inner-results').style.display = 'none';
document.getElementById('trapezoid-results').style.display = 'none';
document.getElementById('extended-results').style.display = 'none';
document.getElementById('coordinates-section').style.display = 'none';
// Plot leeren
Plotly.purge('plot3d');
}
function handleTrapezoidInput(sourceId) {
lastModifiedInput = sourceId;
}
function handleExtendedInput(sourceId) {
lastModifiedInput = sourceId;
}
function handleAngleInput(sourceId) {
// Diese Funktion wird nicht mehr benötigt, da wir nur noch Zeta haben
}
function convertBeta(sourceId) {
const degreesInput = document.getElementById('beta-degrees');
const percentInput = document.getElementById('beta-percent');
const ratioLeftInput = document.getElementById('beta-ratio-left');
const ratioRightInput = document.getElementById('beta-ratio-right');
let value;
if (sourceId === 'beta-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
const right = parseFloat(ratioRightInput.value);
if (isNaN(right)) return;
value = right / left;
sourceId = 'beta-ratio';
} else {
value = parseFloat(document.getElementById(sourceId).value);
if (isNaN(value)) return;
}
let slope = 0;
switch (sourceId) {
case 'beta-degrees':
if (value <= 0 || value >= 90) return;
slope = Math.tan(toRadians(value));
break;
case 'beta-percent':
if (value < 0) return;
slope = value / 100;
break;
case 'beta-ratio':
if (value <= 0) return;
slope = 1 / value;
break;
}
if (sourceId !== 'beta-degrees') {
degreesInput.value = toDegrees(Math.atan(slope)).toFixed(2);
}
if (sourceId !== 'beta-percent') {
percentInput.value = (slope * 100).toFixed(2);
}
if (sourceId !== 'beta-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
ratioRightInput.value = slope === 0 ? '' : (left / slope).toFixed(3);
}
}
function convertZeta(sourceId) {
const degreesInput = document.getElementById('zeta-degrees');
const percentInput = document.getElementById('zeta-percent');
const ratioLeftInput = document.getElementById('zeta-ratio-left');
const ratioRightInput = document.getElementById('zeta-ratio-right');
let value;
if (sourceId === 'zeta-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
const right = parseFloat(ratioRightInput.value);
if (isNaN(right)) return;
value = right / left;
sourceId = 'zeta-ratio';
} else {
value = parseFloat(document.getElementById(sourceId).value);
if (isNaN(value)) return;
}
let slope = 0;
switch (sourceId) {
case 'zeta-degrees':
if (value <= 0 || value >= 90) return;
slope = Math.tan(toRadians(value));
break;
case 'zeta-percent':
if (value < 0) return;
slope = value / 100;
break;
case 'zeta-ratio':
if (value <= 0) return;
slope = 1 / value;
break;
}
if (sourceId !== 'zeta-degrees') {
degreesInput.value = toDegrees(Math.atan(slope)).toFixed(2);
}
if (sourceId !== 'zeta-percent') {
percentInput.value = (slope * 100).toFixed(2);
}
if (sourceId !== 'zeta-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
ratioRightInput.value = slope === 0 ? '' : (left / slope).toFixed(3);
}
}
function convertAngleE(sourceId) {
const degreesInput = document.getElementById('angle-e-degrees');
const percentInput = document.getElementById('angle-e-percent');
const ratioLeftInput = document.getElementById('angle-e-ratio-left');
const ratioRightInput = document.getElementById('angle-e-ratio-right');
let value;
if (sourceId === 'angle-e-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
const right = parseFloat(ratioRightInput.value);
if (isNaN(right)) return;
value = right / left;
sourceId = 'angle-e-ratio';
} else {
value = parseFloat(document.getElementById(sourceId).value);
if (isNaN(value)) return;
}
let slope = 0;
switch (sourceId) {
case 'angle-e-degrees':
if (value <= 0 || value >= 90) return;
slope = Math.tan(toRadians(value));
break;
case 'angle-e-percent':
if (value < 0) return;
slope = value / 100;
break;
case 'angle-e-ratio':
if (value <= 0) return;
slope = 1 / value;
break;
}
if (sourceId !== 'angle-e-degrees') {
degreesInput.value = toDegrees(Math.atan(slope)).toFixed(2);
}
if (sourceId !== 'angle-e-percent') {
percentInput.value = (slope * 100).toFixed(2);
}
if (sourceId !== 'angle-e-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
ratioRightInput.value = slope === 0 ? '' : (left / slope).toFixed(3);
}
}
function convertSlope(sourceId) {
const percentInput = document.getElementById('conv-percent');
const degreesInput = document.getElementById('conv-degrees');
const ratioLeftInput = document.getElementById('conv-ratio-left');
const ratioRightInput = document.getElementById('conv-ratio-right');
const factorInput = document.getElementById('conv-factor');
let value;
if (sourceId === 'conv-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
const right = parseFloat(ratioRightInput.value);
if (isNaN(right)) return;
value = right / left;
sourceId = 'conv-ratio';
} else {
value = parseFloat(document.getElementById(sourceId).value);
if (isNaN(value)) return;
}
let slope = 0;
switch (sourceId) {
case 'conv-percent':
if (value < 0) return;
slope = value / 100;
break;
case 'conv-degrees':
if (value <= 0 || value >= 90) return;
slope = Math.tan(toRadians(value));
break;
case 'conv-ratio':
if (value <= 0) return;
slope = 1 / value;
break;
}
if (sourceId !== 'conv-percent') {
percentInput.value = (slope * 100).toFixed(2);
}
if (sourceId !== 'conv-degrees') {
degreesInput.value = toDegrees(Math.atan(slope)).toFixed(2);
}
if (sourceId !== 'conv-ratio') {
const left = parseFloat(ratioLeftInput.value) || 1;
ratioRightInput.value = slope === 0 ? '' : (left / slope).toFixed(3);
}
// Faktor berechnen
factorInput.value = slope.toFixed(6);
// Multiplikator-Verhältnis automatisch setzen
const multRatioInput = document.getElementById('mult-ratio');
if (sourceId === 'conv-ratio' && !isNaN(value)) {
multRatioInput.value = value.toFixed(3);
calculateMultiplier();
}
}
// Koordinaten kopieren
function copyCoordinates() {
const tbody = document.getElementById('coord-tbody');
let text = 'Punkt\tX\tY\tZ\tGruppe\n';
Array.from(tbody.rows).forEach(row => {
const cells = row.cells;
text += `${cells[0].textContent}\t${cells[1].textContent}\t${cells[2].textContent}\t${cells[3].textContent}\t${cells[4].textContent}\n`;
});
navigator.clipboard.writeText(text).then(() => {
alert('Koordinaten kopiert!');
});
}
function copyAsCSV() {
const tbody = document.getElementById('coord-tbody');
let csv = 'Punkt,X,Y,Z,Gruppe\n';
Array.from(tbody.rows).forEach(row => {
const cells = row.cells;
csv += `${cells[0].textContent},${cells[1].textContent},${cells[2].textContent},${cells[3].textContent},${cells[4].textContent}\n`;
});
navigator.clipboard.writeText(csv).then(() => {
alert('CSV-Daten kopiert!');
});
}
// Export-Funktionen
function storeResults(results) {
currentResults = results;
}
function exportToRhino() {
if (!currentResults) {
alert("Bitte zuerst berechnen!");
return;
}
const script = generateRhinoScript(currentResults);
document.getElementById('rhino-script').value = script;
document.getElementById('dialog-overlay').style.display = 'block';
document.getElementById('rhino-dialog').style.display = 'block';
}
function generateRhinoScript(results) {
const p = results.points;
let script = `import rhinoscriptsyntax as rs
# Geometrie-Export aus Dreieck-Rechner
# Datum: ${new Date().toLocaleString('de-DE')}
# === PUNKTE DEFINIEREN ===
points = {
'A': (${p.A.x.toFixed(3)}, ${p.A.y.toFixed(3)}, ${p.A.z.toFixed(3)}),
'B': (${p.B.x.toFixed(3)}, ${p.B.y.toFixed(3)}, ${p.B.z.toFixed(3)}),
'C': (${p.C.x.toFixed(3)}, ${p.C.y.toFixed(3)}, ${p.C.z.toFixed(3)}),
'D': (${p.D.x.toFixed(3)}, ${p.D.y.toFixed(3)}, ${p.D.z.toFixed(3)}),`;
if (p.S1 && p.S2) {
script += `
'S1': (${p.S1.x.toFixed(3)}, ${p.S1.y.toFixed(3)}, ${p.S1.z.toFixed(3)}),
'S2': (${p.S2.x.toFixed(3)}, ${p.S2.y.toFixed(3)}, ${p.S2.z.toFixed(3)}),`;
}
if (p.E && p.F) {
script += `
'E': (${p.E.x.toFixed(3)}, ${p.E.y.toFixed(3)}, ${p.E.z.toFixed(3)}),
'F': (${p.F.x.toFixed(3)}, ${p.F.y.toFixed(3)}, ${p.F.z.toFixed(3)}),`;
}
if (p.G && p.H) {
script += `
'G': (${p.G.x.toFixed(3)}, ${p.G.y.toFixed(3)}, ${p.G.z.toFixed(3)}),
'H': (${p.H.x.toFixed(3)}, ${p.H.y.toFixed(3)}, ${p.H.z.toFixed(3)}),`;
}
script += `
}
# === PUNKTE HINZUFÜGEN ===
for name, coords in points.items():
pt = rs.AddPoint(coords)
rs.AddTextDot(name, coords)
# === STRECKEN ZEICHNEN ===
# Hauptdreieck ABC
rs.AddPolyline([points['A'], points['B'], points['C'], points['A']])
# Rechteck ABCD
rs.AddLine(points['A'], points['D'])
rs.AddLine(points['B'], points['D'])
rs.AddLine(points['C'], points['D'])
`;
if (p.S1 && p.S2) {
script += `
# Innere Konstruktion
rs.AddLine(points['A'], points['S2']) # Hypotenuse AS2
rs.AddLine(points['S1'], points['S2']) # Basis S1S2
rs.AddLine(points['B'], points['S2']) # Vertikale BS2
rs.AddLine(points['S2'], points['D']) # Verbindung S2D
`;
}
if (p.E && p.F) {
script += `
# Trapez-Konstruktion
rs.AddLine(points['B'], points['E']) # BE
rs.AddLine(points['E'], points['F']) # EF (Höhe)
rs.AddLine(points['S2'], points['F']) # S2F
`;
}
if (p.G && p.H) {
script += `
# Erweiterte Konstruktion
rs.AddLine(points['E'], points['G']) # EG (Hauptlinie)
rs.AddLine(points['G'], points['H']) # GH
rs.AddLine(points['F'], points['G']) # FG
rs.AddLine(points['A'], points['G']) # AG
rs.AddPolyline([points['E'], points['F'], points['G'], points['H'], points['E']])
`;
}
script += `
# === LAYER ORGANISATION ===
layers = {
'Hauptdreieck': [0, 122, 204], # Blau
'Innere_Konstruktion': [40, 167, 69], # Grün
'Trapez': [220, 53, 69], # Rot
'Erweiterte_Konstruktion': [111, 66, 193] # Lila
}
for name, color in layers.items():
if not rs.IsLayer(name):
rs.AddLayer(name, color)
# === BERECHNETE WERTE ===
print("\\n=== BERECHNETE WERTE ===")
print(f"Seite a (BC) = ${results.a.toFixed(3)} m")
print(f"Seite b (AC) = ${results.b.toFixed(3)} m")
print(f"Seite c (AB) = ${results.c.toFixed(3)} m")
print(f"Winkel Alpha = ${results.alpha.toFixed(2)}°")
print(f"Winkel Beta = ${results.beta.toFixed(2)}°")
print(f"Fläche = ${results.area.toFixed(3)} m²")
`;
if (results.trapezoid) {
script += `print(f"Trapez-Fläche = ${results.trapezoid.area.toFixed(3)} m²")
`;
}
if (results.extended) {
script += `print(f"Strecke AG = ${results.extended.ag.toFixed(3)} m")
`;
}
script += `
print("\\nImport abgeschlossen!")
`;
return script;
}
function copyRhinoScript() {
const textarea = document.getElementById('rhino-script');
textarea.select();
document.execCommand('copy');
alert('Script kopiert!');
}
function downloadRhinoScript() {
const script = document.getElementById('rhino-script').value;
const blob = new Blob([script], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dreieck_geometrie_${new Date().toISOString().slice(0,10)}.py`;
a.click();
URL.revokeObjectURL(url);
}
function closeRhinoExport() {
document.getElementById('dialog-overlay').style.display = 'none';
document.getElementById('rhino-dialog').style.display = 'none';
}
// Dark Mode Toggle
function toggleDarkMode() {
document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
}
// Multiplikator berechnen
function calculateMultiplier() {
const inputValue = parseFloat(document.getElementById('mult-input').value);
const ratioValue = parseFloat(document.getElementById('mult-ratio').value);
if (!isNaN(inputValue) && !isNaN(ratioValue)) {
const result = inputValue * ratioValue;
document.getElementById('mult-result').value = result.toFixed(3);
} else {
document.getElementById('mult-result').value = '';
}
}
// Kaskadenrechner Funktionen
let cascadeConfig = {
mulden: [],
bankettStart: true,
bankettEnd: true,
bankettWidth: 1.0,
totalLength: 0
};
function updateCascadeLengths() {
if (!currentResults || !currentResults.innerTriangle) {
document.getElementById('cascade-length-ag').textContent = '-';
document.getElementById('cascade-length-s2f').textContent = '-';
document.getElementById('cascade-length-k1').textContent = '-';
document.getElementById('cascade-length-k2').textContent = '-';
document.getElementById('cascade-ef-current').textContent = '-';
return;
}
const res = currentResults;
const as2_length = res.innerTriangle.f;
// Aktueller EF-Wert
let ef_current = '-';
if (res.trapezoid && res.trapezoid.ef) {
ef_current = res.trapezoid.ef.toFixed(3) + ' m';
document.getElementById('cascade-ef-current').textContent = ef_current;
}
// S2F basierend auf aktuellem EF
let s2f_length = 0;
if (res.trapezoid && res.trapezoid.fs2) {
s2f_length = res.trapezoid.fs2;
}
// FG Länge (wenn erweiterte Konstruktion vorhanden)
let fg_length = 0;
if (res.extended && res.points.F && res.points.G) {
fg_length = Math.sqrt(
Math.pow(res.points.G.x - res.points.F.x, 2) +
Math.pow(res.points.G.z - res.points.F.z, 2)
);
}
// Update Anzeige
document.getElementById('cascade-length-ag').textContent =
(as2_length + s2f_length + fg_length).toFixed(3) + ' m';
document.getElementById('cascade-length-s2f').textContent =
s2f_length.toFixed(3) + ' m';
document.getElementById('cascade-length-k1').textContent =
(as2_length + s2f_length).toFixed(3) + ' m';
document.getElementById('cascade-length-k2').textContent =
(s2f_length + fg_length).toFixed(3) + ' m';
}
function getMuldenLength(type) {
if (!currentResults || !currentResults.innerTriangle) return 0;
const res = currentResults;
const as2_length = res.innerTriangle.f;
let s2f_length = 0;
if (res.trapezoid && res.trapezoid.fs2) {
s2f_length = res.trapezoid.fs2;
}
let fg_length = 0;
if (res.extended && res.points.F && res.points.G) {
fg_length = Math.sqrt(
Math.pow(res.points.G.x - res.points.F.x, 2) +
Math.pow(res.points.G.z - res.points.F.z, 2)
);
}
switch(type) {
case 'AG': return as2_length + s2f_length + fg_length;
case 'S2F': return s2f_length;
case 'K1': return as2_length + s2f_length;
case 'K2': return s2f_length + fg_length;
default: return 0;
}
}
function addMulde(index) { // index kann undefined sein, um am Ende hinzuzufügen
const defaultType = document.getElementById('cascade-default-type').value;
const newMulde = {
type: defaultType,
bankettAfter: true // Standardmäßig mit Bankett danach, UI blendet es für die letzte aus
};
const insertionIndex = (index === undefined || index === null) ? cascadeConfig.mulden.length : index;
cascadeConfig.mulden.splice(insertionIndex, 0, newMulde);
// Wenn am Ende hinzugefügt wird, muss die vorherige Mulde ggf. ihr Bankett bekommen
// Dies wird durch updateCascadeConfigDisplay() und die Logik dort gehandhabt
updateCascadeConfigDisplay();
updateCascadePreview();
}
function removeMulde(index) {
cascadeConfig.mulden.splice(index, 1);
updateCascadeConfigDisplay();
updateCascadePreview();
}
function updateMuldeType(index, type) {
cascadeConfig.mulden[index].type = type;
updateCascadePreview();
}
function toggleBankettAfter(index) {
cascadeConfig.mulden[index].bankettAfter = !cascadeConfig.mulden[index].bankettAfter;
updateCascadePreview();
}
function updateCascadeConfigDisplay() {
const container = document.getElementById('cascade-config-container');
const isDarkMode = document.body.classList.contains('dark-mode');
let html = '<div style="display: flex; flex-direction: column; gap: 5px;">';
// Platzhalter am Anfang, um die erste Mulde hinzuzufügen
html += `<div class="add-mulde-placeholder" onclick="addMulde(0)"><span>${cascadeConfig.mulden.length === 0 ? '+ Erste Mulde hinzufügen' : '+ Mulde am Anfang einfügen'}</span></div>`;
if (cascadeConfig.mulden.length > 0) {
cascadeConfig.mulden.forEach((mulde, index) => {
const muldeLength = getMuldenLength(mulde.type);
const muldeBg = isDarkMode ? '#2a2a2a' : 'white';
const muldeBorder = isDarkMode ? '#555' : '#ddd';
// Die Mulde selbst rendern
html += `
<div style="background: ${muldeBg}; padding: 10px; border: 1px solid ${muldeBorder}; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>Mulde ${index + 1}</strong>
<button onclick="removeMulde(${index})" style="background: #dc3545; padding: 4px 8px; font-size: 12px;">×</button>
</div>
<div style="margin-top: 8px; display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center;">
<select onchange="updateMuldeType(${index}, this.value)" style="padding: 4px;">
<option value="AG" ${mulde.type === 'AG' ? 'selected' : ''}>Typ AG (${getMuldenLength('AG').toFixed(2)}m)</option>
<option value="S2F" ${mulde.type === 'S2F' ? 'selected' : ''}>Typ S2F (${getMuldenLength('S2F').toFixed(2)}m)</option>
<option value="K1" ${mulde.type === 'K1' ? 'selected' : ''}>Typ K1 (${getMuldenLength('K1').toFixed(2)}m)</option>
<option value="K2" ${mulde.type === 'K2' ? 'selected' : ''}>Typ K2 (${getMuldenLength('K2').toFixed(2)}m)</option>
</select>
<span style="font-size: 0.85em; color: #007acc;">${muldeLength.toFixed(3)} m</span>
</div>
${index < cascadeConfig.mulden.length - 1 ? `
<label style="display: flex; align-items: center; gap: 5px; margin-top: 8px; font-size: 0.85em;">
<input type="checkbox" ${mulde.bankettAfter ? 'checked' : ''} onchange="toggleBankettAfter(${index})">
Bankett nach dieser Mulde
</label>
` : ''}
</div>
`;
// Platzhalter NACH der aktuellen Mulde
html += `<div class="add-mulde-placeholder" onclick="addMulde(${index + 1})"><span>+ Mulde hier einfügen</span></div>`;
});
}
html += '</div>';
container.innerHTML = html;
}
function updateCascadePreview() {
cascadeConfig.bankettStart = document.getElementById('cascade-bankett-start').checked;
cascadeConfig.bankettEnd = document.getElementById('cascade-bankett-end').checked;
cascadeConfig.bankettWidth = parseFloat(document.getElementById('cascade-bankett-width').value) || 1.0;
cascadeConfig.totalLength = parseFloat(document.getElementById('cascade-total-length').value) || 0;
// Referenzlängen, Visualisierung und Zusammenfassung aktualisieren
updateCascadeLengths();
drawCascadeVisualization2();
if (cascadeConfig.mulden.length > 0) {
updateCascadeSummary();
} else {
document.getElementById('cascade-summary').style.display = 'none';
document.getElementById('cascade-suggestion').style.display = 'none';
document.getElementById('cascade-distribute-suggestion').style.display = 'none';
}
}
function autoFillMulden() {
const totalLength = parseFloat(document.getElementById('cascade-total-length').value);
const defaultType = document.getElementById('cascade-default-type').value;
if (!totalLength || totalLength <= 0) {
alert('Bitte Gesamtlänge eingeben');
return;
}
if (!currentResults || !currentResults.innerTriangle) {
alert('Bitte zuerst Hauptgeometrie berechnen');
return;
}
cascadeConfig.mulden = [];
// Verfügbare Länge berechnen
let availableLength = totalLength;
if (cascadeConfig.bankettStart) availableLength -= cascadeConfig.bankettWidth;
if (cascadeConfig.bankettEnd) availableLength -= cascadeConfig.bankettWidth;
const muldenLength = getMuldenLength(defaultType);
if (muldenLength <= 0) {
alert('Muldenlänge kann nicht berechnet werden');
return;
}
// Maximale Anzahl ohne Zwischenbankette
let maxMulden = Math.floor(availableLength / muldenLength);
// Mit Zwischenbanketten probieren
for (let n = maxMulden; n >= 1; n--) {
const mitBanketten = availableLength - (n - 1) * cascadeConfig.bankettWidth;
if (mitBanketten >= n * muldenLength) {
// Diese Konfiguration passt
for (let i = 0; i < n; i++) {
cascadeConfig.mulden.push({
type: defaultType,
bankettAfter: i < n - 1
});
}
break;
}
}
updateCascadeConfigDisplay();
updateCascadePreview();
}
function clearAllMulden() {
cascadeConfig.mulden = [];
updateCascadeConfigDisplay();
updateCascadePreview(); // This will hide summary and suggestion
}
function drawCascadeVisualization2() {
const canvas = document.getElementById('cascade-canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
if (cascadeConfig.mulden.length === 0 || cascadeConfig.totalLength === 0) return;
// Dark mode check
const isDarkMode = document.body.classList.contains('dark-mode');
if (isDarkMode) {
ctx.fillStyle = '#2a2a2a';
ctx.fillRect(0, 0, width, height);
}
// Skalierung
const scale = (width - 60) / cascadeConfig.totalLength;
const baseY = height * 0.7;
const muldenHeight = 80;
let currentX = 30;
// Gesamtlängen-Linie
ctx.strokeStyle = isDarkMode ? '#666' : '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(30, baseY + 100);
ctx.lineTo(30 + cascadeConfig.totalLength * scale, baseY + 100);
ctx.stroke();
// Gesamtlängen-Beschriftung
ctx.fillStyle = isDarkMode ? '#e0e0e0' : '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(`Gesamtlänge: ${cascadeConfig.totalLength.toFixed(2)} m`, width / 2, baseY + 120);
// Zeichne Anfangsbankett
if (cascadeConfig.bankettStart) {
ctx.fillStyle = '#ffc107';
ctx.fillRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.strokeStyle = '#e0a800';
ctx.lineWidth = 2;
ctx.strokeRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.fillStyle = isDarkMode ? '#e0e0e0' : '#333';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('B', currentX + (cascadeConfig.bankettWidth * scale / 2), baseY - 25);
ctx.fillText(`${cascadeConfig.bankettWidth}m`, currentX + (cascadeConfig.bankettWidth * scale / 2), baseY - 5);
currentX += cascadeConfig.bankettWidth * scale;
}
// Zeichne Mulden
cascadeConfig.mulden.forEach((mulde, index) => {
const muldenLength = getMuldenLength(mulde.type);
const muldenWidth = muldenLength * scale;
// Muldenfarbe basierend auf Typ
const colors = {
'AG': '#e3f2fd',
'S2F': '#fff3cd',
'K1': '#d4edda',
'K2': '#f8d7da'
};
ctx.fillStyle = colors[mulde.type] || '#e3f2fd';
ctx.strokeStyle = '#007acc';
ctx.lineWidth = 2;
// Zeichne Muldenform
ctx.beginPath();
ctx.moveTo(currentX, baseY);
switch(mulde.type) {
case 'AG': // Durchgehend geneigt
ctx.lineTo(currentX + muldenWidth * 0.33, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth * 0.67, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY);
break;
case 'S2F': // Beidseitig vertikal
ctx.lineTo(currentX, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY);
break;
case 'K1': // Geneigt→vertikal
ctx.lineTo(currentX + muldenWidth * 0.5, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY);
break;
case 'K2': // Vertikal→geneigt
ctx.lineTo(currentX, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth * 0.5, baseY + muldenHeight);
ctx.lineTo(currentX + muldenWidth, baseY);
break;
}
ctx.closePath();
ctx.fill();
ctx.stroke();
// Muldenbeschriftung
ctx.fillStyle = isDarkMode ? '#333' : '#333';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.fillText(`M${index + 1}`, currentX + muldenWidth / 2, baseY + muldenHeight / 2 - 10);
ctx.font = '11px Arial';
ctx.fillText(`${mulde.type}`, currentX + muldenWidth / 2, baseY + muldenHeight / 2 + 5);
ctx.fillText(`${muldenLength.toFixed(2)}m`, currentX + muldenWidth / 2, baseY + muldenHeight / 2 + 20);
currentX += muldenWidth;
// Zwischenbankett
if (mulde.bankettAfter && index < cascadeConfig.mulden.length - 1) {
ctx.fillStyle = '#ffc107';
ctx.fillRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.strokeStyle = '#e0a800';
ctx.lineWidth = 2;
ctx.strokeRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.fillStyle = isDarkMode ? '#e0e0e0' : '#333';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText(`B${index + 1}`, currentX + (cascadeConfig.bankettWidth * scale / 2), baseY - 25);
currentX += cascadeConfig.bankettWidth * scale;
}
});
// Zeichne Endbankett
if (cascadeConfig.bankettEnd) {
ctx.fillStyle = '#ffc107';
ctx.fillRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.strokeStyle = '#e0a800';
ctx.lineWidth = 2;
ctx.strokeRect(currentX, baseY - 20, cascadeConfig.bankettWidth * scale, 20);
ctx.fillStyle = isDarkMode ? '#e0e0e0' : '#333';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('B', currentX + (cascadeConfig.bankettWidth * scale / 2), baseY - 25);
ctx.fillText(`${cascadeConfig.bankettWidth}m`, currentX + (cascadeConfig.bankettWidth * scale / 2), baseY - 5);
}
}
function updateCascadeSummary() {
const summaryDiv = document.getElementById('cascade-summary');
const contentDiv = document.getElementById('cascade-summary-content');
if (cascadeConfig.mulden.length === 0) {
summaryDiv.style.display = 'none';
return;
}
// Berechne Gesamtlängen
let totalMuldenLength = 0;
let totalBankettLength = 0;
let bankettCount = 0;
// Anfangsbankett
if (cascadeConfig.bankettStart) {
totalBankettLength += cascadeConfig.bankettWidth;
bankettCount++;
}
// Mulden und Zwischenbankette
const muldenByType = { AG: 0, S2F: 0, K1: 0, K2: 0 };
cascadeConfig.mulden.forEach((mulde, index) => {
const length = getMuldenLength(mulde.type);
totalMuldenLength += length;
muldenByType[mulde.type]++;
if (mulde.bankettAfter && index < cascadeConfig.mulden.length - 1) {
totalBankettLength += cascadeConfig.bankettWidth;
bankettCount++;
}
});
// Endbankett
if (cascadeConfig.bankettEnd) {
totalBankettLength += cascadeConfig.bankettWidth;
bankettCount++;
}
const totalUsed = totalMuldenLength + totalBankettLength;
const restLength = cascadeConfig.totalLength - totalUsed;
// HTML generieren
let html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<tr><th style="text-align: left; padding: 5px; border-bottom: 2px solid #007acc;">Element</th>';
html += '<th style="text-align: center; padding: 5px; border-bottom: 2px solid #007acc;">Anzahl</th>';
html += '<th style="text-align: right; padding: 5px; border-bottom: 2px solid #007acc;">Länge</th></tr>';
// Mulden nach Typ
Object.entries(muldenByType).forEach(([type, count]) => {
if (count > 0) {
const unitLength = getMuldenLength(type);
html += `<tr>
<td style="padding: 5px;">Mulden Typ ${type}</td>
<td style="text-align: center; padding: 5px;">${count}</td>
<td style="text-align: right; padding: 5px;">${count} × ${unitLength.toFixed(3)} = ${(count * unitLength).toFixed(3)} m</td>
</tr>`;
}
});
// Bankette
if (bankettCount > 0) {
html += `<tr>
<td style="padding: 5px;">Bankette</td>
<td style="text-align: center; padding: 5px;">${bankettCount}</td>
<td style="text-align: right; padding: 5px;">${bankettCount} × ${cascadeConfig.bankettWidth.toFixed(2)} = ${totalBankettLength.toFixed(2)} m</td>
</tr>`;
}
// Summen
html += '<tr style="border-top: 2px solid #007acc; font-weight: bold;">';
html += `<td style="padding: 5px;">Gesamt verwendet</td>`;
html += `<td style="text-align: center; padding: 5px;">${cascadeConfig.mulden.length + bankettCount}</td>`;
html += `<td style="text-align: right; padding: 5px;">${totalUsed.toFixed(3)} m</td>`;
html += '</tr>';
html += `<tr style="color: ${restLength < 0 ? '#dc3545' : '#28a745'}; font-weight: bold;">
<td style="padding: 5px;">Restlänge</td>
<td style="text-align: center; padding: 5px;">-</td>
<td style="text-align: right; padding: 5px;">${restLength.toFixed(3)} m</td>
</tr>`;
html += '</table>';
if (restLength < -0.001) {
html += '<p style="color: #dc3545; margin-top: 10px;">⚠️ Konfiguration überschreitet Gesamtlänge!</p>';
}
contentDiv.innerHTML = html;
summaryDiv.style.display = 'block';
// Vorschlag zur Füllung der Restlänge berechnen und anzeigen
calculateSuggestion(restLength);
// Vorschlag zur Verteilung der Differenz berechnen
calculateDistributeSuggestion(restLength);
}
function calculateSuggestion(restLength) {
const suggestionDiv = document.getElementById('cascade-suggestion');
if (restLength <= 0.001 || !currentResults || !currentResults.points.A) {
suggestionDiv.style.display = 'none';
return;
}
const defaultType = document.getElementById('cascade-default-type').value;
const targetLength = restLength;
const res = currentResults;
const p = res.points;
const L_AS2 = res.innerTriangle.f;
// Isoliere die aktuelle FG-Länge (approximativ)
const s2f_current = getMuldenLength('S2F');
const k2_current = getMuldenLength('K2');
const L_FG_current = (k2_current > s2f_current) ? (k2_current - s2f_current) : 0;
let s2f_new;
switch(defaultType) {
case 'AG':
s2f_new = targetLength - L_AS2 - L_FG_current;
break;
case 'S2F':
s2f_new = targetLength;
break;
case 'K1':
s2f_new = targetLength - L_AS2;
break;
case 'K2':
s2f_new = targetLength - L_FG_current;
break;
default:
suggestionDiv.style.display = 'none';
return;
}
if (s2f_new <= 0) {
suggestionDiv.style.display = 'none';
return;
}
// Berechne den entsprechenden EF-Wert für den neuen S2F-Wert
const m_as2 = (p.S2.z - p.A.z) / (p.S2.x - p.A.x);
const dx = s2f_new / Math.sqrt(1 + m_as2**2);
const Fx_new = p.S2.x + dx;
const Fz_new = m_as2 * Fx_new;
const ef_new = Math.abs(Fz_new - p.B.z);
// Zeige den Vorschlag an
document.getElementById('suggestion-text').textContent = `Um die Restlänge von ${restLength.toFixed(3)} m mit einer weiteren Mulde (Typ ${defaultType}) zu füllen, könnten Sie einen der folgenden Werte verwenden:`;
document.getElementById('suggestion-s2f').textContent = `${s2f_new.toFixed(3)} m`;
document.getElementById('suggestion-ef').textContent = `${ef_new.toFixed(3)} m`;
// Speichere die Werte für die "Anwenden"-Buttons
suggestionDiv.dataset.s2f = s2f_new.toFixed(3);
suggestionDiv.dataset.ef = ef_new.toFixed(3);
suggestionDiv.style.display = 'block';
}
function applySuggestion(type) {
const suggestionDiv = document.getElementById('cascade-suggestion');
if (type === 's2f') {
const s2f_val = suggestionDiv.dataset.s2f;
const s2f_input = document.getElementById('trapezoid-s2f');
s2f_input.value = s2f_val;
handleTrapezoidInput(s2f_input.id);
} else if (type === 'ef') {
const ef_val = suggestionDiv.dataset.ef;
const ef_input = document.getElementById('trapezoid-ef');
ef_input.value = ef_val;
handleTrapezoidInput(ef_input.id);
}
calculate();
}
function calculateDistributeSuggestion(restLength) {
const suggestionDiv = document.getElementById('cascade-distribute-suggestion');
const muldenCount = cascadeConfig.mulden.length;
if (Math.abs(restLength) < 0.001 || muldenCount === 0 || !currentResults) {
suggestionDiv.style.display = 'none';
return;
}
const deltaPerTrough = restLength / muldenCount;
const s2f_current = getMuldenLength('S2F');
const s2f_new = s2f_current + deltaPerTrough;
if (s2f_new <= 0) {
suggestionDiv.style.display = 'none';
return;
}
// Berechne den entsprechenden EF-Wert für den neuen S2F-Wert
const p = currentResults.points;
const m_as2 = (p.S2.z - p.A.z) / (p.S2.x - p.A.x);
const dx = s2f_new / Math.sqrt(1 + m_as2**2);
const Fx_new = p.S2.x + dx;
const Fz_new = m_as2 * Fx_new;
const ef_new = Math.abs(Fz_new - p.B.z);
document.getElementById('distribute-suggestion-text').textContent = `Um die Differenz von ${restLength.toFixed(3)} m auf die ${muldenCount} vorhandenen Mulden zu verteilen, muss S2F angepasst werden:`;
document.getElementById('distribute-suggestion-s2f').textContent = `${s2f_new.toFixed(3)} m`;
document.getElementById('distribute-suggestion-ef').textContent = `${ef_new.toFixed(3)} m`;
suggestionDiv.dataset.s2f = s2f_new.toFixed(3);
suggestionDiv.style.display = 'block';
}
function applyDistributeSuggestion(type) {
const s2f_val = document.getElementById('cascade-distribute-suggestion').dataset.s2f;
const s2f_input = document.getElementById('trapezoid-s2f');
s2f_input.value = s2f_val;
handleTrapezoidInput(s2f_input.id);
calculate();
}
function exportCascadeConfig() {
if (cascadeConfig.mulden.length === 0) {
alert('Keine Mulden zum Exportieren vorhanden');
return;
}
let text = 'KASKADEN-KONFIGURATION\n';
text += '=' .repeat(50) + '\n\n';
text += `Datum: ${new Date().toLocaleString('de-DE')}\n`;
text += `Gesamtlänge: ${cascadeConfig.totalLength} m\n`;
text += `Bankettbreite: ${cascadeConfig.bankettWidth} m\n\n`;
text += 'ELEMENTE:\n';
text += '-'.repeat(30) + '\n';
let position = 0;
if (cascadeConfig.bankettStart) {
text += `Position ${position.toFixed(2)}m: Anfangsbankett (${cascadeConfig.bankettWidth}m)\n`;
position += cascadeConfig.bankettWidth;
}
cascadeConfig.mulden.forEach((mulde, index) => {
const length = getMuldenLength(mulde.type);
text += `Position ${position.toFixed(2)}m: Mulde ${index + 1} - Typ ${mulde.type} (${length.toFixed(3)}m)\n`;
position += length;
if (mulde.bankettAfter && index < cascadeConfig.mulden.length - 1) {
text += `Position ${position.toFixed(2)}m: Zwischenbankett (${cascadeConfig.bankettWidth}m)\n`;
position += cascadeConfig.bankettWidth;
}
});
if (cascadeConfig.bankettEnd) {
text += `Position ${position.toFixed(2)}m: Endbankett (${cascadeConfig.bankettWidth}m)\n`;
}
navigator.clipboard.writeText(text).then(() => {
alert('Konfiguration in Zwischenablage kopiert!');
});
}
// Initialisierung
window.onload = () => {
// Dark Mode aus localStorage laden
if (localStorage.getItem('darkMode') === 'true') {
document.body.classList.add('dark-mode');
}
document.getElementById('beta-ratio-right').value = DEFAULTS.beta_ratio_right;
convertBeta('beta-ratio');
document.getElementById('bs2').value = DEFAULTS.bs2;
document.getElementById('zeta-percent').value = DEFAULTS.zeta_percent;
convertZeta('zeta-percent');
document.getElementById('trapezoid-ef').value = DEFAULTS.trapezoid_ef;
document.getElementById('angle-e-degrees').value = DEFAULTS.angle_e_degrees;
convertAngleE('angle-e-degrees');
// Setze EF als initial "Chef"
lastModifiedInput = 'trapezoid-ef';
calculate();
// Initialisiere Kaskadenrechner
updateCascadeLengths();
updateCascadeConfigDisplay();
};
</script>
<footer style="text-align:center;padding:1rem;margin-top:2rem;border-top:1px solid #e5e7eb;font-size:0.85rem;color:#6b7280;">
<a href="#" onclick="openImpressum();return false;" style="color:#6b7280;text-decoration:none;">Impressum</a>
<span style="color:#d1d5db;margin:0 0.5rem;">|</span>
<a href="#" onclick="openDatenschutz();return false;" style="color:#6b7280;text-decoration:none;">Datenschutz</a>
</footer>
<!-- IMPRESSUM MODAL -->
<div class="legal-modal" id="impressumModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Impressum</h2>
<button onclick="closeImpressum()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/impressum.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<!-- DATENSCHUTZ MODAL -->
<div class="legal-modal" id="datenschutzModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Datenschutz</h2>
<button onclick="closeDatenschutz()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/datenschutz.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<script>
function openImpressum(){document.getElementById("impressumModal").style.display="flex";}
function closeImpressum(){document.getElementById("impressumModal").style.display="none";}
function openDatenschutz(){document.getElementById("datenschutzModal").style.display="flex";}
function closeDatenschutz(){document.getElementById("datenschutzModal").style.display="none";}
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeImpressum();closeDatenschutz();}});
["impressumModal","datenschutzModal"].forEach(function(id){
var el=document.getElementById(id);
if(el)el.addEventListener("click",function(e){if(e.target===this)this.style.display="none";});
});
</script>
</body>
</html>