+ ${header}
${label}
${leds}
${ports}
@@ -661,6 +799,9 @@ function renderComponentFace(component, mode) {
case "patch_panel":
return `
@@ -668,6 +809,9 @@ function renderComponentFace(component, mode) {
case "pdu":
return `
+
+
+ ${header}
${label}
${renderSocketStrip(component)}
${leds}
@@ -676,14 +820,21 @@ function renderComponentFace(component, mode) {
case "ups":
return `
+
+
+ ${header}
+
${label}
`;
case "shelf":
return `
@@ -691,12 +842,18 @@ function renderComponentFace(component, mode) {
case "blank_panel":
return `
`;
default:
return `
`;
@@ -720,6 +877,158 @@ function renderSocketStrip(component) {
return Array.from({ length: totalSockets }, () => "
").join("");
}
+function renderInsertionZones(rack) {
+ const component = getComponent(state.selectedComponentId);
+ if (!component) {
+ return "";
+ }
+
+ const zones = [];
+ for (let y = 0; y <= getRackHeightPx(rack) - getComponentHeightPx(component); y += RACK_UNIT_PX) {
+ const occupied = state.placedItems.some((item) => {
+ const other = getComponent(item.componentId);
+ if (!other) {
+ return false;
+ }
+ return rectanglesOverlap(
+ {
+ left: getDefaultComponentX(component),
+ top: y,
+ width: getComponentWidthPercent(component),
+ height: getComponentHeightPx(component),
+ },
+ {
+ left: item.x,
+ top: item.y,
+ width: getComponentWidthPercent(other),
+ height: getComponentHeightPx(other),
+ }
+ );
+ });
+
+ zones.push(`
+
+ `);
+ }
+
+ return zones.join("");
+}
+
+function getRackHeightPx(rack) {
+ return rack.totalU * RACK_UNIT_PX;
+}
+
+function getComponentHeightPx(component) {
+ return component.heightU * RACK_UNIT_PX;
+}
+
+function getComponentWidthPercent(component) {
+ return component.rackStandard === "10_inch" ? 58 : 86;
+}
+
+function getDefaultComponentX(component) {
+ return (100 - getComponentWidthPercent(component)) / 2;
+}
+
+function getPlacementRect(item, component) {
+ const rackInnerWidthPx = getRackInnerWidthPx();
+ const width = (getComponentWidthPercent(component) / 100) * rackInnerWidthPx;
+ const left = (item.x / 100) * rackInnerWidthPx;
+ return {
+ left,
+ top: item.y,
+ width,
+ height: getComponentHeightPx(component),
+ };
+}
+
+function getPlacementCenterY(item, component) {
+ return item.y + getComponentHeightPx(component) / 2;
+}
+
+function formatRackPosition(y) {
+ return `${(y / RACK_UNIT_PX + 1).toFixed(1)}U`;
+}
+
+function getPlacementOverlaps() {
+ const overlaps = [];
+ for (let index = 0; index < state.placedItems.length; index += 1) {
+ for (let compareIndex = index + 1; compareIndex < state.placedItems.length; compareIndex += 1) {
+ const aItem = state.placedItems[index];
+ const bItem = state.placedItems[compareIndex];
+ const aComponent = getComponent(aItem.componentId);
+ const bComponent = getComponent(bItem.componentId);
+ if (!aComponent || !bComponent) {
+ continue;
+ }
+ if (rectanglesOverlap(getPlacementRect(aItem, aComponent), getPlacementRect(bItem, bComponent))) {
+ overlaps.push({ a: aComponent.name, b: bComponent.name });
+ }
+ }
+ }
+ return overlaps;
+}
+
+function rectanglesOverlap(a, b) {
+ return a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
+}
+
+function getRackInnerWidthPx() {
+ const layer = document.getElementById("rack-items-layer");
+ if (layer) {
+ return layer.clientWidth || 620;
+ }
+ return 620;
+}
+
+function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+}
+
+function applyRackColorTheme(element, color) {
+ const rgb = hexToRgb(color);
+ if (!rgb) {
+ return;
+ }
+
+ element.style.setProperty("--rack-frame", color);
+ element.style.setProperty("--rack-frame-dark", shadeRgb(rgb, -34));
+ element.style.setProperty("--rack-frame-light", shadeRgb(rgb, 28));
+ element.style.setProperty("--rack-rail", shadeRgb(rgb, 18));
+}
+
+function hexToRgb(hex) {
+ const normalized = String(hex).replace("#", "");
+ if (!/^[a-f0-9]{6}$/i.test(normalized)) {
+ return null;
+ }
+
+ return {
+ r: Number.parseInt(normalized.slice(0, 2), 16),
+ g: Number.parseInt(normalized.slice(2, 4), 16),
+ b: Number.parseInt(normalized.slice(4, 6), 16),
+ };
+}
+
+function shadeRgb(rgb, amount) {
+ const channel = (value) => Math.max(0, Math.min(255, value + amount));
+ return `rgb(${channel(rgb.r)}, ${channel(rgb.g)}, ${channel(rgb.b)})`;
+}
+
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
diff --git a/public/index.php b/public/index.php
index ec18e09..ded5764 100644
--- a/public/index.php
+++ b/public/index.php
@@ -58,6 +58,17 @@ $apiBase = $publicBase === '' ? '' : $publicBase;
+
+
+
+
Einsteckmodus
+
Kein Element ausgewaehlt
+
+
+