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)">❮</button>
<button class="side-nav-btn side-nav-right" onclick="window.changePage(1)">❯</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()">× 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()">× 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




