1766 lines
72 KiB
HTML
1766 lines
72 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="Roll Diameter Calculator - Internal Naue Tool">
|
||
<title>Roll Diameter Calculator</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #333; min-height: 100vh; }
|
||
.header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
||
.header-inner { max-width: 900px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||
.header h1 { font-size: 24px; margin-bottom: 5px; }
|
||
.header p { font-size: 13px; opacity: 0.9; }
|
||
.header-help-link { color: #fff; border: 1px solid rgba(255,255,255,0.58); border-radius: 5px; padding: 7px 13px; text-decoration: none; font-size: 13px; font-weight: 700; background: rgba(255,255,255,0.08); }
|
||
.header-help-link:hover { background: rgba(255,255,255,0.18); }
|
||
.container { max-width: 900px; margin: 30px auto; padding: 0 16px 60px; }
|
||
.card { background: #fff; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); margin-bottom: 24px; overflow: hidden; }
|
||
.card-header { background: #f0f4fa; padding: 16px 22px; border-bottom: 1px solid #e0e8f0; }
|
||
.card-header h2 { font-size: 16px; color: #1F5438; font-weight: 700; }
|
||
.card-body { padding: 22px; }
|
||
.tabs { display: flex; border-bottom: 2px solid #e0e8f0; margin-bottom: 20px; }
|
||
.tab-btn { padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 14px; color: #666; font-weight: 500; border-bottom: 3px solid transparent; transition: all 0.2s; }
|
||
.tab-btn.active { color: #2BAC70; border-bottom-color: #2BAC70; font-weight: 700; }
|
||
.tab-pane { display: none; }
|
||
.tab-pane.active { display: block; }
|
||
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px; }
|
||
.form-group { display: flex; flex-direction: column; }
|
||
.form-group label { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 6px; text-transform: uppercase; }
|
||
.form-group input, .form-group select { padding: 9px 11px; border: 1px solid #ccc; border-radius: 5px; font-size: 14px; }
|
||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #2BAC70; box-shadow: 0 0 0 3px rgba(43,172,112,0.1); }
|
||
.input-with-unit { display: flex; align-items: center; gap: 6px; }
|
||
.input-with-unit input { flex: 1; }
|
||
.unit-label { font-size: 12px; color: #888; white-space: nowrap; }
|
||
.tol-row { display: flex; align-items: center; gap: 8px; }
|
||
.tol-row input { width: 80px; }
|
||
.weight-section { background: #f8fafa; border: 1px dashed #aac; border-radius: 6px; padding: 14px 18px; margin-top: 18px; }
|
||
.weight-section h4 { font-size: 13px; color: #555; margin-bottom: 12px; }
|
||
.core-presets { display: flex; gap: 5px; margin-top: 6px; flex-wrap: wrap; }
|
||
.core-preset-btn { padding: 3px 9px; font-size: 11px; font-weight: 600; border: 1px solid #2BAC70; background: #fff; color: #2BAC70; border-radius: 4px; cursor: pointer; transition: all 0.15s; }
|
||
.core-preset-btn:hover, .core-preset-btn.active { background: #2BAC70; color: #fff; }
|
||
.mode-selector { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
||
.mode-btn { padding: 7px 16px; border: 2px solid #2BAC70; background: #fff; color: #2BAC70; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
|
||
.mode-btn.active { background: #2BAC70; color: #fff; }
|
||
.btn-calc { background: #2BAC70; color: #fff; border: none; border-radius: 6px; padding: 11px 28px; font-size: 14px; font-weight: 700; cursor: pointer; transition: background 0.2s; }
|
||
.btn-calc:hover { background: #1F5438; }
|
||
.btn-reset { background: #e0e0e0; color: #444; border: none; border-radius: 6px; padding: 11px 20px; font-size: 14px; cursor: pointer; margin-left: 10px; }
|
||
.btn-reset:hover { background: #d0d0d0; }
|
||
.result-box { background: linear-gradient(135deg, #D6EBE2 0%, #8CC9AB 100%); border: 2px solid #2BAC70; border-radius: 8px; padding: 16px 20px; margin-bottom: 18px; display: none; }
|
||
.result-box.show { display: block; }
|
||
.result-label { font-size: 12px; color: #1F5438; font-weight: 600; text-transform: uppercase; }
|
||
.result-value { font-size: 28px; font-weight: 700; color: #1F5438; line-height: 1.2; }
|
||
.result-range { font-size: 12px; color: #1F5438; margin-top: 4px; font-weight: 500; }
|
||
.result-range div { margin-top: 2px; }
|
||
.result-range .range-warning { color: #9a6700; font-weight: 700; }
|
||
.result-range .range-ok { color: #1F5438; font-weight: 700; }
|
||
.forklift-check { border-radius: 7px; padding: 14px 16px; margin-top: 14px; font-size: 13px; line-height: 1.6; display: none; }
|
||
.forklift-check.show { display: block; }
|
||
.forklift-check.fc-ok { background: #eafaf1; border: 1.5px solid #28a745; }
|
||
.forklift-check.fc-warn { background: #fff8e1; border: 1.5px solid #f0b429; }
|
||
.forklift-check.fc-block { background: #fff0f0; border: 2px solid #dc3545; }
|
||
.forklift-check .fc-title { font-weight: 700; margin-bottom: 8px; }
|
||
.forklift-check ul { margin: 6px 0 0 18px; }
|
||
.forklift-check li { margin-bottom: 3px; }
|
||
.fc-req-ok { color: #28a745; }
|
||
.fc-req-nok { color: #dc3545; font-weight: 600; }
|
||
.formula-box { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #666; margin-top: 18px; font-family: 'Courier New', monospace; }
|
||
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; margin-top: 30px; }
|
||
.footer-build-info { margin-top: 6px; color: #bbb; }
|
||
.disclaimer-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: rgba(20, 28, 34, 0.72);
|
||
}
|
||
.disclaimer-overlay.is-hidden { display: none; }
|
||
.disclaimer-dialog {
|
||
width: min(760px, 100%);
|
||
max-height: min(86vh, 820px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.28);
|
||
overflow: hidden;
|
||
}
|
||
.disclaimer-header {
|
||
padding: 18px 22px;
|
||
background: #1F5438;
|
||
color: #fff;
|
||
}
|
||
.disclaimer-header h2 { font-size: 18px; margin: 0; }
|
||
.disclaimer-content {
|
||
padding: 20px 22px;
|
||
overflow-y: auto;
|
||
line-height: 1.5;
|
||
font-size: 14px;
|
||
}
|
||
.disclaimer-content h3 {
|
||
margin: 0 0 8px;
|
||
color: #1F5438;
|
||
font-size: 15px;
|
||
}
|
||
.disclaimer-content p { margin: 0 0 12px; }
|
||
.disclaimer-language + .disclaimer-language {
|
||
margin-top: 18px;
|
||
padding-top: 18px;
|
||
border-top: 1px solid #e0e8f0;
|
||
}
|
||
.disclaimer-actions {
|
||
padding: 16px 22px;
|
||
border-top: 1px solid #e0e8f0;
|
||
background: #f8fafa;
|
||
}
|
||
.disclaimer-confirm {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
font-size: 13px;
|
||
color: #333;
|
||
}
|
||
.disclaimer-confirm input {
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-top: 1px;
|
||
flex: 0 0 auto;
|
||
}
|
||
.disclaimer-btn {
|
||
width: 100%;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 11px 18px;
|
||
background: #2BAC70;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
.disclaimer-btn:hover { background: #1F5438; }
|
||
.disclaimer-btn:disabled {
|
||
background: #b8c7bf;
|
||
cursor: not-allowed;
|
||
}
|
||
.whats-new-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: rgba(20, 28, 34, 0.55);
|
||
}
|
||
.whats-new-overlay.is-hidden { display: none; }
|
||
.whats-new-dialog {
|
||
width: min(520px, 100%);
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24);
|
||
overflow: hidden;
|
||
}
|
||
.whats-new-header {
|
||
padding: 16px 20px;
|
||
background: #1F5438;
|
||
color: #fff;
|
||
}
|
||
.whats-new-header h2 { font-size: 18px; margin: 0 0 4px; }
|
||
.whats-new-version { font-size: 12px; opacity: 0.88; }
|
||
.whats-new-content {
|
||
padding: 18px 20px 8px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: #333;
|
||
}
|
||
.whats-new-content ul { margin: 8px 0 0 20px; }
|
||
.whats-new-content li { margin-bottom: 5px; }
|
||
.whats-new-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
padding: 16px 20px 18px;
|
||
border-top: 1px solid #e0e8f0;
|
||
background: #f8fafa;
|
||
}
|
||
.whats-new-btn {
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 9px 14px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
.whats-new-btn-primary { background: #2BAC70; color: #fff; }
|
||
.whats-new-btn-primary:hover { background: #1F5438; }
|
||
.whats-new-btn-secondary { background: #fff; color: #1F5438; border: 1px solid #a8c8ba; }
|
||
.whats-new-btn-secondary:hover { background: #eef4f1; }
|
||
|
||
/* ============================================
|
||
LOAD OPTIMIZER STYLES
|
||
============================================ */
|
||
.load-optimizer-section {
|
||
margin-top: 30px;
|
||
border: 2px solid #2BAC70;
|
||
border-radius: 8px;
|
||
background-color: #f0f7ff;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(43,172,112,0.1);
|
||
}
|
||
.lo-accordion-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px 20px;
|
||
background: linear-gradient(135deg, #2BAC70 0%, #1F5438 100%);
|
||
color: white;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: background 0.3s ease;
|
||
}
|
||
.lo-accordion-header:hover {
|
||
background: linear-gradient(135deg, #1F5438 0%, #1F5438 100%);
|
||
}
|
||
.lo-accordion-header h3 {
|
||
margin: 0;
|
||
flex: 1;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
.lo-accordion-icon {
|
||
font-size: 14px;
|
||
transition: transform 0.3s ease;
|
||
display: inline-block;
|
||
}
|
||
.lo-accordion-header.active .lo-accordion-icon {
|
||
transform: rotate(180deg);
|
||
}
|
||
.badge-beta-lo {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
background-color: rgba(255,255,255,0.3);
|
||
color: white;
|
||
}
|
||
.lo-accordion-content {
|
||
padding: 20px;
|
||
display: none;
|
||
}
|
||
.lo-accordion-content.active {
|
||
display: block;
|
||
animation: loSlideDown 0.3s ease;
|
||
}
|
||
@keyframes loSlideDown {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
.lo-inputs {
|
||
display: grid;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
.lo-presets-section, .lo-custom-section {
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 6px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
.lo-presets-section h4, .lo-custom-section h4 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
font-size: 14px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.lo-preset-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
.lo-preset-btn {
|
||
padding: 8px 16px;
|
||
border: 2px solid #2BAC70;
|
||
background: white;
|
||
color: #2BAC70;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.lo-preset-btn:hover { background: #2BAC70; color: white; }
|
||
.lo-preset-btn.active {
|
||
background: #2BAC70;
|
||
color: white;
|
||
box-shadow: 0 0 0 3px rgba(43,172,112,0.2);
|
||
}
|
||
.lo-input-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.lo-input-field { display: flex; flex-direction: column; }
|
||
.lo-input-field label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
.lo-input-field input {
|
||
padding: 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
}
|
||
.lo-input-field input:focus {
|
||
outline: none;
|
||
border-color: #2BAC70;
|
||
box-shadow: 0 0 0 2px rgba(43,172,112,0.1);
|
||
}
|
||
.lo-button-group { display: flex; gap: 12px; }
|
||
.lo-btn-primary, .lo-btn-secondary {
|
||
flex: 1;
|
||
padding: 12px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.lo-btn-primary { background: #2BAC70; color: white; }
|
||
.lo-btn-primary:hover { background: #1F5438; box-shadow: 0 2px 6px rgba(43,172,112,0.3); }
|
||
.lo-btn-secondary { background: #e0e0e0; color: #333; }
|
||
.lo-btn-secondary:hover { background: #d0d0d0; }
|
||
.lo-results { display: grid; gap: 20px; display: none; }
|
||
.lo-results.show { display: grid; }
|
||
.lo-results-summary {
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 6px;
|
||
border-left: 4px solid #2BAC70;
|
||
}
|
||
.lo-results-summary h4 { margin-top: 0; color: #333; font-size: 14px; margin-bottom: 12px; }
|
||
.lo-summary-section {
|
||
padding-top: 12px;
|
||
margin-top: 12px;
|
||
border-top: 1px solid #e6ece9;
|
||
}
|
||
.lo-summary-section:first-of-type {
|
||
padding-top: 0;
|
||
margin-top: 0;
|
||
border-top: 0;
|
||
}
|
||
.lo-summary-section-title {
|
||
margin-bottom: 8px;
|
||
color: #1F5438;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
}
|
||
.lo-summary-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
}
|
||
.lo-summary-item:last-child { margin-bottom: 0; }
|
||
.lo-summary-item.lo-governing-summary {
|
||
background: #eef8f2;
|
||
border-left: 3px solid #2BAC70;
|
||
padding: 8px 10px;
|
||
border-radius: 4px;
|
||
}
|
||
.lo-value-highlight { color: #2BAC70; font-weight: bold; font-size: 16px; }
|
||
.lo-value-number { color: #1F5438; font-weight: bold; }
|
||
.lo-util-group { display: flex; flex-direction: column; gap: 4px; text-align: right; }
|
||
.lo-reserve-note {
|
||
margin-top: 4px;
|
||
color: #666;
|
||
font-size: 12px;
|
||
line-height: 1.35;
|
||
text-align: right;
|
||
max-width: 300px;
|
||
}
|
||
.lo-results-table {
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
}
|
||
.lo-results-table h4 { margin-top: 0; margin-bottom: 12px; }
|
||
.lo-results-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||
.lo-results-table thead { background: #f5f5f5; }
|
||
.lo-results-table th {
|
||
padding: 8px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #333;
|
||
border-bottom: 2px solid #2BAC70;
|
||
white-space: normal;
|
||
}
|
||
.lo-results-table td { padding: 8px; border-bottom: 1px solid #e0e0e0; }
|
||
.lo-results-table tbody tr:hover { background: #f9f9f9; }
|
||
.lo-results-table tbody tr.lo-governing {
|
||
background: #eef8f2;
|
||
font-weight: 600;
|
||
border-left: 3px solid #2BAC70;
|
||
}
|
||
.lo-warning {
|
||
background: #fff8e1;
|
||
border: 1px solid #f0b429;
|
||
border-radius: 5px;
|
||
padding: 10px 14px;
|
||
font-size: 12px;
|
||
color: #7a5c00;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* ---- SETTINGS SECTION ---- */
|
||
.lo-settings-section {
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 6px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
.lo-settings-section h4 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
font-size: 14px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.lo-settings-subtitle {
|
||
font-size: 11px;
|
||
color: #888;
|
||
margin-bottom: 12px;
|
||
}
|
||
.lo-settings-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.lo-settings-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.lo-settings-field label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #555;
|
||
margin-bottom: 4px;
|
||
}
|
||
.lo-settings-input-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.lo-settings-input-wrap input {
|
||
padding: 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
width: 90px;
|
||
}
|
||
.lo-settings-input-wrap input:focus {
|
||
outline: none;
|
||
border-color: #2BAC70;
|
||
box-shadow: 0 0 0 2px rgba(43,172,112,0.1);
|
||
}
|
||
.lo-settings-input-wrap .lo-unit {
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
.lo-settings-effective {
|
||
font-size: 11px;
|
||
color: #2BAC70;
|
||
margin-top: 5px;
|
||
min-height: 16px;
|
||
}
|
||
|
||
/* ===== SIDE VIEW ===== */
|
||
.lo-sideview-section {
|
||
background: white; padding: 16px; border-radius: 6px; border: 1px solid #ddd; display: none;
|
||
}
|
||
.lo-sideview-section.show { display: block; }
|
||
.lo-sideview-section h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
|
||
.lo-sideview-context {
|
||
margin: -6px 0 12px;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
.lo-sideview-wrap {
|
||
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden;
|
||
}
|
||
.lo-sideview-legend { display:flex; gap:20px; margin-top:10px; font-size:12px; color:#555; flex-wrap:wrap; }
|
||
.lo-legend-item { display:flex; align-items:center; gap:6px; }
|
||
.lo-legend-box { width:14px; height:14px; border-radius:2px; display:inline-block; border:1px solid #aaa; }
|
||
.roll-color { background:#a8d4f5; border-color:#2BAC70; }
|
||
.container-color { background:#e8e8e8; border-color:#999; }
|
||
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
|
||
|
||
@media (max-width: 768px) {
|
||
.header { padding: 18px 16px; }
|
||
.header-inner { align-items: flex-start; }
|
||
.whats-new-actions { flex-direction: column; }
|
||
.whats-new-btn { width: 100%; }
|
||
.lo-accordion-header h3 { font-size: 14px; }
|
||
.lo-preset-buttons { flex-direction: column; }
|
||
.lo-preset-btn { width: 100%; }
|
||
.lo-input-grid { grid-template-columns: 1fr; }
|
||
.lo-button-group { flex-direction: column; }
|
||
.form-row { grid-template-columns: 1fr; }
|
||
.lo-settings-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="disclaimer-overlay" id="disclaimerOverlay" role="dialog" aria-modal="true" aria-labelledby="disclaimerTitle">
|
||
<div class="disclaimer-dialog">
|
||
<div class="disclaimer-header">
|
||
<h2 id="disclaimerTitle">Disclaimer</h2>
|
||
</div>
|
||
<div class="disclaimer-content">
|
||
<section class="disclaimer-language" aria-label="Deutscher Disclaimer">
|
||
<h3>Deutsch</h3>
|
||
<p><strong>⚠️ Disclaimer</strong></p>
|
||
<p>Dieses Tool ist ausschließlich zur persönlichen, internen Verwendung bestimmt. Die Weitergabe von Logins ist nicht gestattet.</p>
|
||
<p>Berechtigt sind nur: Produktmanagement, Produktionsleitung, R&D, Controlling und Logistik.</p>
|
||
<p>Wichtig: Alle Eingaben und Ausgaben müssen vor der Verwendung auf Plausibilität geprüft werden. Die Datenbasis für Materialdicke und Flächengewicht wird automatisch erstellt und ist weder geprüft noch erhebt sie Anspruch auf Vollständigkeit.</p>
|
||
</section>
|
||
<section class="disclaimer-language" aria-label="English disclaimer">
|
||
<h3>English</h3>
|
||
<p><strong>⚠️ Disclaimer</strong></p>
|
||
<p>This tool is exclusively for personal, internal use. Sharing login credentials is prohibited.</p>
|
||
<p>Authorized departments only: Product Management, Production Management, R&D, Controlling, and Logistics.</p>
|
||
<p>Important: All inputs and outputs must be verified for plausibility before use. The material thickness and area weight data is automatically generated and has not been reviewed. It does not claim to be complete or comprehensive.</p>
|
||
</section>
|
||
</div>
|
||
<div class="disclaimer-actions">
|
||
<label class="disclaimer-confirm" for="disclaimerAccepted">
|
||
<input type="checkbox" id="disclaimerAccepted">
|
||
<span>I have read and understood the disclaimer / Ich habe den Disclaimer gelesen und verstanden.</span>
|
||
</label>
|
||
<button class="disclaimer-btn" id="disclaimerConfirmBtn" type="button" disabled>Confirm / Bestätigen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="whats-new-overlay is-hidden" id="whatsNewOverlay" role="dialog" aria-modal="true" aria-labelledby="whatsNewTitle">
|
||
<div class="whats-new-dialog">
|
||
<div class="whats-new-header">
|
||
<h2 id="whatsNewTitle">What’s New</h2>
|
||
<div class="whats-new-version">Version: <span id="whatsNewVersion">–</span></div>
|
||
</div>
|
||
<div class="whats-new-content">
|
||
<ul id="whatsNewList"></ul>
|
||
</div>
|
||
<div class="whats-new-actions">
|
||
<button class="whats-new-btn whats-new-btn-secondary" id="whatsNewOpenChangesBtn" type="button">Open Recent Changes</button>
|
||
<button class="whats-new-btn whats-new-btn-primary" id="whatsNewContinueBtn" type="button">Continue</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header">
|
||
<div class="header-inner">
|
||
<div>
|
||
<h1>Naue Roll Diameter Calculator</h1>
|
||
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
|
||
</div>
|
||
<a class="header-help-link" href="{{ url_for('user_manual') }}">User Manual</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2>📝 Roll Diameter Calculator</h2>
|
||
</div>
|
||
<div class="card-body">
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" data-tab="direct">Direct Calculation</button>
|
||
<button class="tab-btn" data-tab="extrap">Extrapolation</button>
|
||
</div>
|
||
|
||
<!-- DIRECT CALCULATION TAB -->
|
||
<div class="tab-pane active" id="tab-direct">
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Product Category</label>
|
||
<select id="d-cat">
|
||
<option value="">– Select –</option>
|
||
<option value="bentofix">Bentofix</option>
|
||
<option value="secutex">Secutex</option>
|
||
<option value="carbofol">Carbofol</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Production Site</label>
|
||
<select id="d-site">
|
||
<option value="">– Select –</option>
|
||
<option value="espelkamp">Espelkamp</option>
|
||
<option value="malaysia">Malaysia</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom: 14px;">
|
||
<label>Calculation Mode</label>
|
||
<div class="mode-selector">
|
||
<button class="mode-btn active" data-mode="diameter">→ Roll Diameter</button>
|
||
<button class="mode-btn" data-mode="length">→ Product Length</button>
|
||
<button class="mode-btn" data-mode="target">🎯 Target Diameter</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="font-size: 13px; color: #555; margin: 18px 0 14px;">Input Parameters</h3>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Core Diameter (d)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-co" value="150" min="0" step="1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
<div class="core-presets">
|
||
<button class="core-preset-btn" data-core="133">133mm Stahl</button>
|
||
<button class="core-preset-btn" data-core="150">150mm PVC</button>
|
||
<button class="core-preset-btn" data-core="168">168mm PVC</button>
|
||
<button class="core-preset-btn" data-core="194">194mm Stahl</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Article Number (optional)</label>
|
||
<input type="text" id="d-art" list="d-art-list" placeholder="Search article..." autocomplete="off">
|
||
<datalist id="d-art-list"></datalist>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Product Thickness (t)</label>
|
||
<div class="tol-row">
|
||
<input type="number" id="d-th" placeholder="mm" min="0" step="0.01">
|
||
<span class="unit-label">±</span>
|
||
<input type="number" id="d-tol" placeholder="mm" min="0" step="0.01">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Product Length (L)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-le" placeholder="m" min="0" step="0.1">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Roll Diameter (D)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-di" placeholder="mm" min="0" step="1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="weight-section">
|
||
<h4>Roll Weight (optional)</h4>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Roll Width</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-wi" placeholder="m" min="0" step="0.01">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Area Weight</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-aw" placeholder="g/m²" min="0" step="1">
|
||
<span class="unit-label">g/m²</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Calculated Weight</label>
|
||
<div class="input-with-unit">
|
||
<input type="text" id="d-wo" readonly placeholder="–" style="background: #f0f0f0;">
|
||
<span class="unit-label">kg</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Max. Roll Diameter</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-target-di" placeholder="mm" min="0" step="1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Max. Roll Weight</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-max-weight" placeholder="kg" min="0" step="1">
|
||
<span class="unit-label">kg</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Minimum Product Length</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="d-min-le" placeholder="m" min="0" step="0.1">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="forklift-check" id="forkliftCheck">
|
||
<div class="fc-title" id="forkliftCheckTitle"></div>
|
||
<div id="forkliftCheckBody"></div>
|
||
</div>
|
||
|
||
<div style="margin-top: 18px;">
|
||
<button class="btn-calc" id="d-calc">▶ Calculate</button>
|
||
<button class="btn-reset" id="d-reset">Reset</button>
|
||
</div>
|
||
|
||
<div class="result-box" id="d-result">
|
||
<div class="result-label" id="d-res-label">Result</div>
|
||
<div class="result-value" id="d-res-val">–</div>
|
||
<div class="result-range" id="d-res-range"></div>
|
||
</div>
|
||
|
||
<div class="formula-box">
|
||
Geometry: π/4 × (D² − d²) = L × t | Weight: G [kg] = Area Weight [g/m²] × L [m] × Width [m] / 1000
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- EXTRAPOLATION TAB -->
|
||
<div class="tab-pane" id="tab-extrap">
|
||
|
||
<h3 style="font-size: 13px; color: #555; margin-bottom: 14px;">Known Reference Roll</h3>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Article Number (optional)</label>
|
||
<input type="text" id="e-art" list="e-art-list" placeholder="Search article..." autocomplete="off">
|
||
<datalist id="e-art-list"></datalist>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Core Diameter (d)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-co" value="150" min="0" step="1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Known Roll Diameter (D₀)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-d0" placeholder="mm" min="0" step="1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Known Roll Length (L₀)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-l0" placeholder="m" min="0" step="0.1">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="font-size: 13px; color: #555; margin: 18px 0 14px;">New Roll Length</h3>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>New Roll Length (L₁)</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-l1" placeholder="m" min="0" step="0.1">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Tolerance (optional)</label>
|
||
<div class="tol-row">
|
||
<span class="unit-label">±</span>
|
||
<input type="number" id="e-tol" placeholder="mm" min="0" step="0.1">
|
||
<span class="unit-label">mm</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="weight-section">
|
||
<h4>Roll Weight (optional)</h4>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Roll Width</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-wi" placeholder="m" min="0" step="0.01">
|
||
<span class="unit-label">m</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Area Weight</label>
|
||
<div class="input-with-unit">
|
||
<input type="number" id="e-aw" placeholder="g/m²" min="0" step="1">
|
||
<span class="unit-label">g/m²</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 18px;">
|
||
<button class="btn-calc" id="e-calc">▶ Calculate Roll Diameter</button>
|
||
<button class="btn-reset" id="e-reset">Reset</button>
|
||
</div>
|
||
|
||
<div class="result-box" id="e-result">
|
||
<div class="result-label">Extrapolated Roll Diameter (D₁)</div>
|
||
<div class="result-value" id="e-res-val">–</div>
|
||
<div class="result-range" id="e-res-range"></div>
|
||
</div>
|
||
|
||
<div class="formula-box">
|
||
Geometry: D₁ = √(d² + L₁/L₀ × (D₀² − d²)) | Weight: G [kg] = Area Weight × L₁ × Width / 1000
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============================================================
|
||
LOAD OPTIMIZER SECTION
|
||
============================================================ -->
|
||
<div id="loadOptimizerContainer" class="load-optimizer-section">
|
||
|
||
<div class="lo-accordion-header" id="loAccordionToggle">
|
||
<span class="lo-accordion-icon">▼</span>
|
||
<h3>🎯 Load Optimizer – Transport Capacity Calculator</h3>
|
||
<span class="badge-beta-lo">BETA</span>
|
||
</div>
|
||
|
||
<div class="lo-accordion-content" id="loAccordionContent">
|
||
|
||
<div class="lo-warning">
|
||
⚠ The Load Optimizer uses roll data from the <strong>Direct Calculation</strong> tab above.
|
||
Please complete a roll calculation first (Roll Diameter + Roll Width + Roll Weight must be filled).
|
||
</div>
|
||
|
||
<div class="lo-inputs">
|
||
|
||
<!-- ===== SETTINGS SECTION ===== -->
|
||
<div class="lo-settings-section">
|
||
<h4>⚙️ Loading Clearances</h4>
|
||
<div class="lo-settings-subtitle">Safety margins applied to the effective loading space before calculation.</div>
|
||
<div class="lo-settings-grid">
|
||
<div class="lo-settings-field">
|
||
<label>Side Wall Clearance (one side)</label>
|
||
<div class="lo-settings-input-wrap">
|
||
<input type="number" id="loMarginSide" value="0" min="0" max="100" step="1">
|
||
<span class="lo-unit">cm</span>
|
||
</div>
|
||
<div class="lo-settings-effective" id="loMarginSideEffective"></div>
|
||
</div>
|
||
<div class="lo-settings-field">
|
||
<label>Ceiling Clearance (top only)</label>
|
||
<div class="lo-settings-input-wrap">
|
||
<input type="number" id="loMarginCeiling" value="20" min="0" max="100" step="1">
|
||
<span class="lo-unit">cm</span>
|
||
</div>
|
||
<div class="lo-settings-effective" id="loMarginCeilingEffective"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- ===== END SETTINGS ===== -->
|
||
|
||
<div class="lo-presets-section">
|
||
<h4>🚚 Preset Transport Type:</h4>
|
||
<div class="lo-preset-buttons">
|
||
<button class="lo-preset-btn" data-preset="container_20ft">20ft Container</button>
|
||
<button class="lo-preset-btn" data-preset="container_40ft">40ft Container</button>
|
||
<button class="lo-preset-btn" data-preset="container_40ft_hc">40ft High Cube</button>
|
||
<button class="lo-preset-btn" data-preset="lkw_sattelzug">LKW Sattelzug</button>
|
||
<button class="lo-preset-btn" data-preset="lkw_tandem">LKW Tandem</button>
|
||
<button class="lo-preset-btn" data-preset="custom">Custom</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lo-custom-section">
|
||
<h4>📦 Dimensions & Weight Limit:</h4>
|
||
<div class="lo-input-grid">
|
||
<div class="lo-input-field">
|
||
<label>Length (m)</label>
|
||
<input type="number" id="customLength" placeholder="e.g. 12.19" step="0.01" min="0">
|
||
</div>
|
||
<div class="lo-input-field">
|
||
<label>Width (m)</label>
|
||
<input type="number" id="customWidth" placeholder="e.g. 2.35" step="0.01" min="0">
|
||
</div>
|
||
<div class="lo-input-field">
|
||
<label>Height (m)</label>
|
||
<input type="number" id="customHeight" placeholder="e.g. 2.39" step="0.01" min="0">
|
||
</div>
|
||
<div class="lo-input-field">
|
||
<label>Max. Load (kg)</label>
|
||
<input type="number" id="customMaxWeight" placeholder="e.g. 30480" step="100" min="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lo-button-group">
|
||
<button class="lo-btn-primary" id="calculateLoad">▶ Calculate</button>
|
||
<button class="lo-btn-secondary" id="resetLoad">↻ Reset</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="lo-results" id="resultsContainer">
|
||
|
||
<div class="lo-results-summary">
|
||
<h4>Capacity Summary:</h4>
|
||
<div class="lo-summary-section">
|
||
<div class="lo-summary-section-title">Governing Constraint</div>
|
||
<div class="lo-summary-item lo-governing-summary">
|
||
<span><strong>Constraint:</strong></span>
|
||
<span id="limitingFactorValue" class="lo-value-highlight">–</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lo-summary-section">
|
||
<div class="lo-summary-section-title">Transport Result</div>
|
||
<div class="lo-summary-item">
|
||
<span><strong>Total Rolls:</strong></span>
|
||
<div style="text-align:right;">
|
||
<span id="totalRollsValue" class="lo-value-highlight">–</span>
|
||
<small id="rollsDetailValue" style="display:block;color:#888;"></small>
|
||
</div>
|
||
</div>
|
||
<div class="lo-summary-item">
|
||
<span><strong>Utilization:</strong></span>
|
||
<div class="lo-util-group">
|
||
<div>Weight Utilization: <span id="weightUtilValue" class="lo-value-number">–</span> %</div>
|
||
<div>Volume Utilization: <span id="volumeUtilValue" class="lo-value-number">–</span> %</div>
|
||
<div>Transported Product Area: <span id="productM2Value" class="lo-value-number">–</span> m²</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lo-summary-section">
|
||
<div class="lo-summary-section-title">Remaining Capacity</div>
|
||
<div class="lo-summary-item">
|
||
<span><strong>Remaining Capacity:</strong></span>
|
||
<div class="lo-util-group">
|
||
<div>Remaining Payload: <span id="remainingPayloadValue" class="lo-value-number">–</span> kg</div>
|
||
<div>Unused Payload: <span id="unusedPayloadValue" class="lo-value-number">–</span> %</div>
|
||
<div>Unused Volume: <span id="unusedVolumeValue" class="lo-value-number">–</span> %</div>
|
||
<div id="capacityInterpretationValue" class="lo-reserve-note"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lo-results-table">
|
||
<h4>Capacity Analysis:</h4>
|
||
<table id="comparisonTable">
|
||
<thead>
|
||
<tr>
|
||
<th>Constraint</th>
|
||
<th>Max Rolls</th>
|
||
<th>Total Weight</th>
|
||
<th>Transported Area [m²]</th>
|
||
<th>Payload Util. %</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="lo-sideview-section" id="sideViewSection">
|
||
<h4>🔍 Front View – Cross-Section</h4>
|
||
<p class="lo-sideview-context">Cross-section of the actual loading configuration.</p>
|
||
<div class="lo-sideview-wrap">
|
||
<svg id="sideViewSVG" width="100%" viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg">
|
||
<text x="400" y="130" text-anchor="middle" fill="#aaa" font-size="14">Run a calculation to see the side view.</text>
|
||
</svg>
|
||
</div>
|
||
<div class="lo-sideview-legend">
|
||
<span class="lo-legend-item"><span class="lo-legend-box roll-color"></span> Roll (cross-section, ⌀ = diameter)</span>
|
||
<span class="lo-legend-item"><span class="lo-legend-box container-color"></span> Container / Vehicle</span>
|
||
<span class="lo-legend-item"><span class="lo-legend-box margin-color"></span> Clearance zone</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<!-- END LOAD OPTIMIZER -->
|
||
|
||
</div>
|
||
|
||
<div class="footer">
|
||
Naue GmbH & Co. KG · Roll Diameter Calculator · Internal Application
|
||
<div class="footer-build-info">
|
||
Version {{ build_info.version }} · Branch {{ build_info.branch }} · Commit {{ build_info.commit }} · Built {{ build_info.timestamp }}
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// =====================================================
|
||
// GLOBAL STATE
|
||
// =====================================================
|
||
window.ARTICLE_DATA = [];
|
||
window.APP_CONFIG = {"forklift_rules":{"applicable_categories":["bentofix"],"rules":[{"weight_threshold_kg":1700,"max_weight_kg":2750,"requirements":{"min_core_outer_diameter_mm":170},"messages":{"en":{"warning_title":"Heavy Roll – Special Equipment Required","core_ok":"Core diameter OK (≥ 170 mm)","core_nok":"Core diameter too small"}}}],"hard_limit":{"max_weight_kg":2750,"messages":{"en":{"block_title":"Weight Limit Exceeded","block_text":"Roll weight exceeds 2750 kg maximum"}}}}};
|
||
const ROLLCALC_ARTICLE_DATA_URL = {{ static_asset_url('article-data.json')|tojson }};
|
||
const ROLLCALC_RECENT_CHANGES_JSON_URL = {{ static_asset_url('recent_changes.json')|tojson }};
|
||
|
||
// =====================================================
|
||
// LOAD ARTICLE DATA
|
||
// =====================================================
|
||
function populateArticleDropdowns() {
|
||
const data = window.ARTICLE_DATA || [];
|
||
|
||
['d-art', 'e-art'].forEach(function(inputId) {
|
||
const dl = document.getElementById(inputId + '-list');
|
||
if (!dl) return;
|
||
data.forEach(function(a) {
|
||
const opt = document.createElement('option');
|
||
opt.value = a.nr + ' – ' + a.name;
|
||
dl.appendChild(opt);
|
||
});
|
||
});
|
||
|
||
const dArt = document.getElementById('d-art');
|
||
if (dArt) {
|
||
dArt.addEventListener('input', function() {
|
||
const val = dArt.value;
|
||
const match = data.find(a => val.startsWith(a.nr + ' –') || val === a.nr);
|
||
if (match) {
|
||
if (match.thickness) document.getElementById('d-th').value = match.thickness;
|
||
if (match.thickness_stddev && match.thickness_stddev > 0) document.getElementById('d-tol').value = match.thickness_stddev;
|
||
if (match.area_weight && match.area_weight > 0) document.getElementById('d-aw').value = match.area_weight;
|
||
if (match.core_type && match.core_type > 0) document.getElementById('d-co').value = match.core_type;
|
||
}
|
||
});
|
||
}
|
||
|
||
const eArt = document.getElementById('e-art');
|
||
if (eArt) {
|
||
eArt.addEventListener('input', function() {
|
||
const val = eArt.value;
|
||
const match = data.find(a => val.startsWith(a.nr + ' –') || val === a.nr);
|
||
if (match) {
|
||
if (match.thickness) document.getElementById('e-th').value = match.thickness;
|
||
if (match.thickness_stddev && match.thickness_stddev > 0) document.getElementById('e-tol').value = match.thickness_stddev;
|
||
if (match.area_weight && match.area_weight > 0) document.getElementById('e-aw').value = match.area_weight;
|
||
if (match.core_type && match.core_type > 0) document.getElementById('e-co').value = match.core_type;
|
||
}
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('.core-preset-btn').forEach(btn => {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const val = btn.dataset.core;
|
||
const pane = btn.closest('.tab-pane');
|
||
if (pane) {
|
||
const inp = pane.querySelector('input[id$="-co"]');
|
||
if (inp) {
|
||
inp.value = val;
|
||
pane.querySelectorAll('.core-preset-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
(async function() {
|
||
try {
|
||
const resp = await fetch(ROLLCALC_ARTICLE_DATA_URL);
|
||
if (resp.ok) {
|
||
window.ARTICLE_DATA = await resp.json();
|
||
console.log('✅ Article data loaded:', window.ARTICLE_DATA.length, 'items');
|
||
populateArticleDropdowns();
|
||
} else {
|
||
console.error('❌ Failed to load article data:', resp.status);
|
||
}
|
||
} catch (e) {
|
||
console.error('❌ Error loading data:', e);
|
||
}
|
||
})();
|
||
|
||
// =====================================================
|
||
// TAB SWITCHING
|
||
// =====================================================
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
|
||
// =====================================================
|
||
// MODE SWITCHING
|
||
// =====================================================
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// =====================================================
|
||
// DIRECT CALCULATION (WITH 2σ)
|
||
// =====================================================
|
||
document.getElementById('d-calc').addEventListener('click', () => {
|
||
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
|
||
const d = parseFloat(document.getElementById('d-co').value) || 0;
|
||
const t = parseFloat(document.getElementById('d-th').value) || 0;
|
||
const tol = parseFloat(document.getElementById('d-tol').value) || 0;
|
||
const L = parseFloat(document.getElementById('d-le').value) || 0;
|
||
const D = parseFloat(document.getElementById('d-di').value) || 0;
|
||
const category = document.getElementById('d-cat')?.value || '';
|
||
|
||
const resultBox = document.getElementById('d-result');
|
||
const resLabel = document.getElementById('d-res-label');
|
||
const resVal = document.getElementById('d-res-val');
|
||
const resRange = document.getElementById('d-res-range');
|
||
|
||
if (mode === 'diameter') {
|
||
if (!d || !t || !L) { alert('Please enter d, t, and L.'); return; }
|
||
const D_nom = Math.sqrt(d*d + (4*L*1000*t)/Math.PI);
|
||
resLabel.textContent = 'Roll Diameter (D)';
|
||
resVal.textContent = D_nom.toFixed(1) + ' mm';
|
||
if (tol) {
|
||
const D_minus2sigma = Math.sqrt(d*d + (4*L*1000*(t-2*tol))/Math.PI);
|
||
const D_plus2sigma = Math.sqrt(d*d + (4*L*1000*(t+2*tol))/Math.PI);
|
||
resRange.textContent = '−2σ: ' + D_minus2sigma.toFixed(1) + ' mm | +2σ: ' + D_plus2sigma.toFixed(1) + ' mm';
|
||
} else {
|
||
resRange.textContent = '';
|
||
}
|
||
document.getElementById('d-di').value = D_nom.toFixed(1);
|
||
} else if (mode === 'length') {
|
||
if (!d || !t || !D) { alert('Please enter d, t, and D.'); return; }
|
||
const L_calc = (Math.PI / (4 * t)) * (D*D - d*d) / 1000;
|
||
resLabel.textContent = 'Product Length (L)';
|
||
resVal.textContent = L_calc.toFixed(2) + ' m';
|
||
resRange.textContent = '';
|
||
document.getElementById('d-le').value = L_calc.toFixed(2);
|
||
} else if (mode === 'target') {
|
||
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
|
||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||
const maxWeight = parseFloat(document.getElementById('d-max-weight').value) || 0;
|
||
const minLength = parseFloat(document.getElementById('d-min-le').value) || 0;
|
||
const D_target = parseFloat(document.getElementById('d-target-di').value) || 0;
|
||
|
||
if (!d || !t || !D_target || !wi || !aw || !maxWeight) {
|
||
alert('Please enter d, t, max. roll diameter, roll width, area weight, and max. roll weight.');
|
||
return;
|
||
}
|
||
if (D_target <= d) {
|
||
alert('Max. roll diameter must be larger than core diameter.');
|
||
return;
|
||
}
|
||
|
||
const L_diameter = (Math.PI / (4 * t)) * (D_target*D_target - d*d) / 1000;
|
||
const L_weight = maxWeight * 1000 / (aw * wi);
|
||
const L_allowed = Math.min(L_diameter, L_weight);
|
||
const D_result = Math.sqrt(d*d + (4*L_allowed*1000*t)/Math.PI);
|
||
const W_result = aw * L_allowed * wi / 1000;
|
||
const diameterUtil = D_result / D_target * 100;
|
||
const weightUtil = W_result / maxWeight * 100;
|
||
const lengthDiff = minLength > 0 ? L_allowed - minLength : null;
|
||
const epsilon = 0.0001;
|
||
let limitingFactor = 'Diameter + Weight';
|
||
|
||
if (L_diameter < L_weight - epsilon) {
|
||
limitingFactor = 'Diameter';
|
||
} else if (L_weight < L_diameter - epsilon) {
|
||
limitingFactor = 'Weight';
|
||
}
|
||
|
||
resLabel.textContent = 'Maximum Allowed Product Length';
|
||
resVal.textContent = L_allowed.toFixed(2) + ' m';
|
||
|
||
const minLengthText = minLength > 0 ? minLength.toFixed(2) + ' m' : 'not set';
|
||
const diffText = lengthDiff !== null ? lengthDiff.toFixed(2) + ' m' : 'n/a';
|
||
let minStatus = '<div>Minimum length not set</div>';
|
||
if (minLength > 0 && L_allowed < minLength) {
|
||
minStatus = '<div class="range-warning">Warning: maximum allowed length is below the minimum product length.</div>';
|
||
} else if (minLength > 0) {
|
||
minStatus = '<div class="range-ok">Minimum length OK</div>';
|
||
}
|
||
|
||
resRange.innerHTML =
|
||
'<div>Resulting Roll Diameter: ' + D_result.toFixed(1) + ' mm</div>' +
|
||
'<div>Resulting weight: ' + W_result.toFixed(1) + ' kg</div>' +
|
||
'<div>Minimum length: ' + minLengthText + '</div>' +
|
||
'<div>Difference: ' + diffText + '</div>' +
|
||
'<div>Limiting factor: ' + limitingFactor + '</div>' +
|
||
'<div>Utilization: Diameter ' + diameterUtil.toFixed(1) + '% | Weight ' + weightUtil.toFixed(1) + '%</div>' +
|
||
minStatus;
|
||
|
||
document.getElementById('d-le').value = L_allowed.toFixed(2);
|
||
document.getElementById('d-wo').value = W_result.toFixed(1);
|
||
}
|
||
|
||
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
|
||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||
const L_final = parseFloat(document.getElementById('d-le').value) || 0;
|
||
if (wi > 0 && aw > 0 && L_final > 0) {
|
||
const weight = aw * L_final * wi / 1000;
|
||
document.getElementById('d-wo').value = weight.toFixed(1);
|
||
checkForklift(weight, d, category);
|
||
} else {
|
||
document.getElementById('forkliftCheck').classList.remove('show');
|
||
}
|
||
|
||
resultBox.style.display = 'block';
|
||
});
|
||
|
||
document.getElementById('d-reset').addEventListener('click', () => {
|
||
['d-co', 'd-th', 'd-tol', 'd-le', 'd-di', 'd-wi', 'd-aw', 'd-wo', 'd-target-di', 'd-max-weight', 'd-min-le'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = id === 'd-co' ? '150' : '';
|
||
});
|
||
document.getElementById('d-art').value = '';
|
||
document.getElementById('d-result').style.display = 'none';
|
||
document.getElementById('forkliftCheck').classList.remove('show');
|
||
});
|
||
|
||
// =====================================================
|
||
// EXTRAPOLATION
|
||
// =====================================================
|
||
document.getElementById('e-calc').addEventListener('click', () => {
|
||
const d = parseFloat(document.getElementById('e-co').value) || 0;
|
||
const D0 = parseFloat(document.getElementById('e-d0').value) || 0;
|
||
const L0 = parseFloat(document.getElementById('e-l0').value) || 0;
|
||
const L1 = parseFloat(document.getElementById('e-l1').value) || 0;
|
||
const tol = parseFloat(document.getElementById('e-tol').value) || 0;
|
||
|
||
if (!d || !D0 || !L0 || !L1) { alert('Please enter all values.'); return; }
|
||
|
||
const D1 = Math.sqrt(d*d + (L1/L0) * (D0*D0 - d*d));
|
||
document.getElementById('e-res-val').textContent = D1.toFixed(1) + ' mm';
|
||
if (tol) {
|
||
document.getElementById('e-res-range').textContent = 'Min: ' + (D1 - tol).toFixed(1) + ' mm | Max: ' + (D1 + tol).toFixed(1) + ' mm';
|
||
} else {
|
||
document.getElementById('e-res-range').textContent = '';
|
||
}
|
||
|
||
document.getElementById('e-result').style.display = 'block';
|
||
});
|
||
|
||
document.getElementById('e-reset').addEventListener('click', () => {
|
||
['e-co', 'e-d0', 'e-l0', 'e-l1', 'e-tol', 'e-wi', 'e-aw'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = id === 'e-co' ? '150' : '';
|
||
});
|
||
document.getElementById('e-art').value = '';
|
||
document.getElementById('e-result').style.display = 'none';
|
||
});
|
||
|
||
// =====================================================
|
||
// FORKLIFT CHECK
|
||
// =====================================================
|
||
function checkForklift(weightKg, coreMm, category) {
|
||
const box = document.getElementById('forkliftCheck');
|
||
const titleEl = document.getElementById('forkliftCheckTitle');
|
||
const bodyEl = document.getElementById('forkliftCheckBody');
|
||
|
||
const cfg = window.APP_CONFIG?.forklift_rules;
|
||
if (!cfg || !cfg.applicable_categories.includes(category.toLowerCase())) {
|
||
box.classList.remove('show');
|
||
return;
|
||
}
|
||
|
||
const rule = cfg.rules[0];
|
||
const limit = cfg.hard_limit;
|
||
|
||
if (weightKg > limit.max_weight_kg) {
|
||
box.className = 'forklift-check fc-block show';
|
||
titleEl.textContent = '⚠️ ' + limit.messages.en.block_title;
|
||
bodyEl.innerHTML = '<p>' + limit.messages.en.block_text.replace('{weight}', weightKg) + '</p>';
|
||
return;
|
||
}
|
||
|
||
if (weightKg >= rule.weight_threshold_kg) {
|
||
const coreOk = coreMm >= rule.requirements.min_core_outer_diameter_mm;
|
||
box.className = 'forklift-check fc-warn show';
|
||
titleEl.textContent = '⚠️ ' + rule.messages.en.warning_title;
|
||
const statusMsg = coreOk ? '<li class="fc-req-ok">✓ Core diameter OK</li>' : '<li class="fc-req-nok">✗ Core diameter ' + coreMm + ' mm (min 170 mm required)</li>';
|
||
bodyEl.innerHTML = '<p>Special equipment required for rolls ≥ ' + rule.weight_threshold_kg + ' kg:</p><ul>' + statusMsg + '</ul>';
|
||
return;
|
||
}
|
||
|
||
box.className = 'forklift-check fc-ok show';
|
||
titleEl.textContent = '✓ Forklift Check OK';
|
||
bodyEl.innerHTML = '<p>Roll weight ' + weightKg.toFixed(1) + ' kg is within standard limits. No special equipment required.</p>';
|
||
}
|
||
|
||
// =====================================================
|
||
// LOAD OPTIMIZER CORE
|
||
// =====================================================
|
||
class LoadOptimizer {
|
||
constructor() {
|
||
this.state = {
|
||
rollDiameter: 0, coreDiameter: 150, rollWidth: 0,
|
||
rollWeight: 0, m2PerRoll: 0, productLength: 0
|
||
};
|
||
this.presets = {
|
||
container_20ft: { name:"20ft Container", length:5.90, width:2.35, height:2.39, maxWeight:21600 },
|
||
container_40ft: { name:"40ft Container", length:12.19, width:2.35, height:2.39, maxWeight:30480 },
|
||
container_40ft_hc:{ name:"40ft High Cube", length:12.19, width:2.35, height:2.69, maxWeight:30480 },
|
||
lkw_sattelzug: { name:"LKW Sattelzug", length:13.50, width:2.50, height:2.70, maxWeight:24000 },
|
||
lkw_tandem: { name:"LKW Tandem", length:13.50, width:2.50, height:2.70, maxWeight:25000 }
|
||
};
|
||
}
|
||
setRollData(data) {
|
||
this.state = {
|
||
rollDiameter: data.rollDiameter || 0,
|
||
coreDiameter: data.coreDiameter || 150,
|
||
rollWidth: data.rollWidth || 0,
|
||
rollWeight: data.rollWeight || 0,
|
||
m2PerRoll: data.m2PerRoll || 0,
|
||
productLength: data.productLength || 0
|
||
};
|
||
}
|
||
calculateLoadUtilization(dim, maxLoadWeight, marginSide, marginCeiling) {
|
||
marginSide = marginSide || 0;
|
||
marginCeiling = marginCeiling || 0;
|
||
|
||
const { rollDiameter, coreDiameter, rollWidth, rollWeight, m2PerRoll } = this.state;
|
||
const D = rollDiameter / 1000, d = coreDiameter / 1000;
|
||
|
||
const effWidth = dim.width - marginSide;
|
||
const effHeight = dim.height - marginCeiling;
|
||
|
||
const rollsAlongLength = Math.floor(dim.length / rollWidth);
|
||
const rollsAlongWidth = Math.floor(effWidth / D);
|
||
const rollsPerLayer = rollsAlongLength * rollsAlongWidth;
|
||
const maxStackHeight = Math.floor(effHeight / D);
|
||
const maxByGeometry = rollsPerLayer * maxStackHeight;
|
||
const maxByWeight = rollWeight > 0 ? Math.floor(maxLoadWeight / rollWeight) : Infinity;
|
||
|
||
const volPerRoll = (Math.PI / 4) * (D*D - d*d) * rollWidth;
|
||
const containerVol = dim.length * dim.width * dim.height;
|
||
const effVol = dim.length * effWidth * effHeight;
|
||
const maxByVolume = volPerRoll > 0 ? Math.floor(effVol / volPerRoll) : Infinity;
|
||
|
||
const scenarios = [
|
||
{ name:'Weight', rolls: maxByWeight, weight: maxByWeight * rollWeight, type:'weight' },
|
||
{ name:'Volume', rolls: maxByVolume, weight: maxByVolume * rollWeight, type:'volume' },
|
||
{ name:'Geometry', rolls: maxByGeometry, weight: maxByGeometry * rollWeight, type:'geometry' }
|
||
];
|
||
const limiting = scenarios.reduce((mn, c) => c.rolls < mn.rolls ? c : mn);
|
||
const finalRolls = limiting.rolls === Infinity ? 0 : limiting.rolls;
|
||
const finalWeight = finalRolls * rollWeight;
|
||
const finalM2 = finalRolls * m2PerRoll;
|
||
|
||
return {
|
||
scenarios, limiting, finalRolls, finalWeight, finalM2,
|
||
utilizationWeight: maxLoadWeight > 0 ? (finalWeight / maxLoadWeight) * 100 : 0,
|
||
utilizationVolume: containerVol > 0 ? (finalRolls * volPerRoll / containerVol) * 100 : 0,
|
||
rollsPerLayer, rollsAlongLength, rollsAlongWidth, maxStackHeight,
|
||
containerVol, volPerRoll, effWidth, effHeight,
|
||
marginSide, marginCeiling
|
||
};
|
||
}
|
||
fmt(n, dec=1) { return isFinite(n) ? n.toFixed(dec) : '–'; }
|
||
}
|
||
|
||
// =====================================================
|
||
// LOAD OPTIMIZER UI
|
||
// =====================================================
|
||
class LoadOptimizerUI {
|
||
constructor() {
|
||
this.optimizer = new LoadOptimizer();
|
||
this.currentPreset = null;
|
||
this.init();
|
||
}
|
||
init() {
|
||
document.getElementById('loAccordionToggle').addEventListener('click', () => this.toggleAccordion());
|
||
document.querySelectorAll('.lo-preset-btn').forEach(btn =>
|
||
btn.addEventListener('click', e => this.loadPreset(e.target.dataset.preset))
|
||
);
|
||
document.getElementById('calculateLoad').addEventListener('click', () => this.calculate());
|
||
document.getElementById('resetLoad').addEventListener('click', () => this.resetForm());
|
||
|
||
const updateEffective = () => this.updateEffectiveHints();
|
||
['loMarginSide','loMarginCeiling','customWidth','customHeight'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.addEventListener('input', updateEffective);
|
||
});
|
||
}
|
||
|
||
updateEffectiveHints() {
|
||
const sideM = parseFloat(document.getElementById('loMarginSide').value) || 0;
|
||
const ceilM = parseFloat(document.getElementById('loMarginCeiling').value) || 0;
|
||
const W = parseFloat(document.getElementById('customWidth').value) || 0;
|
||
const H = parseFloat(document.getElementById('customHeight').value) || 0;
|
||
const sideEl = document.getElementById('loMarginSideEffective');
|
||
const ceilEl = document.getElementById('loMarginCeilingEffective');
|
||
|
||
if (W > 0 && sideM > 0) {
|
||
const eff = (W - sideM / 100).toFixed(2);
|
||
sideEl.textContent = '→ effective width: ' + eff + ' m';
|
||
} else {
|
||
sideEl.textContent = W > 0 ? '→ effective width: ' + W.toFixed(2) + ' m' : '';
|
||
}
|
||
|
||
if (H > 0 && ceilM > 0) {
|
||
const eff = (H - ceilM / 100).toFixed(2);
|
||
ceilEl.textContent = '→ effective height: ' + eff + ' m';
|
||
} else {
|
||
ceilEl.textContent = H > 0 ? '→ effective height: ' + H.toFixed(2) + ' m' : '';
|
||
}
|
||
}
|
||
|
||
toggleAccordion() {
|
||
const header = document.getElementById('loAccordionToggle');
|
||
const content = document.getElementById('loAccordionContent');
|
||
const isOpen = content.classList.contains('active');
|
||
content.classList.toggle('active');
|
||
header.classList.toggle('active');
|
||
}
|
||
loadPreset(id) {
|
||
if (id === 'custom') { this.clearPresets(); return; }
|
||
const p = this.optimizer.presets[id];
|
||
if (!p) return;
|
||
document.getElementById('customLength').value = p.length;
|
||
document.getElementById('customWidth').value = p.width;
|
||
document.getElementById('customHeight').value = p.height;
|
||
document.getElementById('customMaxWeight').value = p.maxWeight;
|
||
this.updatePresetBtns(id);
|
||
this.currentPreset = id;
|
||
this.updateEffectiveHints();
|
||
}
|
||
updatePresetBtns(activeId) {
|
||
document.querySelectorAll('.lo-preset-btn').forEach(b => b.classList.remove('active'));
|
||
if (activeId) document.querySelector('[data-preset="'+activeId+'"]')?.classList.add('active');
|
||
}
|
||
clearPresets() { this.updatePresetBtns(null); this.currentPreset = null; }
|
||
|
||
getMargins() {
|
||
const sideM = (parseFloat(document.getElementById('loMarginSide').value) || 0) / 100;
|
||
const ceilM = (parseFloat(document.getElementById('loMarginCeiling').value) || 0) / 100;
|
||
return { sideM, ceilM };
|
||
}
|
||
|
||
calculate() {
|
||
const L = parseFloat(document.getElementById('customLength').value);
|
||
const W = parseFloat(document.getElementById('customWidth').value);
|
||
const H = parseFloat(document.getElementById('customHeight').value);
|
||
const MW = parseFloat(document.getElementById('customMaxWeight').value);
|
||
if ([L,W,H,MW].some(v => isNaN(v) || v <= 0)) {
|
||
alert('Please fill in all transport dimensions!');
|
||
return;
|
||
}
|
||
const rollData = this.getRollData();
|
||
if (!rollData) {
|
||
alert('Please complete a roll calculation first (Direct Calculation tab).\nRoll Diameter, Roll Width and Roll Weight are required.');
|
||
return;
|
||
}
|
||
const { sideM, ceilM } = this.getMargins();
|
||
this.optimizer.setRollData(rollData);
|
||
const res = this.optimizer.calculateLoadUtilization({ length:L, width:W, height:H }, MW, sideM, ceilM);
|
||
this.showResults(res, MW);
|
||
this.drawSideView(res, { length:L, width:W, height:H });
|
||
}
|
||
|
||
getRollData() {
|
||
const D = parseFloat(document.getElementById('d-di')?.value || 0);
|
||
const W = parseFloat(document.getElementById('d-wi')?.value || 0);
|
||
const GW = parseFloat(document.getElementById('d-wo')?.value || 0);
|
||
const L = parseFloat(document.getElementById('d-le')?.value || 0);
|
||
const d = parseFloat(document.getElementById('d-co')?.value || 150);
|
||
if (!D || !W || !GW) return null;
|
||
return { rollDiameter:D, coreDiameter:d, rollWidth:W, rollWeight:GW, m2PerRoll:L*W, productLength:L };
|
||
}
|
||
|
||
showResults(res, maxW) {
|
||
const m2PerRoll = this.optimizer.state.m2PerRoll || 0;
|
||
const remainingPayload = Math.max(0, maxW - res.finalWeight);
|
||
const unusedPayload = Math.max(0, 100 - res.utilizationWeight);
|
||
const unusedVolume = Math.max(0, 100 - res.utilizationVolume);
|
||
const interpretations = {
|
||
geometry: 'Geometry prevents loading additional rolls although payload capacity remains available.',
|
||
weight: 'Payload limit is reached before the available geometry or volume is fully used.',
|
||
volume: 'Volume capacity is reached before payload or geometric capacity is fully used.'
|
||
};
|
||
document.getElementById('limitingFactorValue').textContent = res.limiting.name;
|
||
document.getElementById('totalRollsValue').textContent = res.finalRolls;
|
||
document.getElementById('rollsDetailValue').textContent =
|
||
'(' + res.rollsAlongLength + ' along length × ' + res.rollsAlongWidth + ' across, ' + res.maxStackHeight + ' layers)';
|
||
document.getElementById('weightUtilValue').textContent = this.optimizer.fmt(res.utilizationWeight);
|
||
document.getElementById('volumeUtilValue').textContent = this.optimizer.fmt(res.utilizationVolume);
|
||
document.getElementById('productM2Value').textContent = this.optimizer.fmt(res.finalM2, 0);
|
||
document.getElementById('remainingPayloadValue').textContent = this.optimizer.fmt(remainingPayload, 0);
|
||
document.getElementById('unusedPayloadValue').textContent = this.optimizer.fmt(unusedPayload);
|
||
document.getElementById('unusedVolumeValue').textContent = this.optimizer.fmt(unusedVolume);
|
||
document.getElementById('capacityInterpretationValue').textContent =
|
||
interpretations[res.limiting.type] || '';
|
||
const tbody = document.querySelector('#comparisonTable tbody');
|
||
tbody.innerHTML = '';
|
||
res.scenarios.forEach(sc => {
|
||
const tr = document.createElement('tr');
|
||
const lim = (sc.type === res.limiting.type);
|
||
if (lim) tr.classList.add('lo-governing');
|
||
const util = maxW > 0 ? (sc.weight / maxW * 100).toFixed(1) : '–';
|
||
const scM2 = isFinite(sc.rolls) && m2PerRoll > 0 ? Math.round(sc.rolls * m2PerRoll) : '–';
|
||
tr.innerHTML =
|
||
'<td>'+ sc.name +(lim?' → Governing Constraint':'')+'</td>'+
|
||
'<td>'+ (sc.rolls === Infinity ? '–' : sc.rolls) +'</td>'+
|
||
'<td>'+ (isFinite(sc.weight) ? Math.round(sc.weight) : '–') +'</td>'+
|
||
'<td>'+ scM2 +'</td>'+
|
||
'<td>'+ util +'%</td>';
|
||
tbody.appendChild(tr);
|
||
});
|
||
document.getElementById('resultsContainer').classList.add('show');
|
||
}
|
||
|
||
drawSideView(res, dim) {
|
||
const svg = document.getElementById('sideViewSVG');
|
||
const section = document.getElementById('sideViewSection');
|
||
if (!svg || !res || res.finalRolls === 0) return;
|
||
|
||
const D_m = this.optimizer.state.rollDiameter / 1000;
|
||
const aW = res.rollsAlongWidth;
|
||
const aL = res.rollsAlongLength;
|
||
const layers = res.maxStackHeight;
|
||
const total = res.finalRolls;
|
||
const marginSide = res.marginSide || 0;
|
||
const marginCeil = res.marginCeiling || 0;
|
||
|
||
const PAD = 52, W_SVG = 800, H_SVG = 300;
|
||
const scaleX = (W_SVG - 2*PAD) / dim.width;
|
||
const scaleY = (H_SVG - 2*PAD) / dim.height;
|
||
const scale = Math.min(scaleX, scaleY);
|
||
|
||
const cW = dim.width * scale;
|
||
const cH = dim.height * scale;
|
||
const cX = PAD + ((W_SVG - 2*PAD) - cW) / 2;
|
||
const cY = PAD + ((H_SVG - 2*PAD) - cH) / 2;
|
||
const rD = D_m * scale;
|
||
|
||
const mSidePx = marginSide * scale;
|
||
const mCeilPx = marginCeil * scale;
|
||
|
||
let s = '';
|
||
s += '<defs>';
|
||
s += '<marker id="arr" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="#666"/></marker>';
|
||
s += '<marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto-start-reverse"><path d="M0,0 L0,6 L6,3 z" fill="#666"/></marker>';
|
||
s += '<pattern id="hatchMargin" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)">';
|
||
s += '<rect width="8" height="8" fill="#fff7e0"/>';
|
||
s += '<line x1="0" y1="0" x2="0" y2="8" stroke="#f0b429" stroke-width="2.5"/>';
|
||
s += '</pattern>';
|
||
s += '</defs>';
|
||
|
||
s += '<rect x="0" y="0" width="800" height="300" fill="#f8f9fa"/>';
|
||
s += '<rect x="'+cX.toFixed(1)+'" y="'+cY.toFixed(1)+'" width="'+cW.toFixed(1)+'" height="'+cH.toFixed(1)+'" fill="#e8e8e8" stroke="#888" stroke-width="2.5" rx="3"/>';
|
||
s += '<rect x="'+cX.toFixed(1)+'" y="'+(cY+cH-5).toFixed(1)+'" width="'+cW.toFixed(1)+'" height="5" fill="#bbb"/>';
|
||
|
||
if (mSidePx > 0.5) {
|
||
s += '<rect x="'+(cX+cW-mSidePx).toFixed(1)+'" y="'+cY.toFixed(1)+'"'
|
||
+ ' width="'+mSidePx.toFixed(1)+'" height="'+cH.toFixed(1)+'"'
|
||
+ ' fill="url(#hatchMargin)" opacity="0.9"/>';
|
||
}
|
||
if (mCeilPx > 0.5) {
|
||
s += '<rect x="'+cX.toFixed(1)+'" y="'+cY.toFixed(1)+'"'
|
||
+ ' width="'+cW.toFixed(1)+'" height="'+mCeilPx.toFixed(1)+'"'
|
||
+ ' fill="url(#hatchMargin)" opacity="0.9"/>';
|
||
}
|
||
|
||
let drawn = 0;
|
||
for (let layer = 0; layer < layers && drawn < total; layer++) {
|
||
for (let col = 0; col < aW && drawn < total; col++) {
|
||
const cx = cX + col * rD + rD / 2;
|
||
const cy = cY + cH - layer * rD - rD / 2 - 5;
|
||
const r = Math.max(1, rD / 2 - 1.0);
|
||
s += '<circle cx="'+(cx+1.5).toFixed(1)+'" cy="'+(cy+1.5).toFixed(1)+'" r="'+r.toFixed(1)+'" fill="rgba(0,0,0,0.08)"/>';
|
||
s += '<circle cx="'+cx.toFixed(1)+'" cy="'+cy.toFixed(1)+'" r="'+r.toFixed(1)+'" fill="#a8d4f5" stroke="#2BAC70" stroke-width="1.2"/>';
|
||
if (rD > 16) {
|
||
const kr = Math.max(1.5, rD * 0.13);
|
||
s += '<circle cx="'+cx.toFixed(1)+'" cy="'+cy.toFixed(1)+'" r="'+kr.toFixed(1)+'" fill="white" stroke="#1F5438" stroke-width="1" opacity="0.8"/>';
|
||
}
|
||
drawn++;
|
||
}
|
||
}
|
||
|
||
const dimY = cY + cH + 20;
|
||
s += '<line x1="'+cX.toFixed(1)+'" y1="'+dimY.toFixed(1)+'" x2="'+(cX+cW).toFixed(1)+'" y2="'+dimY.toFixed(1)+'" stroke="#666" stroke-width="1" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
|
||
s += '<text x="'+(cX+cW/2).toFixed(1)+'" y="'+(dimY+13).toFixed(1)+'" text-anchor="middle" font-size="11" fill="#555">'+dim.width+' m</text>';
|
||
|
||
const dimX = cX - 22;
|
||
const midY = (cY + cH/2).toFixed(1);
|
||
s += '<line x1="'+dimX.toFixed(1)+'" y1="'+cY.toFixed(1)+'" x2="'+dimX.toFixed(1)+'" y2="'+(cY+cH).toFixed(1)+'" stroke="#666" stroke-width="1" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
|
||
s += '<text x="'+(dimX-5).toFixed(1)+'" y="'+midY+'" text-anchor="middle" font-size="11" fill="#555" transform="rotate(-90,'+(dimX-5).toFixed(1)+','+midY+')">'+dim.height+' m</text>';
|
||
|
||
s += '<text x="'+(cX+cW/2).toFixed(1)+'" y="'+(cY-8).toFixed(1)+'" text-anchor="middle" font-size="11" fill="#666" font-style="italic">'+aL+' rows deep (into page)</text>';
|
||
s += '<text x="'+(cX+cW-4).toFixed(1)+'" y="'+(cY+15).toFixed(1)+'" text-anchor="end" font-size="10" fill="#555" font-style="italic">'+aW+' wide × '+layers+' high × '+aL+' deep · '+total+' rolls</text>';
|
||
|
||
if (rD > 30 && aW > 0) {
|
||
const rx1 = cX, rx2 = cX + rD, ry = cY + cH - rD/2 - 5;
|
||
s += '<line x1="'+rx1.toFixed(1)+'" y1="'+(ry+rD/2+10).toFixed(1)+'" x2="'+rx2.toFixed(1)+'" y2="'+(ry+rD/2+10).toFixed(1)+'" stroke="#2BAC70" stroke-width="0.8" stroke-dasharray="3,2" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
|
||
s += '<text x="'+(cX+rD/2).toFixed(1)+'" y="'+(ry+rD/2+22).toFixed(1)+'" text-anchor="middle" font-size="10" fill="#2BAC70">⌀ '+(D_m*1000).toFixed(0)+' mm</text>';
|
||
}
|
||
|
||
svg.setAttribute('viewBox', '0 0 800 300');
|
||
svg.innerHTML = s;
|
||
section.classList.add('show');
|
||
}
|
||
|
||
resetForm() {
|
||
['customLength','customWidth','customHeight','customMaxWeight'].forEach(id =>
|
||
document.getElementById(id).value = ''
|
||
);
|
||
document.getElementById('loMarginSide').value = '0';
|
||
document.getElementById('loMarginCeiling').value = '20';
|
||
document.getElementById('loMarginSideEffective').textContent = '';
|
||
document.getElementById('loMarginCeilingEffective').textContent = '';
|
||
document.getElementById('resultsContainer').classList.remove('show');
|
||
document.getElementById('sideViewSection').classList.remove('show');
|
||
this.clearPresets();
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// INIT
|
||
// =====================================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.loadOptimizerUI = new LoadOptimizerUI();
|
||
});
|
||
|
||
const ROLLCALC_BUILD_VERSION = {{ build_info.version|tojson }};
|
||
const ROLLCALC_LAST_SEEN_VERSION_KEY = 'rollcalc_last_seen_version';
|
||
|
||
function shouldShowWhatsNew(currentVersion, storedVersion) {
|
||
return !!currentVersion &&
|
||
currentVersion !== 'unknown' &&
|
||
currentVersion !== storedVersion;
|
||
}
|
||
|
||
function markWhatsNewSeen(currentVersion) {
|
||
if (!currentVersion || currentVersion === 'unknown') return;
|
||
try {
|
||
localStorage.setItem(ROLLCALC_LAST_SEEN_VERSION_KEY, currentVersion);
|
||
} catch (e) {
|
||
console.warn('Could not store RollCalc release note state', e);
|
||
}
|
||
}
|
||
|
||
function renderWhatsNewItems(items) {
|
||
const list = document.getElementById('whatsNewList');
|
||
if (!list) return;
|
||
|
||
list.innerHTML = '';
|
||
items.forEach(item => {
|
||
const li = document.createElement('li');
|
||
li.textContent = item;
|
||
list.appendChild(li);
|
||
});
|
||
}
|
||
|
||
async function loadWhatsNewData(currentVersion) {
|
||
try {
|
||
const resp = await fetch(ROLLCALC_RECENT_CHANGES_JSON_URL);
|
||
if (!resp.ok) {
|
||
console.warn('Could not load RollCalc release notes summary:', resp.status);
|
||
return null;
|
||
}
|
||
|
||
const data = await resp.json();
|
||
if (!data || typeof data.version !== 'string' || !Array.isArray(data.items)) {
|
||
console.warn('Invalid RollCalc release notes summary format');
|
||
return null;
|
||
}
|
||
|
||
if (data.version !== currentVersion) {
|
||
return null;
|
||
}
|
||
|
||
const items = data.items.filter(item =>
|
||
typeof item === 'string' && item.trim()
|
||
);
|
||
return items.length ? { version: data.version, items } : null;
|
||
} catch (e) {
|
||
console.warn('Could not load RollCalc release notes summary', e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function maybeShowWhatsNew() {
|
||
const overlay = document.getElementById('whatsNewOverlay');
|
||
const versionValue = document.getElementById('whatsNewVersion');
|
||
const continueBtn = document.getElementById('whatsNewContinueBtn');
|
||
const openChangesBtn = document.getElementById('whatsNewOpenChangesBtn');
|
||
|
||
if (!overlay || !versionValue || !continueBtn || !openChangesBtn) return;
|
||
|
||
let storedVersion = null;
|
||
try {
|
||
storedVersion = localStorage.getItem(ROLLCALC_LAST_SEEN_VERSION_KEY);
|
||
} catch (e) {
|
||
storedVersion = null;
|
||
}
|
||
|
||
if (!shouldShowWhatsNew(ROLLCALC_BUILD_VERSION, storedVersion)) return;
|
||
|
||
const releaseData = await loadWhatsNewData(ROLLCALC_BUILD_VERSION);
|
||
if (!releaseData) return;
|
||
|
||
versionValue.textContent = ROLLCALC_BUILD_VERSION;
|
||
renderWhatsNewItems(releaseData.items);
|
||
overlay.classList.remove('is-hidden');
|
||
|
||
continueBtn.addEventListener('click', () => {
|
||
markWhatsNewSeen(ROLLCALC_BUILD_VERSION);
|
||
overlay.classList.add('is-hidden');
|
||
}, { once: true });
|
||
|
||
openChangesBtn.addEventListener('click', () => {
|
||
markWhatsNewSeen(ROLLCALC_BUILD_VERSION);
|
||
window.location.href = {{ url_for('recent_changes')|tojson }};
|
||
}, { once: true });
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const overlay = document.getElementById('disclaimerOverlay');
|
||
const checkbox = document.getElementById('disclaimerAccepted');
|
||
const confirmBtn = document.getElementById('disclaimerConfirmBtn');
|
||
|
||
if (!overlay || !checkbox || !confirmBtn) {
|
||
maybeShowWhatsNew();
|
||
return;
|
||
}
|
||
|
||
checkbox.addEventListener('change', () => {
|
||
confirmBtn.disabled = !checkbox.checked;
|
||
});
|
||
|
||
confirmBtn.addEventListener('click', () => {
|
||
if (!checkbox.checked) return;
|
||
overlay.classList.add('is-hidden');
|
||
maybeShowWhatsNew();
|
||
});
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|