Framing and Surface Mapping for Aircraft

This is a code for some aircraft I’m working on.

Honestly, I’m fairly new to three, only been using it for a few years.

One thing I’ve never built is an airplane

I figured I would start with a simple drone, and a stealth bomber for my first aircraft models.

So, what I came up with is an elliptical tube generator that allows customization of the radius and offset from the axis for each point (similarly to drawing centerlines and offsetting ellipses from the centerline for each critical point along the wings)..

Here’s a simple model of my drone:

And here’s one of the bomber:

I know they aren’t too great, but I’m curious how you’d go about making this properly..

I figured you’d want to trace the outer perimeter for what I have here and then create a mesh that blends in with the inlets for the engines and also the fuselage’s body.

I assume you’d also need to build framing for this as well, which can get complicated.

This is all part of a project I’m working on for a webpage that should show a realistic model of each..

Here’s what I am aiming for, a mesh that uses these geometries to join together and create a shell, hollow for a frame:

Here’s my code too:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Aerospace Command - Unified Airframes</title>
<link href="https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@700&display=swap" rel="stylesheet">

<style>
/* =========================
   DARK ENCHANTED THEME
   ========================= */

:root{
    --bg0:#07050d;
    --bg1:#0e0a18;
    --bg2:#151022;
    --panel:#120c1f;
    --panel2:#1a1230;
    --text:#e9e2ff;
    --muted:#b7a8e6;
    --accent:#a46bff;
    --accent2:#6a3cff;
    --glow:rgba(164,107,255,0.35);
    --danger:#ff3b5c;
    --line:#2a1f44;
}

body {
    margin:0; min-height:100vh; display:flex; flex-direction:column; align-items:center; overflow-x:hidden;
    font-family:sans-serif; color:var(--text);
    background:
        radial-gradient(circle at 20% 10%, rgba(164,107,255,0.15), transparent 40%),
        radial-gradient(circle at 80% 30%, rgba(106,60,255,0.12), transparent 45%),
        radial-gradient(circle at 50% 90%, rgba(53,242,166,0.08), transparent 50%),
        linear-gradient(180deg, var(--bg0), var(--bg1), var(--bg2));
}

.top-panel {
    width: 100%;
    text-align: center;
    box-sizing: border-box;
}

.controls {
    margin: 10px auto 0 auto; 
    display: flex; 
    flex-wrap: nowrap; 
    gap: 12px; 
    align-items: center; 
    justify-content: center;
    width: 95vw; 
    max-width: 1200px; 
    box-sizing: border-box;
    padding: 12px 20px; 
    border-radius: 10px; 
    background: linear-gradient(180deg, var(--panel), var(--panel2));
    border: 1px solid var(--line); 
    box-shadow: 0 0 18px var(--glow), inset 0 0 12px rgba(164,107,255,0.08);
    z-index: 1000; 
    font-size: 12px; 
    overflow-x: auto; 
    white-space: nowrap;
}

.controls::-webkit-scrollbar { height: 8px; }
.controls::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); border-radius: 4px; }
.controls::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 4px; box-shadow: inset 0 0 4px rgba(0,0,0,0.5); }

.controls > * { flex-shrink: 0; }
.controls label { display: flex; align-items: center; gap: 6px; color: var(--text); font-weight: bold; }

.controls select, .controls input {
    background:#0f0a1d; color:var(--text); border:1px solid #3a2a66; padding:6px 10px; border-radius:6px;
    font-size:11px; outline:none; box-shadow: inset 0 0 6px rgba(0,0,0,0.6); cursor:pointer;
}
.controls select:focus, .controls input:focus { border-color:var(--accent); box-shadow:0 0 10px var(--glow); }

.btn-action {
    background:linear-gradient(135deg, var(--accent2), var(--accent)); color:white; border:none;
    padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:bold; font-size:11px;
    box-shadow:0 0 12px var(--glow); transition:0.2s ease;
}
.btn-action:hover { transform:translateY(-1px); box-shadow:0 0 18px var(--glow); }
.btn-danger { background:linear-gradient(135deg, #8b1e2d, var(--danger)); }

.side-nav-btn {
    position:fixed; top:50%; transform:translateY(-50%); width:45px; height:80px; border-radius:10px; cursor:pointer;
    background:linear-gradient(180deg, #120c1f, #0b0814); color:var(--text); border:1px solid #3a2a66;
    box-shadow:0 0 16px var(--glow); font-size:20px; transition:0.2s ease; z-index:999;
}
.side-nav-btn:hover { background:var(--accent); color:#0b0616; box-shadow:0 0 22px var(--glow); }
.side-nav-left { left:15px; } .side-nav-right { right:15px; }

.svg-container {
    width:95vw; max-width:1200px; aspect-ratio:36 / 24; min-height: 500px; margin-top:10px; position:relative; overflow:hidden;
    background: radial-gradient(circle at center, rgba(164,107,255,0.08), transparent 60%), linear-gradient(180deg, #0a0712, #07050d);
    border:1px solid #2b1d4a; box-shadow: 0 0 30px rgba(164,107,255,0.25), inset 0 0 40px rgba(0,0,0,0.6);
}

.sheet-page { display:none; width:100%; height:100%; position:relative; }
.sheet-page.active { display:block; }
.vp-single { width:100%; height:100%; position:absolute; inset:0; z-index:10; }
.sheet-border { position:absolute; inset:0.5in; border:1px solid rgba(164,107,255,0.4); pointer-events:none; z-index:50; }

.title-block {
    position:absolute; bottom:0.58in; right:0.58in; width:300px; border:1px solid #3a2a66;
    background:linear-gradient(180deg, #120c1f, #0b0814); z-index:100; font-family:Arial, sans-serif;
}
.title-row { display:flex; border-bottom:1px solid #2a1f44; }
.title-row:last-child { border-bottom:none; }
.title-cell { padding:4px; font-size:9px; flex:1; color:var(--muted); border-right:1px solid #2a1f44; }
.title-cell:last-child { border-right:none; }
.cell-bold { font-weight:bold; font-size:12px; margin-top:1px; text-transform:uppercase; color:var(--text); }
.text-welcome-brand {
    font-family:'Cabin Sketch', cursive; font-size:22px; text-align:center;
    background:linear-gradient(90deg, #0b0616, #1a1230); color:var(--accent);
    padding:4px; border-bottom:1px solid #3a2a66; text-shadow:0 0 10px var(--glow);
}

.view-tag { position:absolute; left:0.8in; bottom:0.8in; display:flex; align-items:center; z-index:20; }
.vt-circle {
    width:34px; height:34px; border-radius:50%; border:1px solid var(--accent); display:flex; align-items:center; justify-content:center;
    font-weight:bold; font-size:16px; background:rgba(20,14,35,0.8); box-shadow:0 0 10px var(--glow); margin-right:-2px;
}
.vt-lines { display:flex; flex-direction:column; justify-content:center; }
.vt-name { font-size:15px; font-weight:bold; letter-spacing:1px; padding-left:10px; margin-bottom:2px; text-transform:uppercase; color:var(--text); }
.vt-line { height:2px; width:220px; background:linear-gradient(90deg, var(--accent), transparent); }
.vt-scale { font-size:11px; padding-left:10px; margin-top:3px; text-transform:uppercase; font-weight:bold; color:var(--muted); }

/* SHARED MODAL STYLES */
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.85); z-index:2000; justify-content:center; align-items:center; backdrop-filter: blur(3px); }
.modal-content { width:750px; max-height: 90vh; overflow-y: auto; background:linear-gradient(180deg, #120c1f, #0b0814); border:1px solid #3a2a66; color:var(--text); padding:20px; border-radius:10px; box-shadow:0 0 25px var(--glow); display:flex; flex-direction:column; gap:10px; }
.modal-header { display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #2a1f44; padding-bottom:10px; }
.modal-header h3 { margin:0; font-size:18px; color:var(--accent); }
.close-btn { background:linear-gradient(135deg, #2a1f44, #3b2a66); padding:8px 12px; border:none; color:var(--text); cursor:pointer; border-radius:6px; font-weight:bold; box-shadow:0 0 10px rgba(164,107,255,0.2); transition:0.2s;}
.close-btn:hover { box-shadow:0 0 16px var(--glow); filter:brightness(1.2);}

/* MODAL TABS */
.tabs { display: flex; gap: 6px; border-bottom: 2px solid #2a1f44; padding-bottom: 10px; flex-wrap: wrap; margin-bottom:10px; }
.tab-btn { background: #1a1230; color: #b7a8e6; border: 1px solid #3a2a66; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 11px; transition: 0.2s; }
.tab-btn:hover { background: #2a1f44; color: white; }
.tab-btn.active { background: linear-gradient(135deg, var(--accent2), var(--accent)); color: white; border-color: var(--accent); box-shadow: 0 0 10px var(--glow); }
.tab-content { display: none; }
.tab-content.active { display: block; }

.setting-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; margin-bottom: 10px; }
.setting-row select, .setting-row input[type="range"] { width: 200px; }

/* TABLE EDITOR */
.editor-table { display:flex; flex-direction:column; gap:6px; max-height: 60vh; overflow-y: auto; padding-right:10px; }
.editor-table::-webkit-scrollbar { width: 8px; }
.editor-table::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); border-radius: 4px; }
.editor-table::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 4px; }
.editor-row { display:flex; gap:8px; align-items:center; background:#140e25; padding:8px 12px; border-radius:6px; border:1px solid #2a1f44; }
.editor-row b { width: 40px; font-size: 11px; color: #00e5ff; }
.editor-row .input-group { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.editor-row .input-group label, .param-grid label { font-size: 9px; color: var(--muted); margin: 0; }
.editor-row input, .param-grid input { background:#0a0712; color:white; border:1px solid #3a2a66; padding:6px; border-radius:4px; outline:none; font-size:11px; width:100%; box-sizing:border-box;}
.editor-row input:focus, .param-grid input:focus { border-color: var(--accent); }

.param-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; background: #1a1230; padding: 12px; border-radius: 6px; border: 1px solid #3a2a66; margin-bottom: 10px; }
.param-grid .input-group { display: flex; flex-direction: column; gap: 4px; }
</style>
</head>
<body>

<button class="side-nav-btn side-nav-left" onclick="window.changePage(-1)">&#10094;</button>
<button class="side-nav-btn side-nav-right" onclick="window.changePage(1)">&#10095;</button>

<div class="top-panel">
    <div class="controls">
      <strong style="font-family:'Cabin Sketch', cursive; color: #00e5ff; font-size: 16px; margin-right: 10px;">AEROSPACE COMMAND</strong>
      
      <label>Airframe: 
      <select id="craftConfig" onchange="window.updateModel()">
          <option value="drone_legacy">Alpha Concept (Legacy Drone)</option>
          <option value="b2_bomber" selected>B-2 Spirit (Stealth Bomber)</option>
      </select>
      </label>
      
      <label>Render: 
      <select id="renderStyle" onchange="window.updateModel()">
          <option value="shaded" selected>Photorealistic PBR</option>
          <option value="wireframe">Diagnostic Wireframe</option>
          <option value="xray">Diagnostic X-Ray</option>
      </select>
      </label>

      <div style="border-left:1px solid var(--line); height:24px; margin: 0 5px;"></div>

      <div id="legacy-controls" style="display:none; gap:12px;">
          <button class="btn-action" onclick="openDroneModal()">⚙️ CONFIGURE AIRFRAME</button>
      </div>

      <div id="b2-controls" style="display:flex; gap:12px;">
          <button class="btn-action" onclick="openB2Modal()">⚙️ CONFIGURE AIRFRAME</button>
      </div>
    </div>
</div>

<!-- DRONE CONFIG MODAL -->
<div class="modal-overlay" id="droneConfigModal">
    <div class="modal-content">
        <div class="modal-header">
            <h3>LEGACY DRONE CONFIGURATION</h3>
            <button class="close-btn" onclick="closeDroneModal()">&times; CLOSE</button>
        </div>
        <div class="tabs">
            <button class="tab-btn active" onclick="switchDroneTab(event, 'tab-dims')">DIMENSIONS</button>
            <button class="tab-btn" onclick="switchDroneTab(event, 'tab-loadout')">LOADOUT</button>
            <button class="tab-btn" onclick="switchDroneTab(event, 'tab-defense')">DEFENSE</button>
        </div>
        
        <div id="tab-dims" class="tab-content active drone-tab">
            <div class="setting-row">
                <label>Wingspan Scale:</label>
                <input type="range" id="paramWingSpan" min="50" max="150" value="100" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label>Fuselage Length:</label>
                <input type="range" id="paramFuseLen" min="70" max="130" value="100" oninput="window.updateModel()">
            </div>
            <div class="setting-row" style="margin-top:15px; border-top:1px solid #2a1f44; padding-top:10px;">
                <label style="color:var(--accent);">Wing Sweep (Front ◄—► Back):</label>
                <input type="range" id="paramWingSweep" min="-45" max="60" value="-5" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:var(--accent);">Wing Dihedral (X-Plane):</label>
                <input type="range" id="paramWingDihedral" min="-15" max="30" value="2" oninput="window.updateModel()">
            </div>
            <div class="setting-row" style="margin-top:15px; border-top:1px solid #2a1f44; padding-top:10px;">
                <label style="color:#00e5ff;">Fin Scale (Size):</label>
                <input type="range" id="paramFinScale" min="30" max="200" value="150" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:#00e5ff;">Fin Sweep (Front ◄—► Back):</label>
                <input type="range" id="paramFinSweep" min="-45" max="75" value="0" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:#00e5ff;">Fin V-Angle (X-Plane):</label>
                <input type="range" id="paramFinAngle" min="0" max="90" value="60" oninput="window.updateModel()">
            </div>
        </div>
        
        <div id="tab-loadout" class="tab-content drone-tab">
            <div class="setting-row">
                <label>Wing Hardpoints:</label>
                <select id="loadoutWeapons" onchange="window.updateModel()">
                    <option value="none" selected>Clean (Stealth Configuration)</option>
                    <option value="aam">Interceptor (Light Air-to-Air)</option>
                    <option value="agm">Strike (Heavy Air-to-Ground)</option>
                </select>
            </div>
        </div>
        
        <div id="tab-defense" class="tab-content drone-tab">
            <div class="setting-row">
                <label>Dorsal Sensor Array:</label>
                <select id="loadoutRadar" onchange="window.updateModel()">
                    <option value="none" selected>Internal Scanners Only</option>
                    <option value="awacs">AWACS Overwatch Radome</option>
                </select>
            </div>
            <div class="setting-row">
                <label>Countermeasures:</label>
                <select id="loadoutFlares" onchange="window.updateModel()">
                    <option value="none" selected>None</option>
                    <option value="flares">Chaff / Flare Dispensers</option>
                </select>
            </div>
        </div>
    </div>
</div>

<!-- B-2 CONFIG MODAL -->
<div class="modal-overlay" id="b2ConfigModal">
    <div class="modal-content">
        <div class="modal-header">
            <h3>B-2 SPIRIT CONFIGURATION</h3>
            <button class="close-btn" onclick="closeB2Modal()">&times; CLOSE</button>
        </div>
        <div class="tabs">
            <button class="tab-btn b2-tab-btn active" onclick="switchB2Tab(event, 'fuseData')">FUSELAGE</button>
            <button class="tab-btn b2-tab-btn" onclick="switchB2Tab(event, 'wingData')">WINGS</button>
            <button class="tab-btn b2-tab-btn" onclick="switchB2Tab(event, 'finData')">FINS</button>
            <button class="tab-btn b2-tab-btn" onclick="switchB2Tab(event, 'engineData')">ENGINES</button>
        </div>

        <div id="b2EngineParams" class="param-grid" style="display:none;">
            <!-- Injected via JS -->
        </div>

        <div class="editor-table" id="b2EditorContainer">
            <!-- Rows injected via JS -->
        </div>
        <button class="btn-action" style="margin-top:10px; padding:12px; font-size:13px;" onclick="addB2CrossSection()">+ ADD CROSS-SECTION POINT</button>
    </div>
</div>

<div class="svg-container" id="sheets-container"></div>

<script type="importmap">
{
    "imports": {
        "three": "https://unpkg.com/three@0.128.0/build/three.module.js",
        "csg": "https://cdn.jsdelivr.net/gh/manthrax/THREE-CSGMesh/three-csg.js"
    }
}
</script>

<script type="module">
import * as THREE from "three";
import CSG from "csg";

/* ==========================================
   B-2 PROCEDURAL DATA ARRAYS
========================================== */

window.fuseData = [
    { radiusX: 0.05, radiusY: 0.02, yOffset: 0.0,  x: 0.0, z: -3.8 },
    { radiusX: 0.8,  radiusY: 0.25, yOffset: 0.1,  x: 0.0, z: -2.0 },
    { radiusX: 1.5,  radiusY: 0.38, yOffset: 0.15, x: 0.0, z: -0.5 },
    { radiusX: 1.8,  radiusY: 0.28, yOffset: 0.1,  x: 0.0, z: 1.0 }, 
    { radiusX: 1.2,  radiusY: 0.15, yOffset: 0.05, x: 0.0, z: 2.5 }, 
    { radiusX: 0.6,  radiusY: 0.05, yOffset: 0.02, x: 0.0, z: 4.0 }, 
    { radiusX: 0.1,  radiusY: 0.01, yOffset: 0.0,  x: 0.0, z: 5.0 }  
];

window.wingData = [
    { radiusX: 4.1, radiusY: 0.25, yOffset: 0.2, x: 0.9, z: 0.0 },
    { radiusX: 3.1, radiusY: 0.20, yOffset: 0.2, x: 1.5, z: 2.5 },
    { radiusX: 2.7, radiusY: 0.15, yOffset: 0.2, x: 2.6, z: 4.6 },
    { radiusX: 1.4, radiusY: 0.10, yOffset: 0.2, x: 2.7, z: 6.6 },
    { radiusX: 0.7, radiusY: 0.05, yOffset: 0.2, x: 4.5, z: 10.0 },
    { radiusX: 0.1, radiusY: 0.05, yOffset: 0.2, x: 4.7, z: 11.0 }
];

window.finData = [
    { radiusX: 0.01, radiusY: 0.01, yOffset: 0.0, x: 0, z: 0.0 },
    { radiusX: 0.01, radiusY: 0.01, yOffset: 0.0, x: 0, z: 0.1 }
];

window.engineData = [
    { radius: 0.2,  y: 0.5 },
    { radius: 0.4,  y: -0.2 },
    { radius: 0.5,  y: 1.0 },
    { radius: 0.35, y: 2.5 },
    { radius: 0.3,  y: 3.0 }
];

window.engineParams = {
    offsetX: 1.6,
    offsetY: 0.45,
    offsetZ: 1.5,
    scaleX: 1.8,
    scaleY: 0.35,
    scaleZ: 1.0
};

/* ==========================================
   UI CONTROLS & MODALS
========================================== */

// DRONE MODAL
window.openDroneModal = () => document.getElementById('droneConfigModal').style.display = 'flex';
window.closeDroneModal = () => document.getElementById('droneConfigModal').style.display = 'none';
window.switchDroneTab = (event, tabId) => {
    document.querySelectorAll('.drone-tab').forEach(el => el.classList.remove('active'));
    event.currentTarget.parentElement.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
    document.getElementById(tabId).classList.add('active');
    event.currentTarget.classList.add('active');
};

// B-2 MODAL
let currentB2EditorArray = 'fuseData';

window.openB2Modal = () => {
    document.getElementById('b2ConfigModal').style.display = 'flex';
    // Initialize with first tab active
    const firstTab = document.querySelector('.b2-tab-btn');
    switchB2Tab({currentTarget: firstTab}, 'fuseData');
};

window.closeB2Modal = () => { document.getElementById('b2ConfigModal').style.display = 'none'; };

window.switchB2Tab = (event, arrayName) => {
    document.querySelectorAll('.b2-tab-btn').forEach(el => el.classList.remove('active'));
    event.currentTarget.classList.add('active');
    
    currentB2EditorArray = arrayName;
    
    const paramsDiv = document.getElementById('b2EngineParams');
    if (arrayName === 'engineData') {
        paramsDiv.style.display = 'grid';
        renderB2EngineParams();
    } else {
        paramsDiv.style.display = 'none';
    }
    
    window.renderB2EditorRows();
};

window.renderB2EngineParams = () => {
    let ep = window.engineParams;
    let html = `
        <div class="input-group"><label>Offset X (Spread)</label><input type="number" step="0.1" value="${ep.offsetX}" onchange="updateB2EngineParam('offsetX', this.value)"></div>
        <div class="input-group"><label>Offset Y (Height)</label><input type="number" step="0.1" value="${ep.offsetY}" onchange="updateB2EngineParam('offsetY', this.value)"></div>
        <div class="input-group"><label>Offset Z (Forward/Back)</label><input type="number" step="0.1" value="${ep.offsetZ}" onchange="updateB2EngineParam('offsetZ', this.value)"></div>
        <div class="input-group"><label>Scale X (Width)</label><input type="number" step="0.1" value="${ep.scaleX}" onchange="updateB2EngineParam('scaleX', this.value)"></div>
        <div class="input-group"><label>Scale Y (Height)</label><input type="number" step="0.1" value="${ep.scaleY}" onchange="updateB2EngineParam('scaleY', this.value)"></div>
        <div class="input-group"><label>Scale Z (Length)</label><input type="number" step="0.1" value="${ep.scaleZ}" onchange="updateB2EngineParam('scaleZ', this.value)"></div>
    `;
    document.getElementById('b2EngineParams').innerHTML = html;
};

window.updateB2EngineParam = (prop, val) => {
    window.engineParams[prop] = parseFloat(val) || 0;
    window.updateModel();
};

window.renderB2EditorRows = () => {
    let arr = window[currentB2EditorArray];
    let html = '';
    
    if (currentB2EditorArray === 'engineData') {
        arr.forEach((sec, idx) => {
            html += `
            <div class="editor-row">
                <b>Pt ${idx}</b>
                <div class="input-group"><label>Lathe Radius</label><input type="number" step="0.05" value="${sec.radius}" onchange="updateB2Val(${idx}, 'radius', this.value)"></div>
                <div class="input-group"><label>Height (Y)</label><input type="number" step="0.1" value="${sec.y}" onchange="updateB2Val(${idx}, 'y', this.value)"></div>
                <button class="btn-danger" onclick="removeB2CrossSection(${idx})">X</button>
            </div>`;
        });
    } else {
        arr.forEach((sec, idx) => {
            html += `
            <div class="editor-row">
                <b>Pt ${idx}</b>
                <div class="input-group"><label>Radius X</label><input type="number" step="0.1" value="${sec.radiusX}" onchange="updateB2Val(${idx}, 'radiusX', this.value)"></div>
                <div class="input-group"><label>Radius Y</label><input type="number" step="0.1" value="${sec.radiusY}" onchange="updateB2Val(${idx}, 'radiusY', this.value)"></div>
                <div class="input-group"><label>Y Offset</label><input type="number" step="0.1" value="${sec.yOffset}" onchange="updateB2Val(${idx}, 'yOffset', this.value)"></div>
                <div class="input-group"><label>X Sweep</label><input type="number" step="0.1" value="${sec.x}" onchange="updateB2Val(${idx}, 'x', this.value)"></div>
                <div class="input-group"><label>Z Span</label><input type="number" step="0.1" value="${sec.z}" onchange="updateB2Val(${idx}, 'z', this.value)"></div>
                <button class="btn-danger" onclick="removeB2CrossSection(${idx})">X</button>
            </div>`;
        });
    }
    document.getElementById('b2EditorContainer').innerHTML = html;
};

window.updateB2Val = (idx, prop, val) => {
    window[currentB2EditorArray][idx][prop] = parseFloat(val) || 0;
    window.updateModel();
};

window.addB2CrossSection = () => {
    let arr = window[currentB2EditorArray];
    if (currentB2EditorArray === 'engineData') {
        let last = arr[arr.length - 1] || { radius: 0.1, y: 0 };
        arr.push({ radius: last.radius, y: last.y + 0.5 }); 
    } else {
        let last = arr[arr.length - 1] || { radiusX: 0.5, radiusY: 0.5, yOffset: 0, x: 0, z: 0 };
        arr.push({ ...last, z: last.z + 1.0 }); 
    }
    window.renderB2EditorRows();
    window.updateModel();
};

window.removeB2CrossSection = (idx) => {
    let arr = window[currentB2EditorArray];
    if (arr.length <= 2) { alert("Cannot delete. Profile requires at least 2 points."); return; }
    arr.splice(idx, 1);
    window.renderB2EditorRows();
    window.updateModel();
};

/* ==========================================
   3D ENGINE & REALISTIC SCENE SETUP
========================================== */
const viewList = ['1', '2', '3', '4', '5'];
const viewNames = [ "FRONT VIEW", "TOP VIEW", "SIDE VIEW", "ISOMETRIC VIEW", "EXPLODED SCHEMATIC" ];
let scenes = {}, cameras = {}, renderers = {};
let currentPage = 1, maxPages = 5;
let extraGeoms = [];
let sharedEnvTexture = null;

const container = document.getElementById('sheets-container');
viewList.forEach((v, idx) => {
    container.innerHTML += `
    <div id="sheet${v}" class="sheet-page ${v === '1' ? 'active' : ''}">
        <div class="vp-single" id="vp${v}"></div><div class="sheet-border"></div>
        <div class="view-tag">
            <div class="vt-circle">${v}</div>
            <div class="vt-lines">
                <div class="vt-name">${viewNames[idx]}</div>
                <div class="vt-line"></div>
                <div class="vt-scale">SCALE: VARIES</div>
            </div>
        </div>
        <div class="title-block">
            <div class="text-welcome-brand" id="brand-${v}">e-drafts.com</div>
            <div class="title-row"><div class="title-cell">CLASS<div class="cell-bold" id="class-${v}">...</div></div></div>
            <div class="title-row"><div class="title-cell">DESIGNATION<div class="cell-bold" id="title-${v}">...</div></div></div>
            <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">${viewNames[idx]}</div></div></div>
            <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold">NTS</div></div><div class="title-cell">SHEET<div class="cell-bold">${v} OF 5</div></div></div>
        </div>
    </div>`;
});

// PHOTOREALISTIC PBR MATERIALS 
const MATS = {
    darkArmor: new THREE.MeshPhysicalMaterial({ color: 0x1f2126, roughness: 0.4, metalness: 0.6, clearcoat: 0.1, clearcoatRoughness: 0.5, side: THREE.DoubleSide }),
    stealthSkin: new THREE.MeshPhysicalMaterial({ color: 0x141518, roughness: 0.7, metalness: 0.2, clearcoat: 0.05, clearcoatRoughness: 0.8, side: THREE.DoubleSide }),
    heavyMetal: new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.5, metalness: 0.8 }),
    opticGlass: new THREE.MeshPhysicalMaterial({ color: 0x0a0a0a, metalness: 0.9, roughness: 0.05, clearcoat: 1.0, transparent: true, opacity: 0.8 }),
    sensorLens: new THREE.MeshPhysicalMaterial({ color: 0x111111, metalness: 1.0, roughness: 0.0, clearcoat: 1.0 }),
    engineFire: new THREE.MeshBasicMaterial({ color: 0xff5500 }),
    flareGlow: new THREE.MeshBasicMaterial({ color: 0xffffff }),
    holoWire: new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.3 }),
    glass: new THREE.MeshPhysicalMaterial({ color: 0x050505, roughness: 0.0, metalness: 1.0, clearcoat: 1.0 }),
    metalGear: new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.3, metalness: 0.9 }),
    tireRubber: new THREE.MeshStandardMaterial({ color: 0x050505, roughness: 0.95, metalness: 0.0 }),
    bombBaySkin: new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8, metalness: 0.3 })
};

/* ==========================================
   PROCEDURAL ENVIRONMENT (FOR REFLECTIONS)
========================================== */
function createStudioEnvironment(renderer) {
    let pmremGenerator = new THREE.PMREMGenerator(renderer);
    pmremGenerator.compileEquirectangularShader();
    
    let envScene = new THREE.Scene();
    envScene.add(new THREE.DirectionalLight(0xffffff, 3.0).translateY(10));
    envScene.add(new THREE.DirectionalLight(0x88ccff, 2.0).translateX(-10).translateY(-10));
    envScene.add(new THREE.DirectionalLight(0xffaa88, 2.0).translateZ(-10));

    let boxGeo = new THREE.BoxGeometry(10, 10, 1);
    let p1 = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({color: 0xffffff})); p1.position.set(0, 15, 20); envScene.add(p1);
    let p2 = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({color: 0x88ccff})); p2.position.set(-20, 5, 0); p2.lookAt(0,0,0); envScene.add(p2);
    
    return pmremGenerator.fromScene(envScene).texture;
}

/* ==========================================
   SHARED PROCEDURAL HELPERS & GEAR ASSEMBLIES
========================================== */

class SpringCurve extends THREE.Curve {
    constructor(radius, height, coils) {
        super();
        this.radius = radius;
        this.height = height;
        this.coils = coils;
    }
    getPoint(t, optionalTarget = new THREE.Vector3()) {
        return optionalTarget.set(
            Math.cos(t * Math.PI * 2 * this.coils) * this.radius,
            -t * this.height,
            Math.sin(t * Math.PI * 2 * this.coils) * this.radius
        );
    }
}

function buildDetailedWheel(tireR, rimR, width) {
    let wGrp = new THREE.Group();
    // Lathe profile for tire bulge
    let tirePts = [
        new THREE.Vector2(rimR, width/2),
        new THREE.Vector2(tireR*0.85, width/2.1),
        new THREE.Vector2(tireR, width/4),
        new THREE.Vector2(tireR, -width/4),
        new THREE.Vector2(tireR*0.85, -width/2.1),
        new THREE.Vector2(rimR, -width/2)
    ];
    let tireGeo = new THREE.LatheGeometry(tirePts, 32); extraGeoms.push(tireGeo);
    let tire = new THREE.Mesh(tireGeo, MATS.tireRubber);

    // Lathe profile for rim contour
    let rimPts = [
        new THREE.Vector2(0.01, width/2.05),
        new THREE.Vector2(rimR*0.6, width/2.3),
        new THREE.Vector2(rimR, width/2.05),
        new THREE.Vector2(rimR, -width/2.05),
        new THREE.Vector2(rimR*0.6, -width/2.3),
        new THREE.Vector2(0.01, -width/2.05)
    ];
    let rimGeo = new THREE.LatheGeometry(rimPts, 24); extraGeoms.push(rimGeo);
    let rim = new THREE.Mesh(rimGeo, MATS.metalGear);

    // Hub assembly
    let hubGeo = new THREE.CylinderGeometry(0.02, 0.02, width*1.2, 16); extraGeoms.push(hubGeo);
    let hub = new THREE.Mesh(hubGeo, MATS.heavyMetal);
    
    wGrp.add(tire, rim, hub);
    wGrp.rotation.z = Math.PI / 2; // Orient upright to roll along Z
    return wGrp;
}

function buildGearAssembly(scaleFactor, isMain) {
    let asm = new THREE.Group();
    
    // Main oleo outer cylinder
    let strutH = 0.4 * scaleFactor;
    let strutGeo = new THREE.CylinderGeometry(0.04 * scaleFactor, 0.04 * scaleFactor, strutH, 16); extraGeoms.push(strutGeo);
    let strut = new THREE.Mesh(strutGeo, MATS.metalGear);
    strut.position.y = -strutH / 2;
    asm.add(strut);

    // Inner bright oleo cylinder
    let oleoH = 0.3 * scaleFactor;
    let oleoGeo = new THREE.CylinderGeometry(0.025 * scaleFactor, 0.025 * scaleFactor, oleoH, 16); extraGeoms.push(oleoGeo);
    let oleo = new THREE.Mesh(oleoGeo, MATS.heavyMetal);
    oleo.position.y = -strutH - (oleoH / 2);
    asm.add(oleo);

    // Helical suspension spring wrapping the oleo
    let springCurve = new SpringCurve(0.06 * scaleFactor, strutH * 0.8, 5);
    let springGeo = new THREE.TubeGeometry(springCurve, 64, 0.01 * scaleFactor, 8, false); extraGeoms.push(springGeo);
    let spring = new THREE.Mesh(springGeo, MATS.darkArmor);
    spring.position.y = -strutH * 0.1;
    asm.add(spring);

    let axleY = -strutH - oleoH;
    
    if(isMain) {
        // Main gear: Complete dual-wheel bogie assembly
        let axleW = 0.3 * scaleFactor;
        let axleGeo = new THREE.CylinderGeometry(0.02 * scaleFactor, 0.02 * scaleFactor, axleW, 16); axleGeo.rotateZ(Math.PI/2); extraGeoms.push(axleGeo);
        let axle = new THREE.Mesh(axleGeo, MATS.metalGear);
        axle.position.y = axleY;
        asm.add(axle);

        let wheelL = buildDetailedWheel(0.15 * scaleFactor, 0.08 * scaleFactor, 0.08 * scaleFactor); wheelL.position.set(-axleW/2, axleY, 0);
        let wheelR = buildDetailedWheel(0.15 * scaleFactor, 0.08 * scaleFactor, 0.08 * scaleFactor); wheelR.position.set(axleW/2, axleY, 0);
        asm.add(wheelL, wheelR);
    } else {
        // Nose gear: Smaller dual-wheel assembly
        let axleW = 0.2 * scaleFactor;
        let axleGeo = new THREE.CylinderGeometry(0.02 * scaleFactor, 0.02 * scaleFactor, axleW, 16); axleGeo.rotateZ(Math.PI/2); extraGeoms.push(axleGeo);
        let axle = new THREE.Mesh(axleGeo, MATS.metalGear);
        axle.position.y = axleY;
        asm.add(axle);

        let wheelL = buildDetailedWheel(0.10 * scaleFactor, 0.05 * scaleFactor, 0.06 * scaleFactor); wheelL.position.set(-axleW/2, axleY, 0);
        let wheelR = buildDetailedWheel(0.10 * scaleFactor, 0.05 * scaleFactor, 0.06 * scaleFactor); wheelR.position.set(axleW/2, axleY, 0);
        asm.add(wheelL, wheelR);
    }
    return asm;
}

function buildLoftGeometry(sections, radialSegments) {
    const vertices = []; const indices = [];
    for (let i = 0; i < sections.length; i++) {
        const section = sections[i];
        for (let j = 0; j < radialSegments; j++) {
            const angle = (j / radialSegments) * Math.PI * 2;
            const xPos = Math.cos(angle) * section.radiusX + section.x;
            const yPos = Math.sin(angle) * section.radiusY + section.yOffset;
            vertices.push(xPos, yPos, section.z);
        }
    }
    for (let i = 0; i < sections.length - 1; i++) {
        const currentRow = i * radialSegments; const nextRow = (i + 1) * radialSegments;
        for (let j = 0; j < radialSegments; j++) {
            const nextSegment = (j + 1) % radialSegments;
            indices.push(currentRow + j, nextRow + j, nextRow + nextSegment);
            indices.push(currentRow + j, nextRow + nextSegment, currentRow + nextSegment);
        }
    }
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    geometry.setIndex(indices);
    geometry.computeVertexNormals();
    return geometry;
}

function makeEggShape(geo) {
    const pos = geo.attributes.position; let minZ = 0;
    for (let i = 0; i < pos.count; i++) { let z = pos.getZ(i); if (z < minZ) minZ = z; }
    if (minZ === 0) return; 
    for (let i = 0; i < pos.count; i++) {
        let x = pos.getX(i); let z = pos.getZ(i);
        if (z < 0) { let ratio = z / minZ; let taper = 1.0 - (0.35 * ratio); pos.setX(i, x * taper); }
    }
    geo.computeVertexNormals();
}

function attachLoadout(grp, loadout, wX, wY, wZ, rY, rZ, fX, fY, fZ, paramWingSpan) {
    let attachGrp = new THREE.Group(); attachGrp.name = "attachments";
    let scaledWx = wX * paramWingSpan; 
    
    if(loadout.weapons === 'aam' || loadout.weapons === 'agm') {
        let isHeavy = loadout.weapons === 'agm';
        let bodyR = isHeavy ? 0.35 : 0.15;
        let bodyL = isHeavy ? 2.5 : 3.0;
        let mat = isHeavy ? MATS.darkArmor : MATS.heavyMetal;
        let makeWpn = () => {
            let wGrp = new THREE.Group();
            let bGeo = new THREE.CylinderGeometry(bodyR, bodyR, bodyL, 12); bGeo.rotateX(Math.PI/2); extraGeoms.push(bGeo);
            wGrp.add(new THREE.Mesh(bGeo, mat));
            if(isHeavy) {
                let nGeo = new THREE.SphereGeometry(bodyR, 12, 12); extraGeoms.push(nGeo);
                let nose = new THREE.Mesh(nGeo, MATS.heavyMetal); nose.position.z = -1.25; wGrp.add(nose);
            }
            let holsterRadius = bodyR * 1.5;
            let hGeo = new THREE.CylinderGeometry(holsterRadius, holsterRadius, bodyL * 1.1, 16, 1, false, -Math.PI/2, Math.PI);
            hGeo.rotateX(Math.PI/2); hGeo.rotateZ(Math.PI/2); 
            let holster = new THREE.Mesh(hGeo, MATS.stealthSkin); holster.position.y = bodyR * 0.2; 
            wGrp.add(holster); extraGeoms.push(hGeo);
            return wGrp;
        };
        let wL = makeWpn(); wL.position.set(scaledWx, wY, wZ); attachGrp.add(wL);
        let wR = makeWpn(); wR.position.set(-scaledWx, wY, wZ); attachGrp.add(wR);
        if(isHeavy) {
            let wL2 = makeWpn(); wL2.position.set(scaledWx + 1.2, wY, wZ + 0.5); attachGrp.add(wL2);
            let wR2 = makeWpn(); wR2.position.set(-scaledWx - 1.2, wY, wZ + 0.5); attachGrp.add(wR2);
        }
    }
    
    if(loadout.radar === 'awacs') {
        let rGrp = new THREE.Group();
        let sGeo = new THREE.BoxGeometry(0.4, 0.8, 1.0); extraGeoms.push(sGeo);
        rGrp.add(new THREE.Mesh(sGeo, MATS.darkArmor));
        let dGeo = new THREE.CylinderGeometry(2.5, 2.5, 0.4, 32); extraGeoms.push(dGeo);
        let dome = new THREE.Mesh(dGeo, MATS.stealthSkin); dome.position.y = 0.6; rGrp.add(dome);
        rGrp.position.set(0, rY, rZ); attachGrp.add(rGrp);
    }
    
    if(loadout.flares === 'flares') {
        let makeFlare = () => {
            let fGrp = new THREE.Group();
            let pGeo = new THREE.BoxGeometry(0.6, 0.4, 1.5); extraGeoms.push(pGeo);
            fGrp.add(new THREE.Mesh(pGeo, MATS.darkArmor));
            let flGeo = new THREE.BoxGeometry(0.4, 0.1, 0.4); extraGeoms.push(flGeo);
            let flare = new THREE.Mesh(flGeo, MATS.flareGlow); flare.position.set(0, -0.2, 0.4); fGrp.add(flare);
            return fGrp;
        };
        let fL = makeFlare(); fL.position.set(fX, fY, fZ); attachGrp.add(fL);
        let fR = makeFlare(); fR.position.set(-fX, fY, fZ); attachGrp.add(fR);
    }
    grp.add(attachGrp);
}

/* ==========================================
   MODEL BUILDERS
========================================== */

function buildAlphaConcept(loadout, p) {
    let grp = new THREE.Group(); grp.name = 'craftMaster';
    let fuse = new THREE.Group(); fuse.name = "fuselage";
    
    let topGeo = new THREE.SphereGeometry(1.3, 32, 16, 0, Math.PI*2, 0, Math.PI/2); 
    topGeo.scale(1.0, 0.75, 1.5 * p.fuse); makeEggShape(topGeo);
    let topDome = new THREE.Mesh(topGeo, MATS.darkArmor); topDome.position.set(0, 0.5, -5.5); fuse.add(topDome); extraGeoms.push(topGeo);
    
    let rimGeo = new THREE.CylinderGeometry(1.35, 1.35, 0.08, 32); rimGeo.scale(1.0, 1.0, 1.5 * p.fuse); makeEggShape(rimGeo); 
    let rim = new THREE.Mesh(rimGeo, MATS.stealthSkin); rim.position.set(0, 0.46, -5.5); fuse.add(rim); extraGeoms.push(rimGeo);
    
    let botGeo = new THREE.SphereGeometry(1.0, 32, 16, 0, Math.PI*2, Math.PI/2, Math.PI/2); 
    botGeo.scale(1.0, 0.8, 1.5 * p.fuse); makeEggShape(botGeo); 
    let botDome = new THREE.Mesh(botGeo, MATS.darkArmor); botDome.position.set(0, 0.42, -5.5); fuse.add(botDome); extraGeoms.push(botGeo);
    
    let shaftGeo = new THREE.CylinderGeometry(0.95, 0.40, 13 * p.fuse, 48, 40); shaftGeo.rotateX(Math.PI / 2);
    const pos = shaftGeo.attributes.position; const halfLen = (13 * p.fuse) / 2;
    for (let i = 0; i < pos.count; i++) {
        let x = pos.getX(i); let y = pos.getY(i); let z = pos.getZ(i);
        let t = THREE.MathUtils.clamp((z + halfLen) / (halfLen * 2), 0, 1);
        let xScale = THREE.MathUtils.lerp(1.12, 1.00, t); let isSeam = Math.abs(y) < 0.1;
        if (y >= 0) { if (!isSeam) y += 0.05; } else { let bottomScale = THREE.MathUtils.lerp(1.8, 1.10, t) + (Math.sin(t * Math.PI) * 0.4); y *= bottomScale; if (!isSeam) y -= 0.05; }
        x *= xScale; if (isSeam) { x *= 0.85; } pos.setXYZ(i, x, y, z);
    }
    pos.needsUpdate = true; shaftGeo.computeVertexNormals();
    let shaft = new THREE.Mesh(shaftGeo, MATS.stealthSkin); shaft.position.set(0, 0.3, 1.0); fuse.add(shaft); extraGeoms.push(shaftGeo);

    function latheRocketProfile(points){ return new THREE.LatheGeometry(points, 4, 0, Math.PI*2); }
    function makeRocket(rPos){
        const rGrp = new THREE.Group(); const r = 0.28; const len = 2.3;
        const bGeo = latheRocketProfile([ new THREE.Vector2(r,-len/2), new THREE.Vector2(r,len/2) ]);
        const body = new THREE.Mesh(bGeo, new THREE.MeshStandardMaterial({ color:0x222222 })); body.rotation.x = Math.PI/2; rGrp.add(body); extraGeoms.push(bGeo);
        const sGeo = latheRocketProfile([ new THREE.Vector2(r+0.04,-0.035), new THREE.Vector2(r+0.04,0.035) ]);
        const spacer = new THREE.Mesh(sGeo, new THREE.MeshStandardMaterial({ color:0x00e5ff, emissive:0x00e5ff })); spacer.rotation.x = Math.PI/2; spacer.position.z = -len/2-0.035; rGrp.add(spacer); extraGeoms.push(sGeo);
        const nGeo = new THREE.LatheGeometry([ new THREE.Vector2(0,-0.20), new THREE.Vector2(r+0.04,-0.10), new THREE.Vector2(r+0.04,0) ], 4);
        const nose = new THREE.Mesh(nGeo, new THREE.MeshStandardMaterial({ color:0x222222 })); nose.rotation.x = Math.PI/2; nose.position.z = -len/2-0.05; rGrp.add(nose); extraGeoms.push(nGeo);
        rGrp.rotation.z = Math.PI/4; rGrp.position.set(rPos.x, 0.05, rPos.z); return rGrp;
    }
    const rocketPositions=[ {x:-2.6,z:0.5}, {x:-3.8,z:0.5}, {x:2.6,z:0.5}, {x:3.8,z:0.5} ];
    rocketPositions.forEach(rPos=>{ grp.add(makeRocket(rPos)); });

    let gGroup = new THREE.Group(); gGroup.position.set(0, 0.42 - 0.8, -5.5);
    let gBaseGeo = new THREE.CylinderGeometry(0.25, 0.3, 0.1, 16); gGroup.add(new THREE.Mesh(gBaseGeo, MATS.heavyMetal)); extraGeoms.push(gBaseGeo);
    let gSphGeo = new THREE.SphereGeometry(0.22, 16, 16); let gSph = new THREE.Mesh(gSphGeo, MATS.stealthSkin); gSph.position.set(0, -0.15, 0); gGroup.add(gSph); extraGeoms.push(gSphGeo);
    fuse.add(gGroup); grp.add(fuse);

    let wingsGroup = new THREE.Group(); wingsGroup.name = "wings";
    let span = 14 * p.wing; let wingGeoL = new THREE.BoxGeometry(span, 1.0, 1.0, 32, 1, 16); wingGeoL.translate(span / 2, 0, 0);
    let posLW = wingGeoL.attributes.position;
    for (let i = 0; i < posLW.count; i++) {
        let rx = posLW.getX(i) / span; let zN = posLW.getZ(i) * 2.0; let y = posLW.getY(i);
        let profileY = zN > 0 ? Math.sqrt(1.0 - zN * zN) : 1.0 + zN;
        posLW.setXYZ(i, posLW.getX(i), (y > 0 ? profileY : -profileY) * (0.35 * (1.0 - 0.6 * rx) / 2.0), zN * (2.4 * (1.0 - 0.45 * rx) / 2.0));
    }
    wingGeoL.computeVertexNormals(); extraGeoms.push(wingGeoL);
    
    let wingGeoR = wingGeoL.clone(); let posRW = wingGeoR.attributes.position;
    for (let i = 0; i < posRW.count; i++) { posRW.setX(i, -posRW.getX(i)); } wingGeoR.computeVertexNormals(); extraGeoms.push(wingGeoR);

    let wLGrp = new THREE.Group(); wLGrp.name = "wingL"; wLGrp.position.set(0, 0.5, 0.5); wLGrp.rotation.set(0, p.wingSweep, p.wingDihedral); wLGrp.add(new THREE.Mesh(wingGeoL, MATS.darkArmor)); wingsGroup.add(wLGrp);
    let wRGrp = new THREE.Group(); wRGrp.name = "wingR"; wRGrp.position.set(0, 0.5, 0.5); wRGrp.rotation.set(0, -p.wingSweep, -p.wingDihedral); wRGrp.add(new THREE.Mesh(wingGeoR, MATS.darkArmor)); wingsGroup.add(wRGrp);
    grp.add(wingsGroup);

    let tail = new THREE.Group(); tail.name = "tail";
    let vTailSpan = 1.1 * p.finScale; let vTailGeoL = new THREE.BoxGeometry(vTailSpan, 1.0, 1.0, 16, 1, 16); vTailGeoL.translate(vTailSpan / 2, 0, 0);
    let posVL = vTailGeoL.attributes.position;
    for (let i = 0; i < posVL.count; i++) { posVL.setXYZ(i, posVL.getX(i), posVL.getY(i) * (0.15 * (1.0 - 0.6 * (posVL.getX(i) / vTailSpan))), posVL.getZ(i) * (1.4 * (1.0 - 0.5 * (posVL.getX(i) / vTailSpan)))); }
    vTailGeoL.computeVertexNormals(); extraGeoms.push(vTailGeoL);
    let vTailGeoR = vTailGeoL.clone(); let posVR = vTailGeoR.attributes.position;
    for (let i = 0; i < posVR.count; i++) { posVR.setX(i, -posVR.getX(i)); } vTailGeoR.computeVertexNormals(); extraGeoms.push(vTailGeoR);

    let tailLGrp = new THREE.Group(); tailLGrp.add(new THREE.Mesh(vTailGeoL, MATS.stealthSkin)); tailLGrp.position.set(0.3, 0.6, 6.8 * p.fuse); tailLGrp.rotation.set(0, p.finSweep, p.finAngle); tail.add(tailLGrp);
    let tailRGrp = new THREE.Group(); tailRGrp.add(new THREE.Mesh(vTailGeoR, MATS.stealthSkin)); tailRGrp.position.set(-0.3, 0.6, 6.8 * p.fuse); tailRGrp.rotation.set(0, -p.finSweep, -p.finAngle); tail.add(tailRGrp);
    grp.add(tail);

    let eng = new THREE.Group(); eng.name = "engines";
    let oNGeo = new THREE.CylinderGeometry(0.5, 0.6, 1.5, 32); oNGeo.rotateX(Math.PI / 2); let oN = new THREE.Mesh(oNGeo, MATS.darkArmor); oN.position.set(0, 0.3, 7.5 * p.fuse); eng.add(oN); extraGeoms.push(oNGeo);
    grp.add(eng);

    // Add Advanced Tricycle Landing Gear to Drone
    let gearGroup = new THREE.Group(); gearGroup.name = "landingGear";
    let noseGear = buildGearAssembly(0.9, false); noseGear.position.set(0, 0.1, -4.5 * p.fuse); gearGroup.add(noseGear);
    let mainGearL = buildGearAssembly(0.9, true); mainGearL.position.set(-1.8, 0.1, 1.5 * p.fuse); gearGroup.add(mainGearL);
    let mainGearR = buildGearAssembly(0.9, true); mainGearR.position.set(1.8, 0.1, 1.5 * p.fuse); gearGroup.add(mainGearR);
    grp.add(gearGroup);

    attachLoadout(grp, loadout, 4, -0.2, 0.5,  1.5, 0,  0.8, -0.5, 6.0 * p.fuse, p.wing);
    return grp;
}

function buildB2Bomber() {
    let grp = new THREE.Group(); grp.name = 'craftMaster';
    
    // Increased lofting resolution to 48 for incredibly smooth photorealistic curves
    let fuseGeo = buildLoftGeometry(window.fuseData, 48);
    let fuseMesh = new THREE.Mesh(fuseGeo, MATS.stealthSkin); fuseMesh.name = "fuselage"; grp.add(fuseMesh); extraGeoms.push(fuseGeo);

    let wingGeo = buildLoftGeometry(window.wingData, 48); extraGeoms.push(wingGeo);
    let wingL = new THREE.Mesh(wingGeo, MATS.darkArmor); wingL.rotation.y = -Math.PI / 2; wingL.name = "wingL"; grp.add(wingL);
    let wingR = new THREE.Mesh(wingGeo, MATS.darkArmor); wingR.rotation.y = -Math.PI / 2; wingR.scale.z = -1; wingR.name = "wingR"; grp.add(wingR);

    let finGeo = buildLoftGeometry(window.finData, 16); extraGeoms.push(finGeo);
    let finL = new THREE.Mesh(finGeo, MATS.darkArmor); finL.rotation.set(-Math.PI / 2, 0, Math.PI / 2); finL.position.set(-11.0, 0.2, 4.7); finL.name = "finL"; grp.add(finL);
    let finR = new THREE.Mesh(finGeo, MATS.darkArmor); finR.rotation.set(-Math.PI / 2, 0, Math.PI / 2); finR.position.set(11.0, 0.2, 4.7); finR.scale.z = -1; finR.name = "finR"; grp.add(finR);

    // Flatter, integrated stealth canopy
    let canopyGeo = new THREE.SphereGeometry(0.5, 32, 16); extraGeoms.push(canopyGeo);
    let canopy = new THREE.Mesh(canopyGeo, MATS.glass); canopy.scale.set(0.6, 0.20, 1.3); canopy.position.set(0, 0.42, -1.6); canopy.name = "cockpit"; grp.add(canopy);

    // Engines utilizing Dynamic Lathe Points and Engine Parameters
    let engGroup = new THREE.Group(); engGroup.name = "engines";
    let ep = window.engineParams;
    let lathePoints = window.engineData.map(pt => new THREE.Vector2(pt.radius, pt.y));
    let engGeo = new THREE.LatheGeometry(lathePoints, 32); engGeo.rotateX(Math.PI / 2); extraGeoms.push(engGeo);
    
    let engScale = new THREE.Vector3(ep.scaleX, ep.scaleY, ep.scaleZ);
    
    let engL = new THREE.Mesh(engGeo, MATS.stealthSkin); engL.scale.copy(engScale); 
    engL.position.set(-ep.offsetX, ep.offsetY, ep.offsetZ); engGroup.add(engL);
    
    let engR = new THREE.Mesh(engGeo, MATS.stealthSkin); engR.scale.copy(engScale); 
    engR.position.set(ep.offsetX, ep.offsetY, ep.offsetZ); engGroup.add(engR);
    
    // Internal heat glow positioned dynamically at the end of the lathe profile
    let maxZ = Math.max(...window.engineData.map(p => p.y)) * ep.scaleZ;
    let glowGeo = new THREE.BoxGeometry(0.9, 0.04, 0.2); extraGeoms.push(glowGeo);
    
    let glowL = new THREE.Mesh(glowGeo, MATS.engineFire); 
    glowL.position.set(-ep.offsetX, ep.offsetY, ep.offsetZ + maxZ - 0.1); engGroup.add(glowL);
    
    let glowR = new THREE.Mesh(glowGeo, MATS.engineFire); 
    glowR.position.set(ep.offsetX, ep.offsetY, ep.offsetZ + maxZ - 0.1); engGroup.add(glowR);
    
    grp.add(engGroup);

    let bayGeo = new THREE.BoxGeometry(1.2, 0.1, 2.5); extraGeoms.push(bayGeo);
    let bayL = new THREE.Mesh(bayGeo, MATS.bombBaySkin); bayL.position.set(-0.8, -0.15, 0.5); bayL.name = "bombBayL"; grp.add(bayL);
    let bayR = new THREE.Mesh(bayGeo, MATS.bombBaySkin); bayR.position.set(0.8, -0.15, 0.5); bayR.name = "bombBayR"; grp.add(bayR);

    let gearGroup = new THREE.Group(); gearGroup.name = "landingGear";
    
    let noseGear = buildGearAssembly(1.0, false);
    noseGear.position.set(0, 0, -2.5);
    gearGroup.add(noseGear);

    let mainGearL = buildGearAssembly(1.0, true);
    mainGearL.position.set(-1.8, 0, 1.2);
    gearGroup.add(mainGearL);

    let mainGearR = buildGearAssembly(1.0, true);
    mainGearR.position.set(1.8, 0, 1.2);
    gearGroup.add(mainGearR);

    grp.add(gearGroup);

    grp.scale.set(3, 3, 3); grp.position.z = -3;
    return grp;
}

/* ==========================================
   RENDER LIFECYCLE
========================================== */
function init() {
    viewList.forEach((v, idx) => {
        scenes[v] = new THREE.Scene(); 
        scenes[v].background = new THREE.Color(0x0e121a); 
        
        let grid = new THREE.GridHelper(200, 40, 0x3a2a66, 0x1a1230); grid.position.y = -10; scenes[v].add(grid);
        
        // Shadow Catcher Plane
        let planeGeo = new THREE.PlaneGeometry(300, 300);
        let planeMat = new THREE.ShadowMaterial({ opacity: 0.6 });
        let plane = new THREE.Mesh(planeGeo, planeMat);
        plane.rotation.x = -Math.PI / 2; plane.position.y = -10.1;
        plane.receiveShadow = true; scenes[v].add(plane);

        cameras[v] = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10000);
        let cont = document.getElementById('vp'+v);
        
        renderers[v] = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderers[v].setSize(cont.clientWidth||800, cont.clientHeight||600);
        
        // Cinematic Renderer Upgrades
        renderers[v].outputEncoding = THREE.sRGBEncoding;
        renderers[v].toneMapping = THREE.ACESFilmicToneMapping;
        renderers[v].toneMappingExposure = 1.1;
        renderers[v].shadowMap.enabled = true;
        renderers[v].shadowMap.type = THREE.PCFSoftShadowMap;
        cont.appendChild(renderers[v].domElement);
        
        // Setup Procedural PBR Environment Texture
        if(idx === 0) sharedEnvTexture = createStudioEnvironment(renderers[v]);
        scenes[v].environment = sharedEnvTexture;

        scenes[v].add(new THREE.AmbientLight(0xffffff, 0.2));
        
        // Main Key Light (Casts Soft Shadows)
        let mainLight = new THREE.DirectionalLight(0xfff5ee, 1.8); 
        mainLight.position.set(60, 100, 60); 
        mainLight.castShadow = true;
        mainLight.shadow.mapSize.width = 2048;
        mainLight.shadow.mapSize.height = 2048;
        mainLight.shadow.camera.near = 0.5;
        mainLight.shadow.camera.far = 300;
        let d = 60; // Shadow camera bounds large enough to cover explosions
        mainLight.shadow.camera.left = -d; mainLight.shadow.camera.right = d;
        mainLight.shadow.camera.top = d; mainLight.shadow.camera.bottom = -d;
        scenes[v].add(mainLight);

        // Fill Light
        let fillLight = new THREE.DirectionalLight(0x88bbff, 0.8); 
        fillLight.position.set(-60, 20, -60); 
        scenes[v].add(fillLight);
    });
    
    window.addEventListener('resize', window.resizeActiveViewport);
    setTimeout(() => { window.updateModel(); }, 100);
    animate();
}

window.updateModel = () => {
    extraGeoms.forEach(g => g.dispose()); extraGeoms = [];
    viewList.forEach(v => { let g = scenes[v].children.filter(c => c.name.startsWith('craft')); g.forEach(x => scenes[v].remove(x)); });
    
    let configId = document.getElementById('craftConfig').value;
    let style = document.getElementById('renderStyle').value;
    
    if (configId === 'b2_bomber') {
        document.getElementById('legacy-controls').style.display = 'none';
        document.getElementById('b2-controls').style.display = 'flex';
    } else {
        document.getElementById('legacy-controls').style.display = 'flex';
        document.getElementById('b2-controls').style.display = 'none';
    }

    let loadout = { weapons: document.getElementById('loadoutWeapons').value, radar: document.getElementById('loadoutRadar').value, flares: document.getElementById('loadoutFlares').value };
    let params = { wing: document.getElementById('paramWingSpan').value / 100, fuse: document.getElementById('paramFuseLen').value / 100, wingSweep: THREE.MathUtils.degToRad(document.getElementById('paramWingSweep').value), wingDihedral: THREE.MathUtils.degToRad(document.getElementById('paramWingDihedral').value), finSweep: THREE.MathUtils.degToRad(document.getElementById('paramFinSweep').value), finAngle: THREE.MathUtils.degToRad(document.getElementById('paramFinAngle').value), finScale: document.getElementById('paramFinScale').value / 100 };

    let titleText = document.getElementById('craftConfig').options[document.getElementById('craftConfig').selectedIndex].text;
    viewList.forEach(v => {
        document.getElementById(`title-${v}`).innerText = titleText.split(' (')[0];
        document.getElementById(`class-${v}`).innerText = configId === 'b2_bomber' ? "STRATEGIC BOMBER" : "AUTONOMOUS SYSTEM";
    });

    let masterAsm = configId === 'drone_legacy' ? buildAlphaConcept(loadout, params) : buildB2Bomber();

    // Shadow & Wireframe Material Pass
    masterAsm.traverse(child => {
        if(child.isMesh) {
            if(style === 'wireframe') {
                let edgesGeo = new THREE.EdgesGeometry(child.geometry, 20); let edges = new THREE.LineSegments(edgesGeo, MATS.holoWire);
                child.add(edges); child.material = new THREE.MeshBasicMaterial({color: 0x0e121a, transparent: true, opacity: 0.85, side: THREE.DoubleSide}); extraGeoms.push(edgesGeo);
                child.castShadow = false; child.receiveShadow = false;
            } else if(style === 'xray') {
                child.material = child.material.clone(); child.material.transparent = true; child.material.opacity = 0.15; child.material.depthWrite = false;
                child.castShadow = false; child.receiveShadow = false;
            } else {
                child.castShadow = true; child.receiveShadow = true;
            }
        }
    });

    viewList.forEach(v => {
        let clone = masterAsm.clone(); clone.name = 'craft_' + v;
        let cWingL = null, cWingR = null, cTail = null, cEng = null, cAtt = null;
        let cFus = null, cCockpit = null, cGear = null, cBayL = null, cBayR = null;

        clone.traverse(c => {
            if (c.name === 'wingL') cWingL = c; if (c.name === 'wingR') cWingR = c;
            if (c.name === 'tail') cTail = c; if (c.name === 'engines') cEng = c;
            if (c.name === 'attachments') cAtt = c; if (c.name === 'fuselage') cFus = c;
            if (c.name === 'cockpit') cCockpit = c; if (c.name === 'landingGear') cGear = c;
            if (c.name === 'bombBayL') cBayL = c; if (c.name === 'bombBayR') cBayR = c;
        });

        if(v === '5') { 
            let val = 1.0; 
            if(configId === 'b2_bomber') {
                if (cWingL && cWingR) { cWingL.position.x -= val * 4.0; cWingL.position.z += val * 1.5; cWingR.position.x += val * 4.0; cWingR.position.z += val * 1.5; }
                if (cFus) { cFus.position.y += val * 1.5; }
                if (cEng) { cEng.position.y += val * 3.0; cEng.position.z += val * 4.0; }
                if (cCockpit) { cCockpit.position.y += val * 3.5; cCockpit.position.z -= val * 1.0; }
                if (cBayL) cBayL.position.y -= val * 3.0; if (cBayR) cBayR.position.y -= val * 3.0;
                if (cGear) cGear.position.y -= val * 4.5;
            } else {
                if (cWingL && cWingR) { cWingL.position.x += val * 12.0; cWingR.position.x -= val * 12.0; }
                if (cTail) { cTail.position.y += val * 8.0; cTail.position.z += val * 4.0; }
                if (cEng) { cEng.position.z += val * 10.0; }
                if (cAtt) { cAtt.position.y -= val * 8.0; }
            }
        }
        scenes[v].add(clone);
    });

    let sBase = configId === 'b2_bomber' ? 22 : 12;
    cameras['1'].position.set(0, 0, -150); cameras['1'].lookAt(0, 0, 0); cameras['1'].userData.baseSize = sBase - 2;
    cameras['2'].position.set(0, 150, 0); cameras['2'].lookAt(0, 0, 0); cameras['2'].userData.baseSize = sBase + 4;
    cameras['3'].position.set(150, 0, 0); cameras['3'].lookAt(0, 0, 0); cameras['3'].userData.baseSize = sBase + 2;
    cameras['4'].position.set(-60, 40, -60); cameras['4'].lookAt(0, 0, 0); cameras['4'].userData.baseSize = sBase + 6;
    cameras['5'].position.set(-60, 40, -60); cameras['5'].lookAt(0, 0, 0); cameras['5'].userData.baseSize = sBase + 14;
    
    window.resizeActiveViewport();
};

window.resizeActiveViewport = () => {
    let activeVps = [currentPage.toString()];
    activeVps.forEach(v => {
        let vp = document.getElementById('vp' + v);
        if(!vp || vp.clientWidth === 0) return;
        let aspect = vp.clientWidth / vp.clientHeight;
        renderers[v].setSize(vp.clientWidth, vp.clientHeight);
        let cam = cameras[v]; 
        let s = cam.userData.baseSize || 20;
        cam.left = -s * aspect; cam.right = s * aspect; cam.top = s; cam.bottom = -s; 
        cam.updateProjectionMatrix();
    });
};

window.changePage = (dir) => {
    document.getElementById('sheet'+currentPage).classList.remove('active');
    currentPage += dir; if(currentPage < 1) currentPage = maxPages; if(currentPage > maxPages) currentPage = 1;
    document.getElementById('sheet'+currentPage).classList.add('active');
    setTimeout(window.resizeActiveViewport, 10);
};

function animate() {
    requestAnimationFrame(animate);
    let activeVps = [currentPage.toString()];
    activeVps.forEach(v => { let vp = document.getElementById('vp'+v); if(vp && vp.clientWidth > 0) renderers[v].render(scenes[v], cameras[v]); });
}
window.onload = init;
</script>
</body>
</html>

Looking for anyone who might know how to help

I’ve managed to build something that looks close, it’s a mesh at least, a little editing and adding a few more sections should get a basic exterior shape drawn within a reasonable time frame.

There’s issues with the sections I have, they aren’t right, obviously. Need to add more and fix them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Aerospace Command - Unified Airframes</title>
<link href="https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@700&family=Share+Tech+Mono&display=swap" rel="stylesheet">

<style>
/* =========================
   DARK ENCHANTED THEME
   ========================= */

:root{
    --bg0:#07050d;
    --bg1:#0e0a18;
    --bg2:#151022;
    --panel:#120c1f;
    --panel2:#1a1230;
    --text:#e9e2ff;
    --muted:#b7a8e6;
    --accent:#a46bff;
    --accent2:#6a3cff;
    --cyan:#00e5ff;
    --glow:rgba(164,107,255,0.35);
    --danger:#ff3b5c;
    --line:#2a1f44;
}

body {
    margin:0; min-height:100vh; display:flex; flex-direction:column; align-items:center; overflow-x:hidden;
    font-family:sans-serif; color:var(--text);
    background:
        radial-gradient(circle at 20% 10%, rgba(164,107,255,0.15), transparent 40%),
        radial-gradient(circle at 80% 30%, rgba(106,60,255,0.12), transparent 45%),
        radial-gradient(circle at 50% 90%, rgba(53,242,166,0.08), transparent 50%),
        linear-gradient(180deg, var(--bg0), var(--bg1), var(--bg2));
}

.top-panel { width: 100%; text-align: center; box-sizing: border-box; }

.controls {
    margin: 10px auto 0 auto; 
    display: flex; 
    flex-wrap: nowrap; 
    gap: 12px; 
    align-items: center; 
    justify-content: center;
    width: 95vw; 
    max-width: 1200px; 
    box-sizing: border-box;
    padding: 12px 20px; 
    border-radius: 10px; 
    background: linear-gradient(180deg, var(--panel), var(--panel2));
    border: 1px solid var(--line); 
    box-shadow: 0 0 18px var(--glow), inset 0 0 12px rgba(164,107,255,0.08);
    z-index: 1000; 
    font-size: 12px; 
    overflow-x: auto; 
    white-space: nowrap;
}

.controls::-webkit-scrollbar { height: 8px; }
.controls::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); border-radius: 4px; }
.controls::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 4px; box-shadow: inset 0 0 4px rgba(0,0,0,0.5); }

.controls > * { flex-shrink: 0; }
.controls label { display: flex; align-items: center; gap: 6px; color: var(--text); font-weight: bold; }

.controls select, .controls input {
    background:#0f0a1d; color:var(--text); border:1px solid #3a2a66; padding:6px 10px; border-radius:6px;
    font-size:11px; outline:none; box-shadow: inset 0 0 6px rgba(0,0,0,0.6); cursor:pointer;
}
.controls select:focus, .controls input:focus { border-color:var(--accent); box-shadow:0 0 10px var(--glow); }

.btn-action {
    background:linear-gradient(135deg, var(--accent2), var(--accent)); color:white; border:none;
    padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:bold; font-size:11px;
    box-shadow:0 0 12px var(--glow); transition:0.2s ease;
}
.btn-action:hover { transform:translateY(-1px); box-shadow:0 0 18px var(--glow); }
.btn-danger { background:linear-gradient(135deg, #8b1e2d, var(--danger)); }

.side-nav-btn {
    position:fixed; top:50%; transform:translateY(-50%); width:45px; height:80px; border-radius:10px; cursor:pointer;
    background:linear-gradient(180deg, #120c1f, #0b0814); color:var(--text); border:1px solid #3a2a66;
    box-shadow:0 0 16px var(--glow); font-size:20px; transition:0.2s ease; z-index:999;
}
.side-nav-btn:hover { background:var(--accent); color:#0b0616; box-shadow:0 0 22px var(--glow); }
.side-nav-left { left:15px; } .side-nav-right { right:15px; }

.svg-container {
    width:95vw; max-width:1200px; aspect-ratio:36 / 24; min-height: 500px; margin-top:10px; position:relative; overflow:hidden;
    background: radial-gradient(circle at center, rgba(164,107,255,0.08), transparent 60%), linear-gradient(180deg, #0a0712, #07050d);
    border:1px solid #2b1d4a; box-shadow: 0 0 30px rgba(164,107,255,0.25), inset 0 0 40px rgba(0,0,0,0.6);
}

.sheet-page { display:none; width:100%; height:100%; position:relative; }
.sheet-page.active { display:block; }
.vp-single { width:100%; height:100%; position:absolute; inset:0; z-index:10; }
.sheet-border { position:absolute; inset:0.5in; border:1px solid rgba(164,107,255,0.4); pointer-events:none; z-index:50; }

.title-block {
    position:absolute; bottom:0.58in; right:0.58in; width:300px; border:1px solid #3a2a66;
    background:linear-gradient(180deg, #120c1f, #0b0814); z-index:100; font-family:Arial, sans-serif;
}
.title-row { display:flex; border-bottom:1px solid #2a1f44; }
.title-row:last-child { border-bottom:none; }
.title-cell { padding:4px; font-size:9px; flex:1; color:var(--muted); border-right:1px solid #2a1f44; }
.title-cell:last-child { border-right:none; }
.cell-bold { font-weight:bold; font-size:12px; margin-top:1px; text-transform:uppercase; color:var(--text); }
.text-welcome-brand {
    font-family:'Cabin Sketch', cursive; font-size:22px; text-align:center;
    background:linear-gradient(90deg, #0b0616, #1a1230); color:var(--cyan);
    padding:4px; border-bottom:1px solid #3a2a66; text-shadow:0 0 10px var(--glow);
}

.view-tag { position:absolute; left:0.8in; bottom:0.8in; display:flex; align-items:center; z-index:20; }
.vt-circle {
    width:34px; height:34px; border-radius:50%; border:1px solid var(--accent); display:flex; align-items:center; justify-content:center;
    font-weight:bold; font-size:16px; background:rgba(20,14,35,0.8); box-shadow:0 0 10px var(--glow); margin-right:-2px;
}
.vt-lines { display:flex; flex-direction:column; justify-content:center; }
.vt-name { font-size:15px; font-weight:bold; letter-spacing:1px; padding-left:10px; margin-bottom:2px; text-transform:uppercase; color:var(--text); }
.vt-line { height:2px; width:220px; background:linear-gradient(90deg, var(--accent), transparent); }
.vt-scale { font-size:11px; padding-left:10px; margin-top:3px; text-transform:uppercase; font-weight:bold; color:var(--muted); }

/* SHARED MODAL STYLES */
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.85); z-index:2000; justify-content:center; align-items:center; backdrop-filter: blur(3px); }
.modal-content { width:500px; max-height: 90vh; overflow-y: auto; background:linear-gradient(180deg, #120c1f, #0b0814); border:1px solid #3a2a66; color:var(--text); padding:20px; border-radius:10px; box-shadow:0 0 25px var(--glow); display:flex; flex-direction:column; gap:10px; }
.modal-header { display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #2a1f44; padding-bottom:10px; }
.modal-header h3 { margin:0; font-size:18px; color:var(--accent); }
.close-btn { background:linear-gradient(135deg, #2a1f44, #3b2a66); padding:8px 12px; border:none; color:var(--text); cursor:pointer; border-radius:6px; font-weight:bold; box-shadow:0 0 10px rgba(164,107,255,0.2); transition:0.2s;}
.close-btn:hover { box-shadow:0 0 16px var(--glow); filter:brightness(1.2);}

/* MODAL TABS */
.tabs { display: flex; gap: 6px; border-bottom: 2px solid #2a1f44; padding-bottom: 10px; flex-wrap: wrap; margin-bottom:10px; }
.tab-btn { background: #1a1230; color: #b7a8e6; border: 1px solid #3a2a66; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 11px; transition: 0.2s; }
.tab-btn:hover { background: #2a1f44; color: white; }
.tab-btn.active { background: linear-gradient(135deg, var(--accent2), var(--accent)); color: white; border-color: var(--accent); box-shadow: 0 0 10px var(--glow); }
.tab-content { display: none; }
.tab-content.active { display: block; }

.setting-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; margin-bottom: 10px; }
.setting-row select, .setting-row input[type="range"] { width: 200px; }

</style>
</head>
<body>

<button class="side-nav-btn side-nav-left" onclick="window.changePage(-1)">&#10094;</button>
<button class="side-nav-btn side-nav-right" onclick="window.changePage(1)">&#10095;</button>

<div class="top-panel">
    <div class="controls">
      <strong style="font-family:'Cabin Sketch', cursive; color: var(--cyan); font-size: 16px; margin-right: 10px;">AEROSPACE COMMAND</strong>
      
      <label>Airframe: 
      <select id="craftConfig" onchange="window.updateModel()">
          <option value="drone_legacy">Alpha Concept (Legacy Drone)</option>
          <option value="b2_bomber" selected>B-2 Spirit (IDW Mesh Bomber)</option>
      </select>
      </label>
      
      <label>Render: 
      <select id="renderStyle" onchange="window.updateModel()">
          <option value="shaded" selected>Photorealistic PBR</option>
          <option value="wireframe">Diagnostic Wireframe</option>
          <option value="xray">Diagnostic X-Ray</option>
      </select>
      </label>

      <div style="border-left:1px solid var(--line); height:24px; margin: 0 5px;"></div>

      <div id="legacy-controls" style="display:none; gap:12px;">
          <button class="btn-action" onclick="openDroneModal()">⚙️ CONFIGURE AIRFRAME</button>
      </div>

      <div id="b2-controls" style="display:flex; align-items: center; font-family: 'Share Tech Mono', monospace; font-size:11px; color: var(--muted);">
          [ MATHEMATICAL MESH GENERATION ACTIVE ]
      </div>
    </div>
</div>

<!-- DRONE CONFIG MODAL -->
<div class="modal-overlay" id="droneConfigModal">
    <div class="modal-content">
        <div class="modal-header">
            <h3>LEGACY DRONE CONFIGURATION</h3>
            <button class="close-btn" onclick="closeDroneModal()">&times; CLOSE</button>
        </div>
        <div class="tabs">
            <button class="tab-btn active" onclick="switchDroneTab(event, 'tab-dims')">DIMENSIONS</button>
            <button class="tab-btn" onclick="switchDroneTab(event, 'tab-loadout')">LOADOUT</button>
            <button class="tab-btn" onclick="switchDroneTab(event, 'tab-defense')">DEFENSE</button>
        </div>
        
        <div id="tab-dims" class="tab-content active drone-tab">
            <div class="setting-row">
                <label>Wingspan Scale:</label>
                <input type="range" id="paramWingSpan" min="50" max="150" value="100" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label>Fuselage Length:</label>
                <input type="range" id="paramFuseLen" min="70" max="130" value="100" oninput="window.updateModel()">
            </div>
            <div class="setting-row" style="margin-top:15px; border-top:1px solid #2a1f44; padding-top:10px;">
                <label style="color:var(--accent);">Wing Sweep (Front ◄—► Back):</label>
                <input type="range" id="paramWingSweep" min="-45" max="60" value="-5" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:var(--accent);">Wing Dihedral (X-Plane):</label>
                <input type="range" id="paramWingDihedral" min="-15" max="30" value="2" oninput="window.updateModel()">
            </div>
            <div class="setting-row" style="margin-top:15px; border-top:1px solid #2a1f44; padding-top:10px;">
                <label style="color:var(--cyan);">Fin Scale (Size):</label>
                <input type="range" id="paramFinScale" min="30" max="200" value="150" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:var(--cyan);">Fin Sweep (Front ◄—► Back):</label>
                <input type="range" id="paramFinSweep" min="-45" max="75" value="0" oninput="window.updateModel()">
            </div>
            <div class="setting-row">
                <label style="color:var(--cyan);">Fin V-Angle (X-Plane):</label>
                <input type="range" id="paramFinAngle" min="0" max="90" value="60" oninput="window.updateModel()">
            </div>
        </div>
        
        <div id="tab-loadout" class="tab-content drone-tab">
            <div class="setting-row">
                <label>Wing Hardpoints:</label>
                <select id="loadoutWeapons" onchange="window.updateModel()">
                    <option value="none" selected>Clean (Stealth Configuration)</option>
                    <option value="aam">Interceptor (Light Air-to-Air)</option>
                    <option value="agm">Strike (Heavy Air-to-Ground)</option>
                </select>
            </div>
        </div>
        
        <div id="tab-defense" class="tab-content drone-tab">
            <div class="setting-row">
                <label>Dorsal Sensor Array:</label>
                <select id="loadoutRadar" onchange="window.updateModel()">
                    <option value="none" selected>Internal Scanners Only</option>
                    <option value="awacs">AWACS Overwatch Radome</option>
                </select>
            </div>
            <div class="setting-row">
                <label>Countermeasures:</label>
                <select id="loadoutFlares" onchange="window.updateModel()">
                    <option value="none" selected>None</option>
                    <option value="flares">Chaff / Flare Dispensers</option>
                </select>
            </div>
        </div>
    </div>
</div>

<div class="svg-container" id="sheets-container"></div>

<script type="importmap">
{
    "imports": {
        "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
    }
}
</script>

<script type="module">
import * as THREE from "three";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ==========================================
   B-2 GEOMETRIC DATA (IDW MESH)
========================================== */
const b2Coordinates = [
    { id: "NOSE",       x: 0,    y: -200 },
    { id: "RIGHT TIP",  x: 520,  y: 138 },
    { id: "R_INWARD 1", x: 320,  y: 268 },
    { id: "R_OUTWARD",  x: 220,  y: 203 },
    { id: "R_INWARD 2", x: 80,   y: 294 },
    { id: "TAIL V",     x: 0,    y: 242 },
    { id: "L_INWARD 2", x: -80,  y: 294 },
    { id: "L_OUTWARD",  x: -220, y: 203 },
    { id: "L_INWARD 1", x: -320, y: 268 },
    { id: "LEFT TIP",   x: -520, y: 138 }
];

const longData = [ { coord: -200, top: 0, bottom: 0 }, { coord: -150, top: 15, bottom: -5 }, { coord: -70, top: 45, bottom: -15 }, { coord: 0, top: 55, bottom: -20 }, { coord: 100, top: 35, bottom: -15 }, { coord: 180, top: 15, bottom: -5 }, { coord: 242, top: 0, bottom: 0 } ];
const transData = [ { coord: -520, top: 0, bottom: 0 }, { coord: -400, top: 8, bottom: -4 }, { coord: -260, top: 18, bottom: -8 }, { coord: -100, top: 40, bottom: -15 }, { coord: 0, top: 48, bottom: -18 }, { coord: 100, top: 40, bottom: -15 }, { coord: 260, top: 18, bottom: -8 }, { coord: 400, top: 8, bottom: -4 }, { coord: 520, top: 0, bottom: 0 } ];
const engFrontData = [ { coord: -300, top: 12, bottom: -5 }, { coord: -180, top: 20, bottom: -8 }, { coord: -130, top: 38, bottom: -10 }, { coord: -80, top: 32, bottom: -12 }, { coord: 0, top: 50, bottom: -18 }, { coord: 80, top: 32, bottom: -12 }, { coord: 130, top: 38, bottom: -10 }, { coord: 180, top: 20, bottom: -8 }, { coord: 300, top: 12, bottom: -5 } ];
const engRearData = [ { coord: -300, top: 10, bottom: -5 }, { coord: -180, top: 12, bottom: -6 }, { coord: -130, top: 15, bottom: -6 }, { coord: -80, top: 15, bottom: -8 }, { coord: 0, top: 22, bottom: -12 }, { coord: 80, top: 15, bottom: -8 }, { coord: 130, top: 15, bottom: -6 }, { coord: 180, top: 12, bottom: -6 }, { coord: 300, top: 10, bottom: -5 } ];

/* ==========================================
   UI CONTROLS & MODALS
========================================== */
window.openDroneModal = () => document.getElementById('droneConfigModal').style.display = 'flex';
window.closeDroneModal = () => document.getElementById('droneConfigModal').style.display = 'none';
window.switchDroneTab = (event, tabId) => {
    document.querySelectorAll('.drone-tab').forEach(el => el.classList.remove('active'));
    event.currentTarget.parentElement.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
    document.getElementById(tabId).classList.add('active');
    event.currentTarget.classList.add('active');
};

/* ==========================================
   3D ENGINE & REALISTIC SCENE SETUP
========================================== */
const viewList = ['1', '2', '3', '4', '5'];
const viewNames = [ "FRONT VIEW", "TOP VIEW", "SIDE VIEW", "ISOMETRIC VIEW", "EXPLODED SCHEMATIC" ];
let scenes = {}, cameras = {}, renderers = {}, controlsObj = {};
let currentPage = 1, maxPages = 5;
let extraGeoms = [];
let sharedEnvTexture = null;

const container = document.getElementById('sheets-container');
viewList.forEach((v, idx) => {
    container.innerHTML += `
    <div id="sheet${v}" class="sheet-page ${v === '1' ? 'active' : ''}">
        <div class="vp-single" id="vp${v}"></div><div class="sheet-border"></div>
        <div class="view-tag">
            <div class="vt-circle">${v}</div>
            <div class="vt-lines">
                <div class="vt-name">${viewNames[idx]}</div>
                <div class="vt-line"></div>
                <div class="vt-scale">SCALE: VARIES</div>
            </div>
        </div>
        <div class="title-block">
            <div class="text-welcome-brand" id="brand-${v}">e-drafts.com</div>
            <div class="title-row"><div class="title-cell">CLASS<div class="cell-bold" id="class-${v}">...</div></div></div>
            <div class="title-row"><div class="title-cell">DESIGNATION<div class="cell-bold" id="title-${v}">...</div></div></div>
            <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">${viewNames[idx]}</div></div></div>
            <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold">NTS</div></div><div class="title-cell">SHEET<div class="cell-bold">${v} OF 5</div></div></div>
        </div>
    </div>`;
});

// PHOTOREALISTIC PBR MATERIALS 
const MATS = {
    darkArmor: new THREE.MeshPhysicalMaterial({ color: 0x1f2126, roughness: 0.4, metalness: 0.6, clearcoat: 0.1, clearcoatRoughness: 0.5, side: THREE.DoubleSide }),
    stealthSkin: new THREE.MeshPhysicalMaterial({ color: 0x141518, roughness: 0.7, metalness: 0.2, clearcoat: 0.05, clearcoatRoughness: 0.8, side: THREE.DoubleSide }),
    heavyMetal: new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.5, metalness: 0.8 }),
    opticGlass: new THREE.MeshPhysicalMaterial({ color: 0x0a0a0a, metalness: 0.9, roughness: 0.05, clearcoat: 1.0, transparent: true, opacity: 0.8 }),
    sensorLens: new THREE.MeshPhysicalMaterial({ color: 0x111111, metalness: 1.0, roughness: 0.0, clearcoat: 1.0 }),
    engineFire: new THREE.MeshBasicMaterial({ color: 0xff5500 }),
    flareGlow: new THREE.MeshBasicMaterial({ color: 0xffffff }),
    // BUG FIX: Using standard hex value instead of un-evaluated CSS variable to prevent parsing crash
    holoWire: new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.3 }),
    glass: new THREE.MeshPhysicalMaterial({ color: 0x050505, roughness: 0.0, metalness: 1.0, clearcoat: 1.0 }),
    metalGear: new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.3, metalness: 0.9 }),
    tireRubber: new THREE.MeshStandardMaterial({ color: 0x050505, roughness: 0.95, metalness: 0.0 }),
    bombBaySkin: new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8, metalness: 0.3 })
};

function createStudioEnvironment(renderer) {
    let pmremGenerator = new THREE.PMREMGenerator(renderer);
    pmremGenerator.compileEquirectangularShader();
    
    let envScene = new THREE.Scene();
    envScene.add(new THREE.DirectionalLight(0xffffff, 3.0).translateY(10));
    envScene.add(new THREE.DirectionalLight(0x88ccff, 2.0).translateX(-10).translateY(-10));
    envScene.add(new THREE.DirectionalLight(0xffaa88, 2.0).translateZ(-10));

    let boxGeo = new THREE.BoxGeometry(10, 10, 1);
    let p1 = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({color: 0xffffff})); p1.position.set(0, 15, 20); envScene.add(p1);
    let p2 = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({color: 0x88ccff})); p2.position.set(-20, 5, 0); p2.lookAt(0,0,0); envScene.add(p2);
    
    return pmremGenerator.fromScene(envScene).texture;
}

/* ==========================================
   SHARED PROCEDURAL HELPERS & GEAR ASSEMBLIES
========================================== */
class SpringCurve extends THREE.Curve {
    constructor(radius, height, coils) {
        super();
        this.radius = radius;
        this.height = height;
        this.coils = coils;
    }
    getPoint(t, optionalTarget = new THREE.Vector3()) {
        return optionalTarget.set(
            Math.cos(t * Math.PI * 2 * this.coils) * this.radius,
            -t * this.height,
            Math.sin(t * Math.PI * 2 * this.coils) * this.radius
        );
    }
}

function buildDetailedWheel(tireR, rimR, width) {
    let wGrp = new THREE.Group();
    let tirePts = [
        new THREE.Vector2(rimR, width/2),
        new THREE.Vector2(tireR*0.85, width/2.1),
        new THREE.Vector2(tireR, width/4),
        new THREE.Vector2(tireR, -width/4),
        new THREE.Vector2(tireR*0.85, -width/2.1),
        new THREE.Vector2(rimR, -width/2)
    ];
    let tireGeo = new THREE.LatheGeometry(tirePts, 32); extraGeoms.push(tireGeo);
    let tire = new THREE.Mesh(tireGeo, MATS.tireRubber);

    let rimPts = [
        new THREE.Vector2(0.01, width/2.05),
        new THREE.Vector2(rimR*0.6, width/2.3),
        new THREE.Vector2(rimR, width/2.05),
        new THREE.Vector2(rimR, -width/2.05),
        new THREE.Vector2(rimR*0.6, -width/2.3),
        new THREE.Vector2(0.01, -width/2.05)
    ];
    let rimGeo = new THREE.LatheGeometry(rimPts, 24); extraGeoms.push(rimGeo);
    let rim = new THREE.Mesh(rimGeo, MATS.metalGear);

    let hubGeo = new THREE.CylinderGeometry(0.02, 0.02, width*1.2, 16); extraGeoms.push(hubGeo);
    let hub = new THREE.Mesh(hubGeo, MATS.heavyMetal);
    
    wGrp.add(tire, rim, hub);
    wGrp.rotation.z = Math.PI / 2;
    return wGrp;
}

function buildGearAssembly(scaleFactor, isMain) {
    let asm = new THREE.Group();
    
    let strutH = 0.4 * scaleFactor;
    let strutGeo = new THREE.CylinderGeometry(0.04 * scaleFactor, 0.04 * scaleFactor, strutH, 16); extraGeoms.push(strutGeo);
    let strut = new THREE.Mesh(strutGeo, MATS.metalGear);
    strut.position.y = -strutH / 2;
    asm.add(strut);

    let oleoH = 0.3 * scaleFactor;
    let oleoGeo = new THREE.CylinderGeometry(0.025 * scaleFactor, 0.025 * scaleFactor, oleoH, 16); extraGeoms.push(oleoGeo);
    let oleo = new THREE.Mesh(oleoGeo, MATS.heavyMetal);
    oleo.position.y = -strutH - (oleoH / 2);
    asm.add(oleo);

    let springCurve = new SpringCurve(0.06 * scaleFactor, strutH * 0.8, 5);
    let springGeo = new THREE.TubeGeometry(springCurve, 64, 0.01 * scaleFactor, 8, false); extraGeoms.push(springGeo);
    let spring = new THREE.Mesh(springGeo, MATS.darkArmor);
    spring.position.y = -strutH * 0.1;
    asm.add(spring);

    let axleY = -strutH - oleoH;
    
    if(isMain) {
        let axleW = 0.3 * scaleFactor;
        let axleGeo = new THREE.CylinderGeometry(0.02 * scaleFactor, 0.02 * scaleFactor, axleW, 16); axleGeo.rotateZ(Math.PI/2); extraGeoms.push(axleGeo);
        let axle = new THREE.Mesh(axleGeo, MATS.metalGear);
        axle.position.y = axleY;
        asm.add(axle);

        let wheelL = buildDetailedWheel(0.15 * scaleFactor, 0.08 * scaleFactor, 0.08 * scaleFactor); wheelL.position.set(-axleW/2, axleY, 0);
        let wheelR = buildDetailedWheel(0.15 * scaleFactor, 0.08 * scaleFactor, 0.08 * scaleFactor); wheelR.position.set(axleW/2, axleY, 0);
        asm.add(wheelL, wheelR);
    } else {
        let axleW = 0.2 * scaleFactor;
        let axleGeo = new THREE.CylinderGeometry(0.02 * scaleFactor, 0.02 * scaleFactor, axleW, 16); axleGeo.rotateZ(Math.PI/2); extraGeoms.push(axleGeo);
        let axle = new THREE.Mesh(axleGeo, MATS.metalGear);
        axle.position.y = axleY;
        asm.add(axle);

        let wheelL = buildDetailedWheel(0.10 * scaleFactor, 0.05 * scaleFactor, 0.06 * scaleFactor); wheelL.position.set(-axleW/2, axleY, 0);
        let wheelR = buildDetailedWheel(0.10 * scaleFactor, 0.05 * scaleFactor, 0.06 * scaleFactor); wheelR.position.set(axleW/2, axleY, 0);
        asm.add(wheelL, wheelR);
    }
    return asm;
}

function makeEggShape(geo) {
    const pos = geo.attributes.position; let minZ = 0;
    for (let i = 0; i < pos.count; i++) { let z = pos.getZ(i); if (z < minZ) minZ = z; }
    if (minZ === 0) return; 
    for (let i = 0; i < pos.count; i++) {
        let x = pos.getX(i); let z = pos.getZ(i);
        if (z < 0) { let ratio = z / minZ; let taper = 1.0 - (0.35 * ratio); pos.setX(i, x * taper); }
    }
    geo.computeVertexNormals();
}

function attachLoadout(grp, loadout, wX, wY, wZ, rY, rZ, fX, fY, fZ, paramWingSpan) {
    let attachGrp = new THREE.Group(); attachGrp.name = "attachments";
    let scaledWx = wX * paramWingSpan; 
    
    if(loadout.weapons === 'aam' || loadout.weapons === 'agm') {
        let isHeavy = loadout.weapons === 'agm';
        let bodyR = isHeavy ? 0.35 : 0.15;
        let bodyL = isHeavy ? 2.5 : 3.0;
        let mat = isHeavy ? MATS.darkArmor : MATS.heavyMetal;
        let makeWpn = () => {
            let wGrp = new THREE.Group();
            let bGeo = new THREE.CylinderGeometry(bodyR, bodyR, bodyL, 12); bGeo.rotateX(Math.PI/2); extraGeoms.push(bGeo);
            wGrp.add(new THREE.Mesh(bGeo, mat));
            if(isHeavy) {
                let nGeo = new THREE.SphereGeometry(bodyR, 12, 12); extraGeoms.push(nGeo);
                let nose = new THREE.Mesh(nGeo, MATS.heavyMetal); nose.position.z = -1.25; wGrp.add(nose);
            }
            let holsterRadius = bodyR * 1.5;
            let hGeo = new THREE.CylinderGeometry(holsterRadius, holsterRadius, bodyL * 1.1, 16, 1, false, -Math.PI/2, Math.PI);
            hGeo.rotateX(Math.PI/2); hGeo.rotateZ(Math.PI/2); 
            let holster = new THREE.Mesh(hGeo, MATS.stealthSkin); holster.position.y = bodyR * 0.2; 
            wGrp.add(holster); extraGeoms.push(hGeo);
            return wGrp;
        };
        let wL = makeWpn(); wL.position.set(scaledWx, wY, wZ); attachGrp.add(wL);
        let wR = makeWpn(); wR.position.set(-scaledWx, wY, wZ); attachGrp.add(wR);
        if(isHeavy) {
            let wL2 = makeWpn(); wL2.position.set(scaledWx + 1.2, wY, wZ + 0.5); attachGrp.add(wL2);
            let wR2 = makeWpn(); wR2.position.set(-scaledWx - 1.2, wY, wZ + 0.5); attachGrp.add(wR2);
        }
    }
    
    if(loadout.radar === 'awacs') {
        let rGrp = new THREE.Group();
        let sGeo = new THREE.BoxGeometry(0.4, 0.8, 1.0); extraGeoms.push(sGeo);
        rGrp.add(new THREE.Mesh(sGeo, MATS.darkArmor));
        let dGeo = new THREE.CylinderGeometry(2.5, 2.5, 0.4, 32); extraGeoms.push(dGeo);
        let dome = new THREE.Mesh(dGeo, MATS.stealthSkin); dome.position.y = 0.6; rGrp.add(dome);
        rGrp.position.set(0, rY, rZ); attachGrp.add(rGrp);
    }
    
    if(loadout.flares === 'flares') {
        let makeFlare = () => {
            let fGrp = new THREE.Group();
            let pGeo = new THREE.BoxGeometry(0.6, 0.4, 1.5); extraGeoms.push(pGeo);
            fGrp.add(new THREE.Mesh(pGeo, MATS.darkArmor));
            let flGeo = new THREE.BoxGeometry(0.4, 0.1, 0.4); extraGeoms.push(flGeo);
            let flare = new THREE.Mesh(flGeo, MATS.flareGlow); flare.position.set(0, -0.2, 0.4); fGrp.add(flare);
            return fGrp;
        };
        let fL = makeFlare(); fL.position.set(fX, fY, fZ); attachGrp.add(fL);
        let fR = makeFlare(); fR.position.set(-fX, fY, fZ); attachGrp.add(fR);
    }
    grp.add(attachGrp);
}

/* ==========================================
   MODEL BUILDERS (DRONE)
========================================== */
function buildAlphaConcept(loadout, p) {
    let grp = new THREE.Group(); grp.name = 'craftMaster';
    let fuse = new THREE.Group(); fuse.name = "fuselage";
    
    let topGeo = new THREE.SphereGeometry(1.3, 32, 16, 0, Math.PI*2, 0, Math.PI/2); 
    topGeo.scale(1.0, 0.75, 1.5 * p.fuse); makeEggShape(topGeo);
    let topDome = new THREE.Mesh(topGeo, MATS.darkArmor); topDome.position.set(0, 0.5, -5.5); fuse.add(topDome); extraGeoms.push(topGeo);
    
    let rimGeo = new THREE.CylinderGeometry(1.35, 1.35, 0.08, 32); rimGeo.scale(1.0, 1.0, 1.5 * p.fuse); makeEggShape(rimGeo); 
    let rim = new THREE.Mesh(rimGeo, MATS.stealthSkin); rim.position.set(0, 0.46, -5.5); fuse.add(rim); extraGeoms.push(rimGeo);
    
    let botGeo = new THREE.SphereGeometry(1.0, 32, 16, 0, Math.PI*2, Math.PI/2, Math.PI/2); 
    botGeo.scale(1.0, 0.8, 1.5 * p.fuse); makeEggShape(botGeo); 
    let botDome = new THREE.Mesh(botGeo, MATS.darkArmor); botDome.position.set(0, 0.42, -5.5); fuse.add(botDome); extraGeoms.push(botGeo);
    
    let shaftGeo = new THREE.CylinderGeometry(0.95, 0.40, 13 * p.fuse, 48, 40); shaftGeo.rotateX(Math.PI / 2);
    const pos = shaftGeo.attributes.position; const halfLen = (13 * p.fuse) / 2;
    for (let i = 0; i < pos.count; i++) {
        let x = pos.getX(i); let y = pos.getY(i); let z = pos.getZ(i);
        let t = THREE.MathUtils.clamp((z + halfLen) / (halfLen * 2), 0, 1);
        let xScale = THREE.MathUtils.lerp(1.12, 1.00, t); let isSeam = Math.abs(y) < 0.1;
        if (y >= 0) { if (!isSeam) y += 0.05; } else { let bottomScale = THREE.MathUtils.lerp(1.8, 1.10, t) + (Math.sin(t * Math.PI) * 0.4); y *= bottomScale; if (!isSeam) y -= 0.05; }
        x *= xScale; if (isSeam) { x *= 0.85; } pos.setXYZ(i, x, y, z);
    }
    pos.needsUpdate = true; shaftGeo.computeVertexNormals();
    let shaft = new THREE.Mesh(shaftGeo, MATS.stealthSkin); shaft.position.set(0, 0.3, 1.0); fuse.add(shaft); extraGeoms.push(shaftGeo);

    let gGroup = new THREE.Group(); gGroup.position.set(0, 0.42 - 0.8, -5.5);
    let gBaseGeo = new THREE.CylinderGeometry(0.25, 0.3, 0.1, 16); gGroup.add(new THREE.Mesh(gBaseGeo, MATS.heavyMetal)); extraGeoms.push(gBaseGeo);
    let gSphGeo = new THREE.SphereGeometry(0.22, 16, 16); let gSph = new THREE.Mesh(gSphGeo, MATS.stealthSkin); gSph.position.set(0, -0.15, 0); gGroup.add(gSph); extraGeoms.push(gSphGeo);
    fuse.add(gGroup); grp.add(fuse);

    let wingsGroup = new THREE.Group(); wingsGroup.name = "wings";
    let span = 14 * p.wing; let wingGeoL = new THREE.BoxGeometry(span, 1.0, 1.0, 32, 1, 16); wingGeoL.translate(span / 2, 0, 0);
    let posLW = wingGeoL.attributes.position;
    for (let i = 0; i < posLW.count; i++) {
        let rx = posLW.getX(i) / span; let zN = posLW.getZ(i) * 2.0; let y = posLW.getY(i);
        let profileY = zN > 0 ? Math.sqrt(1.0 - zN * zN) : 1.0 + zN;
        posLW.setXYZ(i, posLW.getX(i), (y > 0 ? profileY : -profileY) * (0.35 * (1.0 - 0.6 * rx) / 2.0), zN * (2.4 * (1.0 - 0.45 * rx) / 2.0));
    }
    wingGeoL.computeVertexNormals(); extraGeoms.push(wingGeoL);
    
    let wingGeoR = wingGeoL.clone(); let posRW = wingGeoR.attributes.position;
    for (let i = 0; i < posRW.count; i++) { posRW.setX(i, -posRW.getX(i)); } wingGeoR.computeVertexNormals(); extraGeoms.push(wingGeoR);

    let wLGrp = new THREE.Group(); wLGrp.name = "wingL"; wLGrp.position.set(0, 0.5, 0.5); wLGrp.rotation.set(0, p.wingSweep, p.wingDihedral); wLGrp.add(new THREE.Mesh(wingGeoL, MATS.darkArmor)); wingsGroup.add(wLGrp);
    let wRGrp = new THREE.Group(); wRGrp.name = "wingR"; wRGrp.position.set(0, 0.5, 0.5); wRGrp.rotation.set(0, -p.wingSweep, -p.wingDihedral); wRGrp.add(new THREE.Mesh(wingGeoR, MATS.darkArmor)); wingsGroup.add(wRGrp);
    grp.add(wingsGroup);

    let tail = new THREE.Group(); tail.name = "tail";
    let vTailSpan = 1.1 * p.finScale; let vTailGeoL = new THREE.BoxGeometry(vTailSpan, 1.0, 1.0, 16, 1, 16); vTailGeoL.translate(vTailSpan / 2, 0, 0);
    let posVL = vTailGeoL.attributes.position;
    for (let i = 0; i < posVL.count; i++) { posVL.setXYZ(i, posVL.getX(i), posVL.getY(i) * (0.15 * (1.0 - 0.6 * (posVL.getX(i) / vTailSpan))), posVL.getZ(i) * (1.4 * (1.0 - 0.5 * (posVL.getX(i) / vTailSpan)))); }
    vTailGeoL.computeVertexNormals(); extraGeoms.push(vTailGeoL);
    let vTailGeoR = vTailGeoL.clone(); let posVR = vTailGeoR.attributes.position;
    for (let i = 0; i < posVR.count; i++) { posVR.setX(i, -posVR.getX(i)); } vTailGeoR.computeVertexNormals(); extraGeoms.push(vTailGeoR);

    let tailLGrp = new THREE.Group(); tailLGrp.add(new THREE.Mesh(vTailGeoL, MATS.stealthSkin)); tailLGrp.position.set(0.3, 0.6, 6.8 * p.fuse); tailLGrp.rotation.set(0, p.finSweep, p.finAngle); tail.add(tailLGrp);
    let tailRGrp = new THREE.Group(); tailRGrp.add(new THREE.Mesh(vTailGeoR, MATS.stealthSkin)); tailRGrp.position.set(-0.3, 0.6, 6.8 * p.fuse); tailRGrp.rotation.set(0, -p.finSweep, -p.finAngle); tail.add(tailRGrp);
    grp.add(tail);

    let eng = new THREE.Group(); eng.name = "engines";
    let oNGeo = new THREE.CylinderGeometry(0.5, 0.6, 1.5, 32); oNGeo.rotateX(Math.PI / 2); let oN = new THREE.Mesh(oNGeo, MATS.darkArmor); oN.position.set(0, 0.3, 7.5 * p.fuse); eng.add(oN); extraGeoms.push(oNGeo);
    grp.add(eng);

    // Add Landing Gear
    let gearGroup = new THREE.Group(); gearGroup.name = "landingGear";
    let noseGear = buildGearAssembly(0.9, false); noseGear.position.set(0, 0.1, -4.5 * p.fuse); gearGroup.add(noseGear);
    let mainGearL = buildGearAssembly(0.9, true); mainGearL.position.set(-1.8, 0.1, 1.5 * p.fuse); gearGroup.add(mainGearL);
    let mainGearR = buildGearAssembly(0.9, true); mainGearR.position.set(1.8, 0.1, 1.5 * p.fuse); gearGroup.add(mainGearR);
    grp.add(gearGroup);

    attachLoadout(grp, loadout, 4, -0.2, 0.5,  1.5, 0,  0.8, -0.5, 6.0 * p.fuse, p.wing);
    return grp;
}

/* ==========================================
   MODEL BUILDERS (B-2 IDW MESH)
========================================== */
function sqr(x) { return x * x; }
function dist2(v, w) { return sqr(v.x - w.x) + sqr(v.z - w.z); }
function distToSegment(p, v, w) {
    var l2 = dist2(v, w);
    if (l2 === 0) return Math.sqrt(dist2(p, v));
    var t = ((p.x - v.x) * (w.x - v.x) + (p.z - v.z) * (w.z - v.z)) / l2;
    t = Math.max(0, Math.min(1, t));
    return Math.sqrt(dist2(p, { x: v.x + t * (w.x - v.x), z: v.z + t * (w.z - v.z) }));
}

function getZLE(x) { return -200 + 0.65 * Math.abs(x); }
function getZTE(x) {
    let absX = Math.abs(x);
    if (absX <= 80) return 242 + 0.65 * absX;
    if (absX <= 220) return 294 - 0.65 * (absX - 80);
    if (absX <= 320) return 203 + 0.65 * (absX - 220);
    return 268 - 0.65 * (absX - 320);
}

function evalProfile(data, coord, side) {
    if(coord <= data[0].coord) return data[0][side];
    if(coord >= data[data.length-1].coord) return data[data.length-1][side];
    for(let i=0; i<data.length-1; i++){
        if(coord >= data[i].coord && coord <= data[i+1].coord){
            let t = (coord - data[i].coord) / (data[i+1].coord - data[i].coord);
            return data[i][side] + t * (data[i+1][side] - data[i][side]);
        }
    }
    return 0;
}

function calcIDWHeight(x, z, side) {
    let p = {x: x, z: z};
    let eps = 0.01; let power = 3; 

    let d1 = distToSegment(p, {x:0, z:-200}, {x:0, z:242});
    let d2 = distToSegment(p, {x:-520, z:138}, {x:520, z:138});
    let d3 = distToSegment(p, {x:-300, z:-40}, {x:300, z:-40});
    let d4 = distToSegment(p, {x:-300, z:160}, {x:300, z:160});

    let d5 = Infinity; 
    for(let i=0; i<b2Coordinates.length; i++) {
        let pt1 = { x: b2Coordinates[i].x, z: b2Coordinates[i].y };
        let pt2 = { x: b2Coordinates[(i+1)%b2Coordinates.length].x, z: b2Coordinates[(i+1)%b2Coordinates.length].y };
        d5 = Math.min(d5, distToSegment(p, pt1, pt2));
    }

    let w1 = 1.0 / (Math.pow(d1, power) + eps);
    let w2 = 1.0 / (Math.pow(d2, power) + eps);
    let w3 = 1.0 / (Math.pow(d3, power) + eps);
    let w4 = 1.0 / (Math.pow(d4, power) + eps);
    let w5 = 3.0 / (Math.pow(d5, power) + eps); 

    let y1 = evalProfile(longData, z, side);
    let y2 = evalProfile(transData, x, side);
    let y3 = evalProfile(engFrontData, x, side);
    let y4 = evalProfile(engRearData, x, side);
    let y5 = 0; 

    return (w1*y1 + w2*y2 + w3*y3 + w4*y4 + w5*y5) / (w1 + w2 + w3 + w4 + w5);
}

function buildB2Bomber() {
    let grp = new THREE.Group(); grp.name = 'craftMaster';
    
    // 1. Mesh Hull
    const uSegments = 100, vSegments = 50;
    const verticesTop = [], verticesBot = [], indices = [];

    for(let i=0; i<=uSegments; i++){
        let u = i / uSegments;
        let x = -520 + u * 1040;
        let zLE = getZLE(x);
        let zTE = getZTE(x);

        for(let j=0; j<=vSegments; j++){
            let v = j / vSegments;
            let z = zLE + v * (zTE - zLE);
            verticesTop.push(x, calcIDWHeight(x, z, 'top'), z);
            verticesBot.push(x, calcIDWHeight(x, z, 'bottom'), z);
        }
    }

    let vOffset = (uSegments + 1) * (vSegments + 1);
    for(let i=0; i<uSegments; i++){
        for(let j=0; j<vSegments; j++){
            let a = i * (vSegments + 1) + j;
            let b = i * (vSegments + 1) + j + 1;
            let c = (i + 1) * (vSegments + 1) + j;
            let d = (i + 1) * (vSegments + 1) + j + 1;

            indices.push(a, b, c); indices.push(c, b, d);
            
            let aB = a + vOffset, bB = b + vOffset, cB = c + vOffset, dB = d + vOffset;
            indices.push(aB, cB, bB); indices.push(cB, dB, bB);
        }
    }

    let b2Geo = new THREE.BufferGeometry();
    b2Geo.setAttribute('position', new THREE.Float32BufferAttribute([...verticesTop, ...verticesBot], 3));
    b2Geo.setIndex(indices);
    b2Geo.computeVertexNormals();

    let hull = new THREE.Mesh(b2Geo, MATS.stealthSkin);
    hull.name = "fuselage";
    
    let sc = 0.035;
    hull.scale.set(sc, sc, sc);
    grp.add(hull);

    // 2. Cockpit Canopy
    let canopyGeo = new THREE.SphereGeometry(0.5, 32, 16); extraGeoms.push(canopyGeo);
    let canopy = new THREE.Mesh(canopyGeo, MATS.glass);
    canopy.scale.set(1.5, 0.4, 3.5);
    canopy.position.set(0, 1.8, -2.5);
    canopy.name = "cockpit";
    grp.add(canopy);

    // 3. Engine Glows
    let glowGeo = new THREE.BoxGeometry(2.5, 0.1, 0.5); extraGeoms.push(glowGeo);
    let glowL = new THREE.Mesh(glowGeo, MATS.engineFire);
    glowL.position.set(-3.5, 0.5, 5.2);
    glowL.name = "engineL";
    grp.add(glowL);
    
    let glowR = new THREE.Mesh(glowGeo, MATS.engineFire);
    glowR.position.set(3.5, 0.5, 5.2);
    glowR.name = "engineR";
    grp.add(glowR);

    // 4. Bomb Bays
    let bayGeo = new THREE.BoxGeometry(2.0, 0.1, 4.0); extraGeoms.push(bayGeo);
    let bayL = new THREE.Mesh(bayGeo, MATS.bombBaySkin);
    bayL.position.set(-2.0, -0.6, 1.0);
    bayL.name = "bombBayL";
    grp.add(bayL);
    
    let bayR = new THREE.Mesh(bayGeo, MATS.bombBaySkin);
    bayR.position.set(2.0, -0.6, 1.0);
    bayR.name = "bombBayR";
    grp.add(bayR);

    // 5. Landing Gear
    let gearGroup = new THREE.Group(); gearGroup.name = "landingGear";
    let noseGear = buildGearAssembly(1.5, false);
    noseGear.position.set(0, -0.2, -4.5);
    gearGroup.add(noseGear);

    let mainGearL = buildGearAssembly(1.5, true);
    mainGearL.position.set(-3.5, -0.2, 2.5);
    gearGroup.add(mainGearL);

    let mainGearR = buildGearAssembly(1.5, true);
    mainGearR.position.set(3.5, -0.2, 2.5);
    gearGroup.add(mainGearR);

    grp.add(gearGroup);

    return grp;
}

/* ==========================================
   RENDER LIFECYCLE
========================================== */
function init() {
    viewList.forEach((v, idx) => {
        scenes[v] = new THREE.Scene(); 
        scenes[v].background = new THREE.Color(0x0e121a); 
        
        let grid = new THREE.GridHelper(200, 40, 0x3a2a66, 0x1a1230); grid.position.y = -10; scenes[v].add(grid);
        
        let planeGeo = new THREE.PlaneGeometry(300, 300);
        let planeMat = new THREE.ShadowMaterial({ opacity: 0.6 });
        let plane = new THREE.Mesh(planeGeo, planeMat);
        plane.rotation.x = -Math.PI / 2; plane.position.y = -10.1;
        plane.receiveShadow = true; scenes[v].add(plane);

        cameras[v] = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10000);
        let cont = document.getElementById('vp'+v);
        
        renderers[v] = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderers[v].setSize(cont.clientWidth||800, cont.clientHeight||600);
        
        renderers[v].outputEncoding = THREE.sRGBEncoding;
        renderers[v].toneMapping = THREE.ACESFilmicToneMapping;
        renderers[v].toneMappingExposure = 1.1;
        renderers[v].shadowMap.enabled = true;
        renderers[v].shadowMap.type = THREE.PCFSoftShadowMap;
        cont.appendChild(renderers[v].domElement);
        
        if(idx === 0) sharedEnvTexture = createStudioEnvironment(renderers[v]);
        scenes[v].environment = sharedEnvTexture;

        scenes[v].add(new THREE.AmbientLight(0xffffff, 0.2));
        
        let mainLight = new THREE.DirectionalLight(0xfff5ee, 1.8); 
        mainLight.position.set(60, 100, 60); 
        mainLight.castShadow = true;
        mainLight.shadow.mapSize.width = 2048;
        mainLight.shadow.mapSize.height = 2048;
        let d = 60; 
        mainLight.shadow.camera.left = -d; mainLight.shadow.camera.right = d;
        mainLight.shadow.camera.top = d; mainLight.shadow.camera.bottom = -d;
        scenes[v].add(mainLight);

        let fillLight = new THREE.DirectionalLight(0x88bbff, 0.8); 
        fillLight.position.set(-60, 20, -60); 
        scenes[v].add(fillLight);
        
        controlsObj[v] = new OrbitControls(cameras[v], renderers[v].domElement);
        controlsObj[v].enableDamping = true;
        controlsObj[v].dampingFactor = 0.05;
    });
    
    window.addEventListener('resize', window.resizeActiveViewport);
    setTimeout(() => { window.updateModel(); }, 100);
    animate();
}

window.updateModel = () => {
    extraGeoms.forEach(g => g.dispose()); extraGeoms = [];
    viewList.forEach(v => { let g = scenes[v].children.filter(c => c.name.startsWith('craft')); g.forEach(x => scenes[v].remove(x)); });
    
    let configId = document.getElementById('craftConfig').value;
    let style = document.getElementById('renderStyle').value;
    
    if (configId === 'b2_bomber') {
        document.getElementById('legacy-controls').style.display = 'none';
        document.getElementById('b2-controls').style.display = 'flex';
    } else {
        document.getElementById('legacy-controls').style.display = 'flex';
        document.getElementById('b2-controls').style.display = 'none';
    }

    let loadout = { weapons: document.getElementById('loadoutWeapons').value, radar: document.getElementById('loadoutRadar').value, flares: document.getElementById('loadoutFlares').value };
    let params = { wing: document.getElementById('paramWingSpan').value / 100, fuse: document.getElementById('paramFuseLen').value / 100, wingSweep: THREE.MathUtils.degToRad(document.getElementById('paramWingSweep').value), wingDihedral: THREE.MathUtils.degToRad(document.getElementById('paramWingDihedral').value), finSweep: THREE.MathUtils.degToRad(document.getElementById('paramFinSweep').value), finAngle: THREE.MathUtils.degToRad(document.getElementById('paramFinAngle').value), finScale: document.getElementById('paramFinScale').value / 100 };

    let titleText = document.getElementById('craftConfig').options[document.getElementById('craftConfig').selectedIndex].text;
    viewList.forEach(v => {
        document.getElementById(`title-${v}`).innerText = titleText.split(' (')[0];
        document.getElementById(`class-${v}`).innerText = configId === 'b2_bomber' ? "STRATEGIC BOMBER" : "AUTONOMOUS SYSTEM";
    });

    let masterAsm = configId === 'drone_legacy' ? buildAlphaConcept(loadout, params) : buildB2Bomber();

    // Smart Material Override for both Drone & B2 Models
    masterAsm.traverse(child => {
        if(child.isMesh) {
            if(!child.userData.origMat) child.userData.origMat = child.material;
            
            if(child.userData.edges) { child.remove(child.userData.edges); child.userData.edges.geometry.dispose(); child.userData.edges = null; }

            if(style === 'wireframe') {
                let edgesGeo = new THREE.EdgesGeometry(child.geometry, 15); extraGeoms.push(edgesGeo);
                let edges = new THREE.LineSegments(edgesGeo, MATS.holoWire);
                child.add(edges);
                child.userData.edges = edges;
                
                child.material = new THREE.MeshBasicMaterial({color: 0x0e121a, transparent: true, opacity: 0.85, side: THREE.DoubleSide});
                child.castShadow = false; child.receiveShadow = false;
            } else if(style === 'xray') {
                let xMat = child.userData.origMat.clone();
                xMat.transparent = true; xMat.opacity = 0.15; xMat.depthWrite = false;
                child.material = xMat;
                child.castShadow = false; child.receiveShadow = false;
            } else {
                child.material = child.userData.origMat;
                child.castShadow = true; child.receiveShadow = true;
                
                if(configId === 'b2_bomber' && child.name === 'fuselage') {
                    let wireGeo = new THREE.WireframeGeometry(child.geometry); extraGeoms.push(wireGeo);
                    let edges = new THREE.LineSegments(wireGeo, new THREE.LineBasicMaterial({color: 0x00e5ff, transparent: true, opacity: 0.05}));
                    child.add(edges);
                    child.userData.edges = edges;
                }
            }
        }
    });

    viewList.forEach(v => {
        let clone = masterAsm.clone(); clone.name = 'craft_' + v;
        
        let cWingL = clone.getObjectByName('wingL');
        let cWingR = clone.getObjectByName('wingR');
        let cTail = clone.getObjectByName('tail');
        let cEng = clone.getObjectByName('engines');
        let cAtt = clone.getObjectByName('attachments');
        let cFus = clone.getObjectByName('fuselage');
        let cCockpit = clone.getObjectByName('cockpit');
        let cGear = clone.getObjectByName('landingGear');
        let cBayL = clone.getObjectByName('bombBayL');
        let cBayR = clone.getObjectByName('bombBayR');
        let cEngL = clone.getObjectByName('engineL');
        let cEngR = clone.getObjectByName('engineR');

        if(v === '5') { 
            let val = 1.0; 
            if(configId === 'b2_bomber') {
                if (cFus) { cFus.position.y += val * 2.5; }
                if (cCockpit) { cCockpit.position.y += val * 3.5; }
                if (cBayL) cBayL.position.y -= val * 3.0; 
                if (cBayR) cBayR.position.y -= val * 3.0;
                if (cGear) cGear.position.y -= val * 4.5;
                if (cEngL) { cEngL.position.y += val * 3.0; cEngL.position.x -= 2.0; }
                if (cEngR) { cEngR.position.y += val * 3.0; cEngR.position.x += 2.0; }
            } else {
                if (cWingL && cWingR) { cWingL.position.x += val * 12.0; cWingR.position.x -= val * 12.0; }
                if (cTail) { cTail.position.y += val * 8.0; cTail.position.z += val * 4.0; }
                if (cEng) { cEng.position.z += val * 10.0; }
                if (cAtt) { cAtt.position.y -= val * 8.0; }
            }
        }
        scenes[v].add(clone);
    });

    let sBase = configId === 'b2_bomber' ? 22 : 12;
    cameras['1'].position.set(0, 0, -150); cameras['1'].lookAt(0, 0, 0); cameras['1'].userData.baseSize = sBase - 2;
    cameras['2'].position.set(0, 150, 0); cameras['2'].lookAt(0, 0, 0); cameras['2'].userData.baseSize = sBase + 4;
    cameras['3'].position.set(150, 0, 0); cameras['3'].lookAt(0, 0, 0); cameras['3'].userData.baseSize = sBase + 2;
    cameras['4'].position.set(-60, 40, -60); cameras['4'].lookAt(0, 0, 0); cameras['4'].userData.baseSize = sBase + 6;
    cameras['5'].position.set(-60, 40, -60); cameras['5'].lookAt(0, 0, 0); cameras['5'].userData.baseSize = sBase + 14;
    
    viewList.forEach(v => { controlsObj[v].target.set(0,0,0); controlsObj[v].update(); });

    window.resizeActiveViewport();
};

window.resizeActiveViewport = () => {
    let activeVps = [currentPage.toString()];
    activeVps.forEach(v => {
        let vp = document.getElementById('vp' + v);
        if(!vp || vp.clientWidth === 0) return;
        let aspect = vp.clientWidth / vp.clientHeight;
        renderers[v].setSize(vp.clientWidth, vp.clientHeight);
        
        let cam = cameras[v]; 
        let s = cam.userData.baseSize || 20;
        let currentZoom = cam.zoom || 1;
        cam.left = -s * aspect / currentZoom; 
        cam.right = s * aspect / currentZoom; 
        cam.top = s / currentZoom; 
        cam.bottom = -s / currentZoom; 
        cam.updateProjectionMatrix();
    });
};

window.changePage = (dir) => {
    document.getElementById('sheet'+currentPage).classList.remove('active');
    currentPage += dir; if(currentPage < 1) currentPage = maxPages; if(currentPage > maxPages) currentPage = 1;
    document.getElementById('sheet'+currentPage).classList.add('active');
    setTimeout(window.resizeActiveViewport, 10);
};

function animate() {
    requestAnimationFrame(animate);
    let activeVps = [currentPage.toString()];
    activeVps.forEach(v => { 
        let vp = document.getElementById('vp'+v); 
        if(vp && vp.clientWidth > 0) {
            controlsObj[v].update();
            let cam = cameras[v];
            let aspect = vp.clientWidth / vp.clientHeight;
            let s = cam.userData.baseSize || 20;
            cam.left = -s * aspect / cam.zoom; 
            cam.right = s * aspect / cam.zoom; 
            cam.top = s / cam.zoom; 
            cam.bottom = -s / cam.zoom; 
            cam.updateProjectionMatrix();
            renderers[v].render(scenes[v], cameras[v]); 
        }
    });
}
window.onload = init;
</script>
</body>
</html>

The window lol:

Not too bad looking for my first airplanes:

I use Blender to create my aircraft. I generally use cylinders to create almost everything, including the wings (which are flattened cylinders).

But you seem to have done a good job using three.js to create a flying wing.