This commit is contained in:
@@ -197,6 +197,11 @@ button, input, select {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-card__preview {
|
.component-card__preview {
|
||||||
@@ -400,12 +405,19 @@ button, input, select {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
opacity: 0.32;
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-insert-zone.is-disabled {
|
.rack-insert-zone.is-disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rack-insert-zone.is-preview,
|
||||||
|
.rack-insert-zone:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.rack-insert-zone__ear {
|
.rack-insert-zone__ear {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: calc(100% - 4px);
|
height: calc(100% - 4px);
|
||||||
@@ -482,11 +494,11 @@ button, input, select {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rgba(34, 38, 44, 0.55);
|
border: 1px solid rgba(32, 36, 42, 0.8);
|
||||||
background: linear-gradient(180deg, rgba(237, 241, 246, 0.96), rgba(205, 213, 222, 0.96));
|
background: linear-gradient(180deg, rgba(235, 239, 244, 0.98), rgba(191, 200, 211, 0.98));
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
inset 0 1px 0 rgba(255, 255, 255, 0.94),
|
||||||
0 10px 18px rgba(5, 6, 7, 0.28);
|
0 8px 14px rgba(5, 6, 7, 0.24);
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
@@ -509,8 +521,10 @@ button, input, select {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 10px 8px 12px;
|
padding: 6px 10px 6px 10px;
|
||||||
background: linear-gradient(90deg, rgba(250, 251, 252, 0.86), rgba(250, 251, 252, 0.08) 38%, rgba(250, 251, 252, 0.72));
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 42%),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0.06) 34%, rgba(255, 255, 255, 0.18));
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-item__face {
|
.rack-item__face {
|
||||||
@@ -527,6 +541,7 @@ button, input, select {
|
|||||||
|
|
||||||
.rack-item__header strong {
|
.rack-item__header strong {
|
||||||
color: #17191c;
|
color: #17191c;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-item__drag-hint {
|
.rack-item__drag-hint {
|
||||||
@@ -537,6 +552,7 @@ button, input, select {
|
|||||||
color: rgba(245, 247, 250, 0.95);
|
color: rgba(245, 247, 250, 0.95);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-item__remove {
|
.rack-item__remove {
|
||||||
@@ -557,6 +573,10 @@ button, input, select {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rack-item__remove:hover {
|
||||||
|
background: rgba(134, 24, 24, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
.subpanel {
|
.subpanel {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -646,7 +666,7 @@ button, input, select {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 8px 28px;
|
padding: 6px 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +719,7 @@ button, input, select {
|
|||||||
|
|
||||||
.device-brand {
|
.device-brand {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 7px;
|
top: 6px;
|
||||||
left: 36px;
|
left: 36px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -710,7 +730,7 @@ button, input, select {
|
|||||||
.device-silkscreen {
|
.device-silkscreen {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 36px;
|
left: 36px;
|
||||||
bottom: 8px;
|
bottom: 6px;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -765,7 +785,8 @@ button, input, select {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
right: 70px;
|
right: 70px;
|
||||||
top: 22px;
|
top: 48%;
|
||||||
|
transform: translateY(-50%);
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,7 +813,8 @@ button, input, select {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
top: 20px;
|
top: 48%;
|
||||||
|
transform: translateY(-50%);
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,10 +829,11 @@ button, input, select {
|
|||||||
|
|
||||||
.device-uplink {
|
.device-uplink {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 50%;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
transform: translateY(-50%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(110, 117, 127, 0.25);
|
border: 1px solid rgba(110, 117, 127, 0.25);
|
||||||
background: linear-gradient(180deg, #5b646f, #262b31);
|
background: linear-gradient(180deg, #5b646f, #262b31);
|
||||||
@@ -818,9 +841,10 @@ button, input, select {
|
|||||||
|
|
||||||
.device-sockets {
|
.device-sockets {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 50%;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
right: 56px;
|
right: 56px;
|
||||||
|
transform: translateY(-50%);
|
||||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -838,9 +862,10 @@ button, input, select {
|
|||||||
.device-display {
|
.device-display {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
top: 16px;
|
top: 50%;
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
transform: translateY(-50%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(46, 180, 156, 0.9), rgba(18, 97, 90, 0.92)),
|
linear-gradient(180deg, rgba(46, 180, 156, 0.9), rgba(18, 97, 90, 0.92)),
|
||||||
@@ -852,10 +877,11 @@ button, input, select {
|
|||||||
|
|
||||||
.device-vents {
|
.device-vents {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 50%;
|
||||||
right: 42px;
|
right: 42px;
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
transform: translateY(-50%);
|
||||||
background:
|
background:
|
||||||
repeating-linear-gradient(90deg, rgba(102, 108, 118, 0.36) 0 3px, transparent 3px 7px);
|
repeating-linear-gradient(90deg, rgba(102, 108, 118, 0.36) 0 3px, transparent 3px 7px);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@@ -879,8 +905,9 @@ button, input, select {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
top: 18px;
|
top: 50%;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
transform: translateY(-50%);
|
||||||
border-radius: 4px 4px 12px 12px;
|
border-radius: 4px 4px 12px 12px;
|
||||||
background: linear-gradient(180deg, #d1d7df, #8f98a4);
|
background: linear-gradient(180deg, #d1d7df, #8f98a4);
|
||||||
box-shadow: 0 5px 12px rgba(78, 82, 90, 0.18);
|
box-shadow: 0 5px 12px rgba(78, 82, 90, 0.18);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const state = {
|
|||||||
dragPlacementId: null,
|
dragPlacementId: null,
|
||||||
pointerDrag: null,
|
pointerDrag: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
|
libraryDragComponentId: null,
|
||||||
|
rackDropPreviewY: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ui = {};
|
const ui = {};
|
||||||
@@ -64,6 +66,7 @@ function bindEvents() {
|
|||||||
ui.exportBom.addEventListener("click", copyBomCsv);
|
ui.exportBom.addEventListener("click", copyBomCsv);
|
||||||
document.addEventListener("pointermove", handlePointerMove);
|
document.addEventListener("pointermove", handlePointerMove);
|
||||||
document.addEventListener("pointerup", handlePointerUp);
|
document.addEventListener("pointerup", handlePointerUp);
|
||||||
|
document.addEventListener("dragend", handleLibraryDragEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBootstrap() {
|
async function loadBootstrap() {
|
||||||
@@ -152,7 +155,7 @@ function renderLibrary() {
|
|||||||
ui.componentLibrary.innerHTML = filtered
|
ui.componentLibrary.innerHTML = filtered
|
||||||
.map(
|
.map(
|
||||||
(component) => `
|
(component) => `
|
||||||
<article class="component-card component-card--${escapeHtml(component.category)}">
|
<article class="component-card component-card--${escapeHtml(component.category)}" draggable="true" data-component-drag-id="${escapeHtml(component.id)}">
|
||||||
<div class="component-card__preview">
|
<div class="component-card__preview">
|
||||||
${renderComponentFace(component, "library")}
|
${renderComponentFace(component, "library")}
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +172,7 @@ function renderLibrary() {
|
|||||||
<span>${formatCurrency(component.priceNet, component.currency)}</span>
|
<span>${formatCurrency(component.priceNet, component.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-action="select-component" data-component-id="${escapeHtml(component.id)}">
|
<button type="button" data-action="select-component" data-component-id="${escapeHtml(component.id)}">
|
||||||
${state.selectedComponentId === component.id ? "Ausgewaehlt" : "Zum Einstecken waehlen"}
|
${state.selectedComponentId === component.id ? "Bereit zum Ziehen" : "Ins Rack ziehen"}
|
||||||
</button>
|
</button>
|
||||||
</article>
|
</article>
|
||||||
`
|
`
|
||||||
@@ -182,40 +185,21 @@ function renderLibrary() {
|
|||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function addComponentToRack(componentId) {
|
ui.componentLibrary.querySelectorAll("[data-component-drag-id]").forEach((card) => {
|
||||||
const component = state.components.find((entry) => entry.id === componentId);
|
card.addEventListener("dragstart", (event) => beginLibraryDrag(event, card.dataset.componentDragId));
|
||||||
const rack = getCurrentRackTemplate();
|
|
||||||
if (!component || !rack) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = findFirstFreePosition(component.heightU);
|
|
||||||
if (position === null) {
|
|
||||||
alert("Keine freie Position fuer diese Komponente im aktuellen Rack.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.placedItems.push({
|
|
||||||
placementId: `p${state.nextPlacementId++}`,
|
|
||||||
componentId: component.id,
|
|
||||||
y: position,
|
|
||||||
x: getDefaultComponentX(component),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
renderAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectionInfo() {
|
function renderSelectionInfo() {
|
||||||
const component = getComponent(state.selectedComponentId);
|
const component = getComponent(state.libraryDragComponentId || state.selectedComponentId);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
ui.selectedComponentInfo.textContent = "Kein Element ausgewaehlt";
|
ui.selectedComponentInfo.textContent = "Kein Element ausgewaehlt";
|
||||||
ui.selectedComponentInfo.classList.remove("is-active");
|
ui.selectedComponentInfo.classList.remove("is-active");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · freie HE-Zone anklicken`;
|
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · aus der Bibliothek ins Rack ziehen`;
|
||||||
ui.selectedComponentInfo.classList.add("is-active");
|
ui.selectedComponentInfo.classList.add("is-active");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +235,10 @@ function renderRack() {
|
|||||||
|
|
||||||
const layer = document.getElementById("rack-items-layer");
|
const layer = document.getElementById("rack-items-layer");
|
||||||
layer.style.height = "100%";
|
layer.style.height = "100%";
|
||||||
|
const bay = ui.rackGrid.querySelector(".rack-grid__bay");
|
||||||
|
bay.addEventListener("dragover", handleRackDragOver);
|
||||||
|
bay.addEventListener("drop", handleRackDrop);
|
||||||
|
bay.addEventListener("dragleave", handleRackDragLeave);
|
||||||
|
|
||||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||||
@@ -282,7 +270,7 @@ function renderRack() {
|
|||||||
<strong>${escapeHtml(component.name)}</strong>
|
<strong>${escapeHtml(component.name)}</strong>
|
||||||
<div class="rack-item__meta">${component.heightU}U · ${component.depthMm} mm · ${formatCurrency(component.priceNet, component.currency)}</div>
|
<div class="rack-item__meta">${component.heightU}U · ${component.depthMm} mm · ${formatCurrency(component.priceNet, component.currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="chip">${formatRackPosition(item.y)}</span>
|
<span class="chip">${formatRackPosition(item.y, component)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rack-item__drag-hint">Ziehen zum verschieben</div>
|
<div class="rack-item__drag-hint">Ziehen zum verschieben</div>
|
||||||
<button type="button" class="rack-item__remove" data-action="remove" aria-label="Komponente entfernen">×</button>
|
<button type="button" class="rack-item__remove" data-action="remove" aria-label="Komponente entfernen">×</button>
|
||||||
@@ -300,19 +288,20 @@ function renderRack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function insertSelectedComponentAt(y) {
|
function insertSelectedComponentAt(y) {
|
||||||
if (!state.selectedComponentId) {
|
const componentId = state.libraryDragComponentId || state.selectedComponentId;
|
||||||
|
if (!componentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = getComponent(state.selectedComponentId);
|
const component = getComponent(componentId);
|
||||||
const rack = getCurrentRackTemplate();
|
const rack = getCurrentRackTemplate();
|
||||||
if (!component || !rack) {
|
if (!component || !rack) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextRect = {
|
const nextRect = {
|
||||||
left: getDefaultComponentX(component),
|
left: 0,
|
||||||
top: y,
|
top: snapToUnit(y),
|
||||||
width: getComponentWidthPercent(component),
|
width: getComponentWidthPercent(component),
|
||||||
height: getComponentHeightPx(component),
|
height: getComponentHeightPx(component),
|
||||||
};
|
};
|
||||||
@@ -340,9 +329,11 @@ function insertSelectedComponentAt(y) {
|
|||||||
state.placedItems.push({
|
state.placedItems.push({
|
||||||
placementId: `p${state.nextPlacementId++}`,
|
placementId: `p${state.nextPlacementId++}`,
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
y,
|
y: snapToUnit(y),
|
||||||
x: getDefaultComponentX(component),
|
x: 0,
|
||||||
});
|
});
|
||||||
|
state.libraryDragComponentId = null;
|
||||||
|
state.rackDropPreviewY = null;
|
||||||
state.selectedComponentId = null;
|
state.selectedComponentId = null;
|
||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
@@ -442,7 +433,7 @@ function renderCableSelectors() {
|
|||||||
if (!component) {
|
if (!component) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y)}</option>`;
|
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y, component)}</option>`;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
@@ -628,43 +619,6 @@ async function copyBomCsv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFirstFreePosition(heightU) {
|
|
||||||
const rack = getCurrentRackTemplate();
|
|
||||||
if (!rack) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unitHeight = getUnitHeightPx();
|
|
||||||
const testHeight = heightU * unitHeight;
|
|
||||||
for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += unitHeight) {
|
|
||||||
const blocked = state.placedItems.some((item) => {
|
|
||||||
const otherComponent = getComponent(item.componentId);
|
|
||||||
if (!otherComponent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return rectanglesOverlap(
|
|
||||||
{
|
|
||||||
left: getDefaultComponentX(component),
|
|
||||||
top: y,
|
|
||||||
width: getComponentWidthPercent(component),
|
|
||||||
height: testHeight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
left: item.x,
|
|
||||||
top: item.y,
|
|
||||||
width: getComponentWidthPercent(otherComponent),
|
|
||||||
height: getComponentHeightPx(otherComponent),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (!blocked) {
|
|
||||||
return y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentRackTemplate() {
|
function getCurrentRackTemplate() {
|
||||||
return state.bootstrap?.rackTemplates.find((template) => template.id === state.rackTemplateId) ?? null;
|
return state.bootstrap?.rackTemplates.find((template) => template.id === state.rackTemplateId) ?? null;
|
||||||
}
|
}
|
||||||
@@ -701,9 +655,7 @@ function beginPointerDrag(event, item, component) {
|
|||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
state.pointerDrag = {
|
state.pointerDrag = {
|
||||||
placementId: item.placementId,
|
placementId: item.placementId,
|
||||||
offsetX: event.clientX - rect.left,
|
|
||||||
offsetY: event.clientY - rect.top,
|
offsetY: event.clientY - rect.top,
|
||||||
widthPercent: getComponentWidthPercent(component),
|
|
||||||
heightPx: getComponentHeightPx(component),
|
heightPx: getComponentHeightPx(component),
|
||||||
};
|
};
|
||||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||||
@@ -727,12 +679,10 @@ function handlePointerMove(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const layerRect = layer.getBoundingClientRect();
|
const layerRect = layer.getBoundingClientRect();
|
||||||
const widthPx = (state.pointerDrag.widthPercent / 100) * layerRect.width;
|
|
||||||
const xPx = clamp(event.clientX - layerRect.left - state.pointerDrag.offsetX, 0, layerRect.width - widthPx);
|
|
||||||
const yPx = clamp(event.clientY - layerRect.top - state.pointerDrag.offsetY, 0, getRackHeightPx(rack) - state.pointerDrag.heightPx);
|
const yPx = clamp(event.clientY - layerRect.top - state.pointerDrag.offsetY, 0, getRackHeightPx(rack) - state.pointerDrag.heightPx);
|
||||||
|
|
||||||
item.x = (xPx / layerRect.width) * 100;
|
item.x = 0;
|
||||||
item.y = yPx;
|
item.y = snapToUnit(yPx);
|
||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,7 +807,11 @@ function renderSocketStrip(component) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderInsertionZones(rack) {
|
function renderInsertionZones(rack) {
|
||||||
const component = getComponent(state.selectedComponentId);
|
if (!state.libraryDragComponentId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = getComponent(state.libraryDragComponentId);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -872,27 +826,28 @@ function renderInsertionZones(rack) {
|
|||||||
}
|
}
|
||||||
return rectanglesOverlap(
|
return rectanglesOverlap(
|
||||||
{
|
{
|
||||||
left: getDefaultComponentX(component),
|
left: 0,
|
||||||
top: y,
|
top: y,
|
||||||
width: getComponentWidthPercent(component),
|
width: 100,
|
||||||
height: getComponentHeightPx(component),
|
height: getComponentHeightPx(component),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: item.x,
|
left: 0,
|
||||||
top: item.y,
|
top: item.y,
|
||||||
width: getComponentWidthPercent(other),
|
width: 100,
|
||||||
height: getComponentHeightPx(other),
|
height: getComponentHeightPx(other),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const isPreview = state.rackDropPreviewY !== null && Math.abs(state.rackDropPreviewY - y) < 1;
|
||||||
|
|
||||||
zones.push(`
|
zones.push(`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rack-insert-zone${occupied ? " is-disabled" : ""}"
|
class="rack-insert-zone${occupied ? " is-disabled" : ""}${isPreview ? " is-preview" : ""}"
|
||||||
data-action="insert-component"
|
data-action="insert-component"
|
||||||
data-insert-y="${y}"
|
data-insert-y="${y}"
|
||||||
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:${getDefaultComponentX(component)}%;width:${getComponentWidthPercent(component)}%;"
|
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:0;width:100%;"
|
||||||
${occupied ? "disabled" : ""}
|
${occupied ? "disabled" : ""}
|
||||||
>
|
>
|
||||||
<span class="rack-insert-zone__ear rack-insert-zone__ear--left"></span>
|
<span class="rack-insert-zone__ear rack-insert-zone__ear--left"></span>
|
||||||
@@ -921,11 +876,11 @@ function getComponentHeightPx(component) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getComponentWidthPercent(component) {
|
function getComponentWidthPercent(component) {
|
||||||
return component.rackStandard === "10_inch" ? 58 : 86;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultComponentX(component) {
|
function getDefaultComponentX(component) {
|
||||||
return (100 - getComponentWidthPercent(component)) / 2;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlacementRect(item, component) {
|
function getPlacementRect(item, component) {
|
||||||
@@ -948,6 +903,17 @@ function formatRackPosition(y) {
|
|||||||
return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`;
|
return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRackPosition(y, component) {
|
||||||
|
const rack = getCurrentRackTemplate();
|
||||||
|
if (!rack) {
|
||||||
|
return "1U";
|
||||||
|
}
|
||||||
|
const rowIndex = Math.round(y / getUnitHeightPx());
|
||||||
|
const componentHeightU = component?.heightU ?? 1;
|
||||||
|
const u = rack.totalU - rowIndex - componentHeightU + 1;
|
||||||
|
return `${Math.max(1, u)}U`;
|
||||||
|
}
|
||||||
|
|
||||||
function getPlacementOverlaps() {
|
function getPlacementOverlaps() {
|
||||||
const overlaps = [];
|
const overlaps = [];
|
||||||
for (let index = 0; index < state.placedItems.length; index += 1) {
|
for (let index = 0; index < state.placedItems.length; index += 1) {
|
||||||
@@ -992,6 +958,86 @@ function clamp(value, min, max) {
|
|||||||
return Math.max(min, Math.min(max, value));
|
return Math.max(min, Math.min(max, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapToUnit(value) {
|
||||||
|
const unitHeight = getUnitHeightPx();
|
||||||
|
return Math.round(value / unitHeight) * unitHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginLibraryDrag(event, componentId) {
|
||||||
|
state.libraryDragComponentId = componentId;
|
||||||
|
state.selectedComponentId = componentId;
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = "copy";
|
||||||
|
event.dataTransfer.setData("text/plain", componentId);
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
renderAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRackDragOver(event) {
|
||||||
|
if (!state.libraryDragComponentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const rack = getCurrentRackTemplate();
|
||||||
|
const component = getComponent(state.libraryDragComponentId);
|
||||||
|
const layer = document.getElementById("rack-items-layer");
|
||||||
|
if (!rack || !component || !layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = layer.getBoundingClientRect();
|
||||||
|
const y = clamp(event.clientY - rect.top - getComponentHeightPx(component) / 2, 0, getRackHeightPx(rack) - getComponentHeightPx(component));
|
||||||
|
const snappedY = snapToUnit(y);
|
||||||
|
if (state.rackDropPreviewY !== snappedY) {
|
||||||
|
state.rackDropPreviewY = snappedY;
|
||||||
|
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||||
|
if (insertionLayer) {
|
||||||
|
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||||
|
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||||
|
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRackDrop(event) {
|
||||||
|
if (!state.libraryDragComponentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.rackDropPreviewY !== null) {
|
||||||
|
insertSelectedComponentAt(state.rackDropPreviewY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRackDragLeave(event) {
|
||||||
|
if (!state.libraryDragComponentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.rackDropPreviewY = null;
|
||||||
|
const rack = getCurrentRackTemplate();
|
||||||
|
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||||
|
if (rack && insertionLayer) {
|
||||||
|
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||||
|
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||||
|
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLibraryDragEnd() {
|
||||||
|
if (!state.libraryDragComponentId && state.rackDropPreviewY === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.libraryDragComponentId = null;
|
||||||
|
state.rackDropPreviewY = null;
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
function applyRackColorTheme(element, color) {
|
function applyRackColorTheme(element, color) {
|
||||||
const rgb = hexToRgb(color);
|
const rgb = hexToRgb(color);
|
||||||
if (!rgb) {
|
if (!rgb) {
|
||||||
|
|||||||
Reference in New Issue
Block a user