Add complete manufacturing build pipeline with enhanced features
Build Pipeline: - Generate schematic PDF with all sheets - Generate 11 individual board layer PDFs (copper, silkscreen, soldermask, paste, fabrication, outline) - Add "Layer: <name>" text overlay to each board layer PDF - Merge all board PDFs into single document - Generate Gerbers with all layers - Generate drill files in Excellon format - Generate ODB++ package (optional) - Export STEP 3D model - Generate BOMs for active variant - Package all outputs into timestamped ZIP file - Real-time progress bar (0-100%) with status updates - Detailed build log with timestamps - Auto-download ZIP on completion Symbol & Footprint Library Integration: - Browse KiCad symbol libraries from UM_KICAD environment variable - Live SVG preview of selected symbols with pins, graphics, and labels - Browse KiCad footprint libraries (.pretty directories) - Live SVG preview of selected footprints with pads and silkscreen - Associate symbols and footprints with parts in database - Store as LibraryName:ComponentName format WebSocket Connection Improvements: - Increase ping timeout to 120 seconds (from 60s default) - Add 25-second ping interval to keep connections alive - Wait 10 seconds for reconnection before shutdown (handles page refresh) - Cancel shutdown timer when client reconnects - Use hidden link download to preserve WebSocket connection (not window.location) PDF Text Overlay: - Add reportlab and PyPDF2 imports for PDF manipulation - Add add_text_overlay_to_pdf() helper function - Overlay layer names in upper left corner of board PDFs - Use Helvetica-Bold 14pt font at position (50, 750) Bug Fixes: - Fix BOM generator argument order (schematic, project, variant, dnp_uuids, pcb_file) - Pass empty JSON array '[]' for dnp_uuids instead of output directory - Move generated BOM files from project dir to output dir for packaging - Fix datetime import (was missing) - Use app_args instead of config for getting schematic/board file paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -356,16 +356,34 @@
|
||||
<p>Generate all manufacturing outputs in a single operation with progress feedback.</p>
|
||||
|
||||
<div class="actions">
|
||||
<h3>Coming Soon</h3>
|
||||
<p>This tab will allow you to:</p>
|
||||
<ul style="margin-left: 20px; line-height: 1.8;">
|
||||
<li>Generate PDFs (Schematic + Board)</li>
|
||||
<li>Export Gerbers & Drill Files</li>
|
||||
<li>Export STEP model</li>
|
||||
<li>Generate BOM</li>
|
||||
<li>Run design checks</li>
|
||||
</ul>
|
||||
<p style="margin-top: 20px;">All with a single button press and real-time progress tracking.</p>
|
||||
<button id="buildAllBtn" class="btn" style="font-size: 16px; padding: 12px 24px; margin: 20px 0;">
|
||||
🚀 Build All Manufacturing Outputs
|
||||
</button>
|
||||
|
||||
<div id="buildProgress" style="display: none; margin-top: 30px;">
|
||||
<h3>Build Progress</h3>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div style="background: #e0e0e0; border-radius: 10px; height: 30px; margin: 20px 0; position: relative; overflow: hidden;">
|
||||
<div id="buildProgressBar" style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: 0%; transition: width 0.3s ease; display: flex; align-items: center; justify-content: center;">
|
||||
<span id="buildProgressPercent" style="color: white; font-weight: bold; font-size: 14px; position: absolute;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Status -->
|
||||
<div id="buildStatus" style="padding: 15px; background: #f8f9fa; border-left: 4px solid #007bff; border-radius: 5px; margin-bottom: 20px;">
|
||||
<strong>Status:</strong> <span id="buildStatusText">Initializing...</span>
|
||||
</div>
|
||||
|
||||
<!-- Build Steps Log -->
|
||||
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; max-height: 300px; overflow-y: auto;">
|
||||
<h4 style="margin-top: 0;">Build Log:</h4>
|
||||
<div id="buildLog" style="font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.6;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="buildMessage" class="message" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End publishTab -->
|
||||
@@ -427,6 +445,43 @@
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Datasheet URL (optional):</label>
|
||||
<input type="text" id="datasheetInput" placeholder="https://..." style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<!-- Symbol Selection -->
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Symbol (optional):</label>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div>
|
||||
<select id="symbolLibrarySelect" style="width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
|
||||
<option value="">-- Select Library --</option>
|
||||
</select>
|
||||
<select id="symbolSelect" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;" disabled>
|
||||
<option value="">-- Select Symbol --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="symbolPreview" style="border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; background: white; min-height: 150px; display: flex; align-items: center; justify-content: center; overflow: auto;">
|
||||
<span style="color: #999;">No symbol selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footprint Selection -->
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Footprint (optional):</label>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div>
|
||||
<select id="footprintLibrarySelect" style="width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
|
||||
<option value="">-- Select Library --</option>
|
||||
</select>
|
||||
<select id="footprintSelect" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;" disabled>
|
||||
<option value="">-- Select Footprint --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="footprintPreview" style="border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; background: #2C2C2C; min-height: 150px; display: flex; align-items: center; justify-content: center; overflow: auto;">
|
||||
<span style="color: #999;">No footprint selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="createPartBtn" class="btn" disabled>Create Part</button>
|
||||
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
|
||||
</div>
|
||||
@@ -990,7 +1045,21 @@
|
||||
const datasheetInput = document.getElementById('datasheetInput');
|
||||
const createPartBtn = document.getElementById('createPartBtn');
|
||||
|
||||
// Symbol and Footprint selection elements
|
||||
const symbolLibrarySelect = document.getElementById('symbolLibrarySelect');
|
||||
const symbolSelect = document.getElementById('symbolSelect');
|
||||
const symbolPreview = document.getElementById('symbolPreview');
|
||||
const footprintLibrarySelect = document.getElementById('footprintLibrarySelect');
|
||||
const footprintSelect = document.getElementById('footprintSelect');
|
||||
const footprintPreview = document.getElementById('footprintPreview');
|
||||
|
||||
let missingPartsData = [];
|
||||
let symbolLibraries = [];
|
||||
let footprintLibraries = [];
|
||||
let selectedSymbolLibraryPath = '';
|
||||
let selectedFootprintLibraryPath = '';
|
||||
let selectedSymbol = '';
|
||||
let selectedFootprint = '';
|
||||
|
||||
function showCreatePartMessage(text, type) {
|
||||
createPartMessage.textContent = text;
|
||||
@@ -1006,6 +1075,9 @@
|
||||
createPartForm.style.display = 'none';
|
||||
createPartMessage.style.display = 'none';
|
||||
socket.emit('get_missing_ipns');
|
||||
// Load symbol and footprint libraries
|
||||
socket.emit('get_symbol_libraries');
|
||||
socket.emit('get_footprint_libraries');
|
||||
});
|
||||
|
||||
// Cancel and return to browser
|
||||
@@ -1020,10 +1092,142 @@
|
||||
manufacturerInput.value = '';
|
||||
mpnInput.value = '';
|
||||
datasheetInput.value = '';
|
||||
symbolLibrarySelect.value = '';
|
||||
symbolSelect.value = '';
|
||||
symbolSelect.disabled = true;
|
||||
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
|
||||
footprintLibrarySelect.value = '';
|
||||
footprintSelect.value = '';
|
||||
footprintSelect.disabled = true;
|
||||
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
||||
selectedSymbol = '';
|
||||
selectedFootprint = '';
|
||||
createPartBtn.disabled = true;
|
||||
missingPartsData = [];
|
||||
});
|
||||
|
||||
// Symbol library handlers
|
||||
socket.on('symbol_libraries_result', (data) => {
|
||||
symbolLibraries = data.libraries;
|
||||
symbolLibrarySelect.innerHTML = '<option value="">-- Select Library --</option>';
|
||||
for (const lib of data.libraries) {
|
||||
const option = document.createElement('option');
|
||||
option.value = lib.path;
|
||||
option.textContent = lib.name;
|
||||
symbolLibrarySelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
symbolLibrarySelect.addEventListener('change', () => {
|
||||
const libraryPath = symbolLibrarySelect.value;
|
||||
if (!libraryPath) {
|
||||
symbolSelect.innerHTML = '<option value="">-- Select Symbol --</option>';
|
||||
symbolSelect.disabled = true;
|
||||
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
|
||||
selectedSymbol = '';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedSymbolLibraryPath = libraryPath;
|
||||
symbolSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
symbolSelect.disabled = true;
|
||||
socket.emit('get_symbols_in_library', { library_path: libraryPath });
|
||||
});
|
||||
|
||||
socket.on('symbols_in_library_result', (data) => {
|
||||
symbolSelect.innerHTML = '<option value="">-- Select Symbol --</option>';
|
||||
for (const symbol of data.symbols) {
|
||||
const option = document.createElement('option');
|
||||
option.value = symbol;
|
||||
option.textContent = symbol;
|
||||
symbolSelect.appendChild(option);
|
||||
}
|
||||
symbolSelect.disabled = false;
|
||||
});
|
||||
|
||||
symbolSelect.addEventListener('change', () => {
|
||||
const symbolName = symbolSelect.value;
|
||||
if (!symbolName) {
|
||||
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
|
||||
selectedSymbol = '';
|
||||
return;
|
||||
}
|
||||
|
||||
symbolPreview.innerHTML = '<span style="color: #999;">Loading...</span>';
|
||||
socket.emit('render_symbol', {
|
||||
library_path: selectedSymbolLibraryPath,
|
||||
symbol_name: symbolName
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('symbol_render_result', (data) => {
|
||||
symbolPreview.innerHTML = data.svg;
|
||||
// Store as LibraryName:SymbolName format
|
||||
const libName = symbolLibrarySelect.options[symbolLibrarySelect.selectedIndex].text;
|
||||
selectedSymbol = `${libName}:${data.symbol_name}`;
|
||||
});
|
||||
|
||||
// Footprint library handlers
|
||||
socket.on('footprint_libraries_result', (data) => {
|
||||
footprintLibraries = data.libraries;
|
||||
footprintLibrarySelect.innerHTML = '<option value="">-- Select Library --</option>';
|
||||
for (const lib of data.libraries) {
|
||||
const option = document.createElement('option');
|
||||
option.value = lib.path;
|
||||
option.textContent = lib.name;
|
||||
footprintLibrarySelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
footprintLibrarySelect.addEventListener('change', () => {
|
||||
const libraryPath = footprintLibrarySelect.value;
|
||||
if (!libraryPath) {
|
||||
footprintSelect.innerHTML = '<option value="">-- Select Footprint --</option>';
|
||||
footprintSelect.disabled = true;
|
||||
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
||||
selectedFootprint = '';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFootprintLibraryPath = libraryPath;
|
||||
footprintSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
footprintSelect.disabled = true;
|
||||
socket.emit('get_footprints_in_library', { library_path: libraryPath });
|
||||
});
|
||||
|
||||
socket.on('footprints_in_library_result', (data) => {
|
||||
footprintSelect.innerHTML = '<option value="">-- Select Footprint --</option>';
|
||||
for (const footprint of data.footprints) {
|
||||
const option = document.createElement('option');
|
||||
option.value = footprint;
|
||||
option.textContent = footprint;
|
||||
footprintSelect.appendChild(option);
|
||||
}
|
||||
footprintSelect.disabled = false;
|
||||
});
|
||||
|
||||
footprintSelect.addEventListener('change', () => {
|
||||
const footprintName = footprintSelect.value;
|
||||
if (!footprintName) {
|
||||
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
||||
selectedFootprint = '';
|
||||
return;
|
||||
}
|
||||
|
||||
footprintPreview.innerHTML = '<span style="color: #999;">Loading...</span>';
|
||||
socket.emit('render_footprint', {
|
||||
library_path: selectedFootprintLibraryPath,
|
||||
footprint_name: footprintName
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('footprint_render_result', (data) => {
|
||||
footprintPreview.innerHTML = data.svg;
|
||||
// Store as LibraryName:FootprintName format
|
||||
const libName = footprintLibrarySelect.options[footprintLibrarySelect.selectedIndex].text;
|
||||
selectedFootprint = `${libName}:${data.footprint_name}`;
|
||||
});
|
||||
|
||||
socket.on('missing_ipns_result', (data) => {
|
||||
createPartFormLoading.style.display = 'none';
|
||||
|
||||
@@ -1085,7 +1289,9 @@
|
||||
class: part.class,
|
||||
manufacturer: part.manufacturer,
|
||||
mpn: part.mpn,
|
||||
datasheet: datasheetInput.value.trim()
|
||||
datasheet: datasheetInput.value.trim(),
|
||||
symbol: selectedSymbol,
|
||||
footprint: selectedFootprint
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1110,6 +1316,16 @@
|
||||
manufacturerInput.value = '';
|
||||
mpnInput.value = '';
|
||||
datasheetInput.value = '';
|
||||
symbolLibrarySelect.value = '';
|
||||
symbolSelect.value = '';
|
||||
symbolSelect.disabled = true;
|
||||
symbolPreview.innerHTML = '<span style="color: #999;">No symbol selected</span>';
|
||||
footprintLibrarySelect.value = '';
|
||||
footprintSelect.value = '';
|
||||
footprintSelect.disabled = true;
|
||||
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
||||
selectedSymbol = '';
|
||||
selectedFootprint = '';
|
||||
createPartBtn.disabled = true;
|
||||
|
||||
if (missingPartsData.length === 0) {
|
||||
@@ -1118,6 +1334,77 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Build Pipeline
|
||||
// ========================================
|
||||
const buildAllBtn = document.getElementById('buildAllBtn');
|
||||
const buildProgress = document.getElementById('buildProgress');
|
||||
const buildProgressBar = document.getElementById('buildProgressBar');
|
||||
const buildProgressPercent = document.getElementById('buildProgressPercent');
|
||||
const buildStatusText = document.getElementById('buildStatusText');
|
||||
const buildLog = document.getElementById('buildLog');
|
||||
const buildMessage = document.getElementById('buildMessage');
|
||||
|
||||
function showBuildMessage(text, type) {
|
||||
buildMessage.textContent = text;
|
||||
buildMessage.className = 'message ' + type;
|
||||
buildMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
function addBuildLogEntry(text, isError = false) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const color = isError ? '#dc3545' : '#333';
|
||||
buildLog.innerHTML += `<div style="color: ${color};">[${timestamp}] ${text}</div>`;
|
||||
buildLog.parentElement.scrollTop = buildLog.parentElement.scrollHeight;
|
||||
}
|
||||
|
||||
function updateBuildProgress(percent, status) {
|
||||
buildProgressBar.style.width = percent + '%';
|
||||
buildProgressPercent.textContent = percent + '%';
|
||||
buildStatusText.textContent = status;
|
||||
}
|
||||
|
||||
buildAllBtn.addEventListener('click', () => {
|
||||
buildAllBtn.disabled = true;
|
||||
buildProgress.style.display = 'block';
|
||||
buildMessage.style.display = 'none';
|
||||
buildLog.innerHTML = '';
|
||||
updateBuildProgress(0, 'Starting build...');
|
||||
addBuildLogEntry('Build pipeline started');
|
||||
|
||||
socket.emit('build_all');
|
||||
});
|
||||
|
||||
socket.on('build_progress', (data) => {
|
||||
updateBuildProgress(data.percent, data.status);
|
||||
addBuildLogEntry(data.message);
|
||||
});
|
||||
|
||||
socket.on('build_complete', (data) => {
|
||||
updateBuildProgress(100, 'Build complete!');
|
||||
addBuildLogEntry('✓ Build completed successfully');
|
||||
buildAllBtn.disabled = false;
|
||||
|
||||
// Auto-download the ZIP file using hidden link (preserves WebSocket connection)
|
||||
if (data.download_url) {
|
||||
addBuildLogEntry('Downloading build package...');
|
||||
const link = document.createElement('a');
|
||||
link.href = data.download_url;
|
||||
link.download = data.filename || 'manufacturing.zip';
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
showBuildMessage('Build complete! Download started automatically.', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('build_error', (data) => {
|
||||
addBuildLogEntry('✗ Error: ' + data.error, true);
|
||||
showBuildMessage('Build failed: ' + data.error, 'error');
|
||||
buildAllBtn.disabled = false;
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Variant Manager
|
||||
// ========================================
|
||||
|
||||
Reference in New Issue
Block a user