fixed overlap

This commit is contained in:
Finn Tews
2026-05-09 00:43:39 +02:00
parent 1eda1ea695
commit 74afe4dfce
3 changed files with 271 additions and 16 deletions

View File

@@ -176,6 +176,27 @@ function inferHorizontalPitch(positions) {
return Number.isFinite(pitch) ? pitch : 0;
}
function inferVerticalPitch(positions) {
const epsilon = 1e-3;
const cols = new Map();
for (const pos of positions) {
const key = pos[0].toFixed(4);
if (!cols.has(key)) cols.set(key, []);
cols.get(key).push(pos[1]);
}
let pitch = Infinity;
for (const colYs of cols.values()) {
colYs.sort((a, b) => a - b);
for (let index = 1; index < colYs.length; index++) {
const delta = colYs[index] - colYs[index - 1];
if (delta > epsilon) pitch = Math.min(pitch, delta);
}
}
return Number.isFinite(pitch) ? pitch : 0;
}
function computeBoundaryRoundoverFeatures(cellIndices, positions, padRadius) {
if (cellIndices.length < 2) {
return { extraPads: [], extraSegments: [] };
@@ -249,7 +270,11 @@ function computeBoundaryRoundoverFeatures(cellIndices, positions, padRadius) {
return { extraPads, extraSegments };
}
function computeEdgeOverlapFeatures(cellIndices, positions, layoutType, overlapLength) {
function computeEdgeOverlapFeatures(cellIndices, positions, layoutType, overlapLength, cellRadius, spacing) {
if (layoutType === 'vertical') {
return computeEdgeOverlapFeaturesVertical(cellIndices, positions, overlapLength, cellRadius, spacing);
}
if (cellIndices.length < 2) {
return { extraPads: [], extraSegments: [] };
}
@@ -374,6 +399,204 @@ function computeEdgeOverlapFeatures(cellIndices, positions, layoutType, overlapL
return { extraPads, extraSegments };
}
function computeBoundaryRoundoverFeaturesVertical(cellIndices, positions, padRadius) {
if (cellIndices.length < 2) {
return { extraPads: [], extraSegments: [] };
}
const selected = cellIndices
.map((index) => ({ index, pos: positions[index] }))
.filter((entry) => Array.isArray(entry.pos) && entry.pos.length >= 2);
if (selected.length !== cellIndices.length) {
return { extraPads: [], extraSegments: [] };
}
const epsilon = 1e-3;
const verticalPitch = inferVerticalPitch(positions);
if (verticalPitch <= epsilon) {
return { extraPads: [], extraSegments: [] };
}
const yTolerance = Math.max(0.5, verticalPitch * 0.25);
const xTolerance = 1e-3;
const hasVerticalNeighbor = (entry, direction) => positions.some((pos) => {
if (Math.abs(pos[0] - entry.pos[0]) > xTolerance) return false;
const deltaY = pos[1] - entry.pos[1];
if (direction === 'top' && deltaY >= -epsilon) return false;
if (direction === 'bottom' && deltaY <= epsilon) return false;
return Math.abs(Math.abs(deltaY) - verticalPitch) <= yTolerance;
});
const cols = new Map();
for (const entry of selected) {
const key = entry.pos[0].toFixed(4);
if (!cols.has(key)) cols.set(key, []);
cols.get(key).push(entry);
}
const topBoundary = [];
const bottomBoundary = [];
for (const colEntries of cols.values()) {
colEntries.sort((a, b) => a.pos[1] - b.pos[1]);
topBoundary.push(colEntries[0]);
bottomBoundary.push(colEntries[colEntries.length - 1]);
}
const topExposed = topBoundary.filter((entry) => !hasVerticalNeighbor(entry, 'top'));
const bottomExposed = bottomBoundary.filter((entry) => !hasVerticalNeighbor(entry, 'bottom'));
const extraPads = [];
const extraSegments = [];
const fillRadius = Math.max(0.2, padRadius * 0.55);
const outwardShift = Math.max(0.2, padRadius * 0.22);
const addSideRoundovers = (entries, side) => {
const direction = side === 'top' ? -1 : 1;
const ordered = entries.slice().sort((a, b) => a.pos[0] - b.pos[0]);
for (let index = 0; index < ordered.length - 1; index++) {
const left = ordered[index];
const right = ordered[index + 1];
const fillKey = `boundary_round_${side}_${index}`;
const fillPos = [
(left.pos[0] + right.pos[0]) / 2,
(left.pos[1] + right.pos[1]) / 2 + direction * outwardShift,
];
extraPads.push({ key: fillKey, pos: fillPos, radius: fillRadius });
extraSegments.push({ from: fillPos, to: left.pos, fromKey: fillKey, toKey: `c${left.index}`, radius: fillRadius });
extraSegments.push({ from: fillPos, to: right.pos, fromKey: fillKey, toKey: `c${right.index}`, radius: fillRadius });
}
};
addSideRoundovers(topExposed, 'top');
addSideRoundovers(bottomExposed, 'bottom');
return { extraPads, extraSegments };
}
function computeEdgeOverlapFeaturesVertical(cellIndices, positions, overlapLength, cellRadius, spacing) {
if (cellIndices.length < 2) {
return { extraPads: [], extraSegments: [] };
}
const selected = cellIndices
.map((index) => ({ index, pos: positions[index] }))
.filter((entry) => Array.isArray(entry.pos) && entry.pos.length >= 2);
if (selected.length !== cellIndices.length) {
return { extraPads: [], extraSegments: [] };
}
// Measure the X pitch between adjacent columns in the full layout.
const uniqueColXs = [...new Set(positions.map(p => Math.round(p[0] * 10)))]
.sort((a, b) => a - b);
let colPitch = Infinity;
for (let i = 1; i < uniqueColXs.length; i++) {
const d = (uniqueColXs[i] - uniqueColXs[i - 1]) / 10;
if (d > 1e-3) colPitch = Math.min(colPitch, d);
}
if (!Number.isFinite(colPitch) || colPitch <= 1e-3) {
return { extraPads: [], extraSegments: [] };
}
const colTolerance = colPitch * 0.3;
// Group selected cells by column (keyed by rounded X*10).
const colMap = new Map();
for (const entry of selected) {
const key = Math.round(entry.pos[0] * 10);
if (!colMap.has(key)) colMap.set(key, []);
colMap.get(key).push(entry);
}
const sortedColKeys = [...colMap.keys()].sort((a, b) => a - b);
const leftColX = sortedColKeys[0] / 10;
const rightColX = sortedColKeys[sortedColKeys.length - 1] / 10;
// Count how many boundary-column cells have no full-layout column neighbor.
const leftColCells = colMap.get(sortedColKeys[0]);
const rightColCells = colMap.get(sortedColKeys[sortedColKeys.length - 1]);
const countExposed = (colCells, side) => colCells.filter((entry) => {
const neighborX = side === 'left' ? leftColX - colPitch : rightColX + colPitch;
return !positions.some((p) => Math.abs(p[0] - neighborX) < colTolerance);
}).length;
const leftExposedCount = countExposed(leftColCells, 'left');
const rightExposedCount = countExposed(rightColCells, 'right');
// Skip if the busbar is only a single column — a single-column spine that
// runs straight through all pads adds no useful material above what the
// pad-and-edge geometry already provides.
// (We still proceed when there are multiple columns.)
let chosenSide, boundaryCells, boundaryColX;
if (leftExposedCount > rightExposedCount) {
chosenSide = 'left';
boundaryCells = leftColCells;
boundaryColX = leftColX;
} else if (rightExposedCount > leftExposedCount) {
chosenSide = 'right';
boundaryCells = rightColCells;
boundaryColX = rightColX;
} else if (leftExposedCount > 0) {
// Tie: prefer left.
chosenSide = 'left';
boundaryCells = leftColCells;
boundaryColX = leftColX;
} else {
return { extraPads: [], extraSegments: [] };
}
const extension = Number.isFinite(Number(overlapLength)) && Number(overlapLength) > 0
? Number(overlapLength)
: 10;
// Place the spine relative to the pack boundary (not the cell centre) so the
// overlap tab is clearly visible outside the pack rectangle.
const packLeft = Math.min(...positions.map(p => p[0])) - cellRadius - spacing;
const packRight = Math.max(...positions.map(p => p[0])) + cellRadius + spacing;
const spineX = chosenSide === 'left'
? packLeft - extension
: packRight + extension;
const extraPads = [];
const extraSegments = [];
const sortedBoundaryCells = boundaryCells.slice().sort((a, b) => a.pos[1] - b.pos[1]);
const overlapPads = sortedBoundaryCells.map((entry, index) => {
const key = `edge_overlap_${index}`;
const overlapPos = [spineX, entry.pos[1]];
extraPads.push({ key, pos: overlapPos });
extraSegments.push({ from: overlapPos, to: entry.pos, fromKey: key, toKey: `c${entry.index}` });
return { key, pos: overlapPos };
});
// Connect adjacent spine pads with a vertical segment and add fill pads.
for (let index = 0; index < overlapPads.length - 1; index++) {
const topPad = overlapPads[index];
const bottomPad = overlapPads[index + 1];
const topCell = sortedBoundaryCells[index];
const bottomCell = sortedBoundaryCells[index + 1];
extraSegments.push({
from: topPad.pos,
to: bottomPad.pos,
fromKey: topPad.key,
toKey: bottomPad.key,
});
const fillKey = `edge_overlap_fill_${index}`;
const fillPos = [
(topPad.pos[0] + bottomPad.pos[0] + topCell.pos[0] + bottomCell.pos[0]) / 4,
(topPad.pos[1] + bottomPad.pos[1] + topCell.pos[1] + bottomCell.pos[1]) / 4,
];
extraPads.push({ key: fillKey, pos: fillPos });
extraSegments.push({ from: fillPos, to: topPad.pos, fromKey: fillKey, toKey: topPad.key });
extraSegments.push({ from: fillPos, to: bottomPad.pos, fromKey: fillKey, toKey: bottomPad.key });
extraSegments.push({ from: fillPos, to: topCell.pos, fromKey: fillKey, toKey: `c${topCell.index}` });
extraSegments.push({ from: fillPos, to: bottomCell.pos, fromKey: fillKey, toKey: `c${bottomCell.index}` });
}
return { extraPads, extraSegments };
}
function computeCellCutouts(cellIndices, positions, cellCutoutEnabled) {
if (cellCutoutEnabled !== true) return [];
@@ -439,9 +662,11 @@ export function computeBusbarGeometry(cellIndices, positions, cellRadius, padRad
});
const overlapFeatures = overlapEnabled
? computeEdgeOverlapFeatures(cellIndices, positions, layoutType, overlapSize)
? computeEdgeOverlapFeatures(cellIndices, positions, layoutType, overlapSize, cellRadius, spacing)
: { extraPads: [], extraSegments: [] };
const roundoverFeatures = computeBoundaryRoundoverFeatures(cellIndices, positions, padRadius);
const roundoverFeatures = layoutType === 'vertical'
? computeBoundaryRoundoverFeaturesVertical(cellIndices, positions, padRadius)
: computeBoundaryRoundoverFeatures(cellIndices, positions, padRadius);
const cutouts = computeCellCutouts(cellIndices, positions, cellCutoutEnabled);
return {

View File

@@ -19,7 +19,6 @@ function scaleCanvasById(id) {
const canvas = document.getElementById(id);
if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
// Let CSS (width:100%, height:720px) control the display size.
// Only update the drawing buffer to match the current CSS-rendered size × DPR.
const rect = canvas.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
@@ -672,6 +671,22 @@ async function initializeApp() {
await initOC();
// Keep canvas drawing buffers in sync whenever their CSS size changes
// (e.g. window resize, flex layout settling, face-filter toggles).
const canvasResizeObserver = new ResizeObserver(() => {
scaleCanvasForDPI();
updatePreview(false);
});
const previewCanvas = document.getElementById('preview');
const previewBottomCanvas = document.getElementById('preview-bottom');
if (previewCanvas) canvasResizeObserver.observe(previewCanvas);
if (previewBottomCanvas) canvasResizeObserver.observe(previewBottomCanvas);
window.addEventListener('resize', () => {
scaleCanvasForDPI();
updatePreview(false);
});
setTimeout(() => {
// Force a fresh render after both OC init and config load are complete.
// This ensures viewTransform and geometries are in sync with loaded busbars.

View File

@@ -10,26 +10,29 @@ body {
min-height: 100vh;
color: #e2e8f0;
line-height: 1.6;
padding: 48px 24px;
padding: 12px 24px;
}
.container {
max-width: 1600px;
margin: 0 auto;
height: 100%;
display: flex;
flex-direction: column;
transition: max-width 0.3s ease, padding 0.3s ease;
}
/* Header */
.header-row {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 10px;
gap: 12px;
margin-bottom: 4px;
}
h1 {
font-size: 2.5em;
font-size: 1.6em;
font-weight: 700;
color: #fff;
margin-bottom: 0;
@@ -45,8 +48,8 @@ h1:hover {
.subtitle {
color: #94a3b8;
font-size: 1em;
margin-bottom: 32px;
font-size: 0.88em;
margin-bottom: 6px;
transition: color 0.3s ease;
animation: fadeInUp 0.6s ease-out 0.1s both;
}
@@ -100,8 +103,8 @@ h1:hover {
align-items: center;
justify-content: center;
gap: 10px;
margin: 0 auto 18px;
padding: 10px 18px;
margin: 0 auto 8px;
padding: 6px 14px;
max-width: 700px;
background: rgba(234, 179, 8, 0.1);
border: 1px solid rgba(234, 179, 8, 0.45);
@@ -163,6 +166,9 @@ h1:hover {
display: grid;
grid-template-columns: 460px 1fr;
gap: 28px;
flex: 1;
min-height: 0;
align-items: stretch;
transition: grid-template-columns 0.4s cubic-bezier(0.4, 0, 0.2, 1), gap 0.3s ease;
}
@@ -191,6 +197,8 @@ h1:hover {
border: 1px solid rgba(100, 149, 237, 0.1);
border-radius: 14px;
padding: 20px 20px 24px;
overflow-y: auto;
min-height: 0;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
@@ -400,6 +408,9 @@ h1:hover {
.preview-container {
padding: 28px;
display: flex;
flex-direction: column;
min-height: 0;
}
.section:hover {
@@ -763,12 +774,15 @@ input[type="checkbox"]:focus {
.previews-row {
display: flex;
gap: 12px;
align-items: flex-start;
flex: 1;
min-height: 0;
align-items: stretch;
}
.preview-face-wrap {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 6px;
@@ -791,13 +805,14 @@ input[type="checkbox"]:focus {
#preview,
#preview-bottom {
width: 100%;
height: 720px;
height: min(calc(100vh - 280px), 580px);
min-height: 380px;
border: 1px solid rgba(100, 149, 237, 0.2);
border-radius: 10px;
background: #1e293b;
display: block;
cursor: grab;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#preview:hover,