Several quality improvements including top and bottom bus bar, seperate downloads for bus bars and a sharable configuration link

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Finn Tews
2026-04-27 23:04:13 +02:00
parent 0cd23b8198
commit e033ce35fa
375 changed files with 250919 additions and 293 deletions

View File

@@ -7,9 +7,9 @@ import {
generateVerticalHoneycombLayout,
getCachedPositions,
} from './layouts.js';
import { drawPreview, clearCanvas } from './preview.js';
import { drawPreview, clearCanvas, drawPreviewCopy, drawPreviewMirroredCopy } from './preview.js';
import { create3DModel } from './model.js';
import { downloadSTEP } from './step-export.js';
import { downloadSTEP, buildSTEPBytes } from './step-export.js';
import { buildBusbarDXF, downloadDXF } from './dxf-export.js';
import { busbarStore } from './busbars.js';
import { computeBusbarGeometry } from './busbar-geometry.js';
@@ -23,10 +23,33 @@ export function getLastBusbarGeometries() {
return lastComputedGeometries;
}
function drawBothCanvases(positions, cellSize, padRadius, spacing) {
drawPreview(positions, cellSize);
const indexed = busbarStore.list.map((bb, i) => ({ bb, geom: lastComputedGeometries[i] }));
const topPairs = indexed.filter(p => (p.bb.face || 'top') === 'top');
const bottomPairs = indexed.filter(p => (p.bb.face || 'top') === 'bottom');
// Copy the clean pack layout BEFORE any busbar overlay is painted.
drawPreviewMirroredCopy('preview-bottom');
// Now draw each face's busbars on its own canvas only.
drawBusbarsOverlay(
topPairs.map(p => p.bb), topPairs.map(p => p.geom),
positions, cellSize, padRadius, spacing, busbarStore.activeId, 'preview'
);
drawBusbarsOverlay(
bottomPairs.map(p => p.bb), bottomPairs.map(p => p.geom),
positions, cellSize, padRadius, spacing, busbarStore.activeId, 'preview-bottom',
false, true
);
}
export function redrawBusbarOverlay() {
if (!lastBusbarDrawArgs) return;
const { positions, cellSize, padRadius, spacing } = lastBusbarDrawArgs;
drawBusbarsOverlay(busbarStore.list, lastComputedGeometries, positions, cellSize, padRadius, spacing, busbarStore.activeId);
drawBothCanvases(positions, cellSize, padRadius, spacing);
}
export function updatePreview(resetView = false) {
@@ -176,8 +199,6 @@ export function updatePreview(resetView = false) {
stats.style.color = '#10b981';
}
drawPreview(positions, cellSize);
const cellRadius = cellSize / 2;
const busbarPadRadius = Math.max(cellRadius - ledgeWidth, 1.0);
const busbarKeepoutRadius = 4.0;
@@ -185,7 +206,7 @@ export function updatePreview(resetView = false) {
computeBusbarGeometry(bb.cellIndices, positions, cellRadius, busbarPadRadius, spacing, busbarKeepoutRadius)
);
lastBusbarDrawArgs = { positions, cellSize, padRadius: busbarPadRadius, spacing };
drawBusbarsOverlay(busbarStore.list, lastComputedGeometries, positions, cellSize, busbarPadRadius, spacing, busbarStore.activeId);
drawBothCanvases(positions, cellSize, busbarPadRadius, spacing);
const blockedByBusbarId = {};
busbarStore.list.forEach((bb, i) => {
@@ -546,3 +567,110 @@ export async function generateLayout() {
showLoading(false);
}
}
// ── Per-busbar download helpers ────────────────────────────────────────────────
function getBusbarExportContext() {
if (!lastBusbarDrawArgs || lastComputedGeometries.length === 0) return null;
const { positions, padRadius } = lastBusbarDrawArgs;
const cx = (Math.min(...positions.map(p => p[0])) + Math.max(...positions.map(p => p[0]))) / 2;
const cy = (Math.min(...positions.map(p => p[1])) + Math.max(...positions.map(p => p[1]))) / 2;
const centeredPositions = positions.map(([x, y]) => [x - cx, y - cy]);
const height = parseFloat(document.getElementById('height').value);
const busbarFormat = document.getElementById('busbarFormat')?.value || 'step';
const safeName = (name) => (name || '').replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'busbar';
return { centeredPositions, padRadius, height, busbarFormat, safeName };
}
export async function downloadSingleBusbar(busbarId) {
const ctx = getBusbarExportContext();
if (!ctx) {
showStatus('Configure the layout first to enable busbar downloads.', 'error');
return;
}
const bbIdx = busbarStore.list.findIndex(b => b.id === busbarId);
if (bbIdx < 0) return;
const bb = busbarStore.list[bbIdx];
if (bb.cellIndices.length === 0) {
showStatus(`${bb.name} has no cells assigned.`, 'error');
return;
}
const geom = lastComputedGeometries[bbIdx];
if (!geom || geom.blocked) {
showStatus(`${bb.name}: ${geom?.blocked?.reason ?? 'geometry unavailable'}`, 'error');
return;
}
if (ctx.busbarFormat === 'step' && !ocRef.initialized) {
showStatus('3D engine not ready. Please wait.', 'error');
return;
}
const base = `busbar_${ctx.safeName(bb.name)}`;
showLoading(true, `Exporting ${bb.name}`, '');
await new Promise(r => setTimeout(r, 20));
try {
if (ctx.busbarFormat === 'dxf') {
const content = buildBusbarDXF(geom, ctx.centeredPositions, ctx.padRadius);
downloadDXF(content, `${base}.dxf`);
} else {
const shape = build3DBusbar(geom, ctx.centeredPositions, ctx.padRadius, ctx.height, bb.thickness);
if (!shape) { showStatus(`Failed to build 3D shape for ${bb.name}.`, 'error'); return; }
downloadSTEP(shape, `${base}.step`);
}
} catch (e) {
showStatus(`Export error: ${e.message}`, 'error');
} finally {
showLoading(false);
}
}
export async function downloadAllBusbarsZip() {
const ctx = getBusbarExportContext();
if (!ctx) {
showStatus('Configure the layout first to enable busbar downloads.', 'error');
return;
}
const eligible = busbarStore.list
.map((bb, i) => ({ bb, geom: lastComputedGeometries[i], i }))
.filter(({ bb, geom }) => bb.cellIndices.length > 0 && geom && !geom.blocked);
if (eligible.length === 0) {
showStatus('No busbars with cells to export.', 'error');
return;
}
if (ctx.busbarFormat === 'step' && !ocRef.initialized) {
showStatus('3D engine not ready. Please wait.', 'error');
return;
}
showLoading(true, 'Building busbar ZIP', 'Please wait...');
await new Promise(r => setTimeout(r, 50));
try {
const { default: JSZip } = await import('jszip');
const zip = new JSZip();
for (const { bb, geom } of eligible) {
const base = `busbar_${ctx.safeName(bb.name)}`;
if (ctx.busbarFormat === 'dxf') {
const content = buildBusbarDXF(geom, ctx.centeredPositions, ctx.padRadius);
zip.file(`${base}.dxf`, content);
} else {
const shape = build3DBusbar(geom, ctx.centeredPositions, ctx.padRadius, ctx.height, bb.thickness);
if (!shape) continue;
const bytes = buildSTEPBytes(shape, `_zip_${base}.step`);
if (bytes) zip.file(`${base}.step`, bytes);
}
}
const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'busbars.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showStatus(`Downloaded busbars.zip (${eligible.length} file${eligible.length === 1 ? '' : 's'}).`, 'success');
} catch (e) {
console.error('ZIP export error:', e);
showStatus('ZIP export error: ' + e.message, 'error');
} finally {
showLoading(false);
}
}