fixed overlap
This commit is contained in:
@@ -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 {
|
||||
|
||||
17
src/main.js
17
src/main.js
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user