2025-04-18 13:20:01 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<title>集装箱装箱优化系统</title>
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body { margin: 0; }
|
|
|
|
|
|
#container { width: 100vw; height: 100vh; }
|
|
|
|
|
|
#inputPanel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
left: 20px;
|
|
|
|
|
|
background: rgba(255,255,255,0.8);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-family: Arial;
|
|
|
|
|
|
}
|
|
|
|
|
|
#result { margin-top: 20px; font-weight: bold; }
|
|
|
|
|
|
.axis { position: absolute; bottom: 20px; left: 20px; background: rgba(255,255,255,0.8); padding: 10px; border-radius: 4px; }
|
2025-04-18 14:03:36 +08:00
|
|
|
|
.tooltip {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
background: rgba(0,0,0,0.7);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.3s;
|
2025-04-23 11:13:26 +08:00
|
|
|
|
white-space: nowrap;
|
2025-04-18 14:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
.layer-checkbox { margin-bottom: 10px; }
|
2025-04-21 14:12:26 +08:00
|
|
|
|
#weightLimit { margin-top: 10px; }
|
|
|
|
|
|
.layer-checkboxes { margin-top: 20px; }
|
2025-04-23 11:13:26 +08:00
|
|
|
|
.box-tooltip {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
2025-04-18 13:20:01 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div id="inputPanel">
|
|
|
|
|
|
<h3>参数输入</h3>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
集装箱尺寸(mm):<br>
|
|
|
|
|
|
长:<input type="number" id="conLen" value="12014"><br>
|
|
|
|
|
|
宽:<input type="number" id="conWid" value="2337"><br>
|
|
|
|
|
|
高:<input type="number" id="conHei" value="2388"><br>
|
2025-04-21 14:12:26 +08:00
|
|
|
|
承重上限(kg):<input type="number" id="weightLimit" value="2000"><br>
|
2025-04-18 13:20:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
纸箱尺寸(mm):<br>
|
|
|
|
|
|
长:<input type="number" id="boxLen" value="685"><br>
|
|
|
|
|
|
宽:<input type="number" id="boxWid" value="548"><br>
|
|
|
|
|
|
高:<input type="number" id="boxHei" value="489"><br>
|
2025-04-21 14:12:26 +08:00
|
|
|
|
重量(kg):<input type="number" id="boxWeight" value="10"><br>
|
2025-04-18 13:20:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button onclick="calculate()">开始计算</button>
|
|
|
|
|
|
<div id="result"></div>
|
2025-04-18 14:03:36 +08:00
|
|
|
|
<div id="instructions" style="margin-top:20px; max-width:300px; line-height:1.5;"></div>
|
|
|
|
|
|
<div class="layer-checkboxes"></div>
|
2025-04-18 13:20:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div id="axisInfo" class="axis">视角:前视图</div>
|
|
|
|
|
|
<div id="container"></div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
let scene, camera, renderer, controls;
|
|
|
|
|
|
let containerMesh, boxMeshes = [];
|
2025-04-18 14:03:36 +08:00
|
|
|
|
let result = {};
|
2025-04-18 13:20:01 +08:00
|
|
|
|
|
|
|
|
|
|
function initThree() {
|
|
|
|
|
|
scene = new THREE.Scene();
|
2025-04-21 14:12:26 +08:00
|
|
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 100000);
|
2025-04-18 13:20:01 +08:00
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
|
|
document.getElementById('container').appendChild(renderer.domElement);
|
|
|
|
|
|
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
2025-04-21 14:12:26 +08:00
|
|
|
|
controls.enableZoom = true;
|
|
|
|
|
|
controls.enablePan = true;
|
|
|
|
|
|
controls.enableRotate = true;
|
|
|
|
|
|
|
2025-04-18 13:20:01 +08:00
|
|
|
|
camera.position.set(15000, 5000, 15000);
|
2025-04-21 14:12:26 +08:00
|
|
|
|
controls.target.set(0, 0, 0);
|
2025-04-18 13:20:01 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const gridHelper = new THREE.GridHelper(20000, 20, 0xffffff, 0x444444);
|
|
|
|
|
|
gridHelper.material.opacity = 0.5;
|
2025-04-18 13:20:01 +08:00
|
|
|
|
scene.add(gridHelper);
|
|
|
|
|
|
|
|
|
|
|
|
const axesHelper = new THREE.AxesHelper(5000);
|
|
|
|
|
|
scene.add(axesHelper);
|
|
|
|
|
|
|
2025-04-22 18:59:06 +08:00
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
|
|
|
camera.updateProjectionMatrix();
|
|
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-18 13:20:01 +08:00
|
|
|
|
animate();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function animate() {
|
|
|
|
|
|
requestAnimationFrame(animate);
|
|
|
|
|
|
controls.update();
|
|
|
|
|
|
renderer.render(scene, camera);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function calculate() {
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
container: {
|
|
|
|
|
|
length: parseFloat(document.getElementById('conLen').value),
|
|
|
|
|
|
width: parseFloat(document.getElementById('conWid').value),
|
2025-04-21 14:12:26 +08:00
|
|
|
|
height: parseFloat(document.getElementById('conHei').value),
|
|
|
|
|
|
weightLimit: parseFloat(document.getElementById('weightLimit').value)
|
2025-04-18 13:20:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
box: {
|
|
|
|
|
|
length: parseFloat(document.getElementById('boxLen').value),
|
|
|
|
|
|
width: parseFloat(document.getElementById('boxWid').value),
|
2025-04-21 14:12:26 +08:00
|
|
|
|
height: parseFloat(document.getElementById('boxHei').value),
|
|
|
|
|
|
weight: parseFloat(document.getElementById('boxWeight').value)
|
2025-04-18 13:20:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-04-22 18:59:06 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/calculate', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
|
});
|
2025-04-22 18:59:06 +08:00
|
|
|
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
2025-04-21 14:12:26 +08:00
|
|
|
|
result = await response.json();
|
|
|
|
|
|
document.getElementById('result').innerHTML = `最优装箱数:${result.count}`;
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const instructions = `
|
|
|
|
|
|
装箱方案说明:<br>
|
|
|
|
|
|
1. 纸箱尺寸:长${result.boxLength}mm × 宽${result.boxWidth}mm × 高${result.boxHeight}mm<br>
|
|
|
|
|
|
2. 排列策略:${result.strategy}<br>
|
|
|
|
|
|
3. 空间利用率:${result.spaceUtilization.toFixed(2)}%<br>
|
|
|
|
|
|
4. 总重量:${result.totalWeight.toFixed(2)} kg(承重上限:${data.container.weightLimit} kg)<br>
|
|
|
|
|
|
${result.totalWeight > data.container.weightLimit ? "⚠️ 超重!" : ""}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.getElementById('instructions').innerHTML = instructions;
|
2025-04-22 18:59:06 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
scene.remove(containerMesh);
|
|
|
|
|
|
boxMeshes.forEach(mesh => scene.remove(mesh));
|
|
|
|
|
|
boxMeshes = [];
|
2025-04-22 18:59:06 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const containerGeo = new THREE.BoxGeometry(
|
|
|
|
|
|
data.container.length,
|
|
|
|
|
|
data.container.height,
|
|
|
|
|
|
data.container.width
|
|
|
|
|
|
);
|
|
|
|
|
|
const containerMat = new THREE.MeshBasicMaterial({
|
2025-04-22 18:59:06 +08:00
|
|
|
|
color: 0xAAAAAA,
|
|
|
|
|
|
wireframe: true
|
2025-04-21 14:12:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
containerMesh = new THREE.Mesh(containerGeo, containerMat);
|
|
|
|
|
|
containerMesh.position.set(
|
|
|
|
|
|
data.container.length / 2,
|
|
|
|
|
|
data.container.height / 2,
|
|
|
|
|
|
data.container.width / 2
|
|
|
|
|
|
);
|
|
|
|
|
|
scene.add(containerMesh);
|
2025-04-22 18:59:06 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const layerCheckboxes = document.querySelector('.layer-checkboxes');
|
|
|
|
|
|
layerCheckboxes.innerHTML = '';
|
|
|
|
|
|
result.layers.forEach((layer, index) => {
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
const checkbox = document.createElement('input');
|
|
|
|
|
|
checkbox.type = 'checkbox';
|
|
|
|
|
|
checkbox.id = `layer-${index}`;
|
|
|
|
|
|
checkbox.checked = true;
|
|
|
|
|
|
checkbox.addEventListener('change', updateVisibility);
|
|
|
|
|
|
const label = document.createElement('label');
|
|
|
|
|
|
label.htmlFor = `layer-${index}`;
|
|
|
|
|
|
label.textContent = `第${index + 1}层 (${layer.count}箱)`;
|
|
|
|
|
|
div.appendChild(checkbox);
|
|
|
|
|
|
div.appendChild(label);
|
|
|
|
|
|
layerCheckboxes.appendChild(div);
|
|
|
|
|
|
});
|
2025-04-22 18:59:06 +08:00
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
updateVisibility();
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
2025-04-22 18:59:06 +08:00
|
|
|
|
const light = new THREE.DirectionalLight(0xffffff, 1);
|
|
|
|
|
|
light.position.set(1, 1, 1).normalize();
|
|
|
|
|
|
scene.add(light);
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
|
|
|
|
|
scene.add(ambientLight);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error:', error);
|
|
|
|
|
|
document.getElementById('result').innerHTML = '计算失败,请检查输入参数。';
|
|
|
|
|
|
}
|
2025-04-18 14:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateVisibility() {
|
|
|
|
|
|
boxMeshes.forEach(mesh => scene.remove(mesh));
|
|
|
|
|
|
boxMeshes = [];
|
|
|
|
|
|
const checkedLayers = Array.from(document.querySelectorAll('.layer-checkboxes input:checked'))
|
|
|
|
|
|
.map(cb => parseInt(cb.id.split('-')[1]));
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
2025-04-18 14:03:36 +08:00
|
|
|
|
checkedLayers.forEach(layerIndex => {
|
|
|
|
|
|
const layer = result.layers[layerIndex];
|
|
|
|
|
|
layer.layout.forEach(pos => {
|
|
|
|
|
|
const boxGeo = new THREE.BoxGeometry(
|
|
|
|
|
|
result.boxLength,
|
|
|
|
|
|
result.boxHeight,
|
|
|
|
|
|
result.boxWidth
|
|
|
|
|
|
);
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建实体部分
|
|
|
|
|
|
const boxMatFill = new THREE.MeshLambertMaterial({ color: 0xFFAAAA });
|
|
|
|
|
|
const boxFill = new THREE.Mesh(boxGeo, boxMatFill);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建激光线框
|
|
|
|
|
|
const lineGeo = new THREE.EdgesGeometry(boxGeo);
|
|
|
|
|
|
const lineMat = new THREE.LineBasicMaterial({
|
|
|
|
|
|
color: 0xffff00,
|
|
|
|
|
|
linewidth: 2
|
|
|
|
|
|
});
|
|
|
|
|
|
const line = new THREE.LineSegments(lineGeo, lineMat);
|
|
|
|
|
|
|
|
|
|
|
|
boxFill.add(line);
|
|
|
|
|
|
|
|
|
|
|
|
boxFill.position.set(
|
2025-04-21 14:12:26 +08:00
|
|
|
|
pos.x + result.boxLength / 2,
|
|
|
|
|
|
pos.y + result.boxHeight / 2,
|
|
|
|
|
|
pos.z + result.boxWidth / 2
|
2025-04-18 14:03:36 +08:00
|
|
|
|
);
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
boxFill.rotation.set(
|
2025-04-22 18:59:06 +08:00
|
|
|
|
pos.rotationX * Math.PI / 180,
|
|
|
|
|
|
pos.rotationY * Math.PI / 180,
|
|
|
|
|
|
pos.rotationZ * Math.PI / 180
|
2025-04-18 14:03:36 +08:00
|
|
|
|
);
|
2025-04-23 11:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
boxFill.userData.index = pos.boxNumber;
|
|
|
|
|
|
boxFill.addEventListener('pointerover', showTooltip);
|
|
|
|
|
|
boxFill.addEventListener('pointerout', hideTooltip);
|
|
|
|
|
|
|
|
|
|
|
|
scene.add(boxFill);
|
|
|
|
|
|
boxMeshes.push(boxFill);
|
2025-04-18 14:03:36 +08:00
|
|
|
|
});
|
2025-04-18 13:20:01 +08:00
|
|
|
|
});
|
2025-04-18 14:03:36 +08:00
|
|
|
|
}
|
2025-04-18 13:20:01 +08:00
|
|
|
|
|
2025-04-18 14:03:36 +08:00
|
|
|
|
function showTooltip(event) {
|
|
|
|
|
|
const box = event.target;
|
|
|
|
|
|
const position = box.position;
|
|
|
|
|
|
const rotation = box.rotation;
|
|
|
|
|
|
const dimensions = box.geometry.parameters;
|
2025-04-22 18:59:06 +08:00
|
|
|
|
const tooltip = document.createElement('div');
|
|
|
|
|
|
tooltip.className = 'tooltip';
|
2025-04-18 14:03:36 +08:00
|
|
|
|
tooltip.innerHTML = `
|
|
|
|
|
|
箱号:${box.userData.index}<br>
|
|
|
|
|
|
位置:(${position.x.toFixed(0)}, ${position.y.toFixed(0)}, ${position.z.toFixed(0)})<br>
|
|
|
|
|
|
尺寸:${dimensions.width}×${dimensions.height}×${dimensions.depth}<br>
|
2025-04-21 14:12:26 +08:00
|
|
|
|
旋转:X=${(rotation.x * 180 / Math.PI).toFixed(0)}°,
|
|
|
|
|
|
Y=${(rotation.y * 180 / Math.PI).toFixed(0)}°,
|
|
|
|
|
|
Z=${(rotation.z * 180 / Math.PI).toFixed(0)}°
|
2025-04-18 14:03:36 +08:00
|
|
|
|
`;
|
2025-04-22 18:59:06 +08:00
|
|
|
|
document.body.appendChild(tooltip);
|
2025-04-18 14:03:36 +08:00
|
|
|
|
tooltip.style.left = `${event.clientX + 10}px`;
|
|
|
|
|
|
tooltip.style.top = `${event.clientY - 30}px`;
|
|
|
|
|
|
tooltip.style.opacity = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hideTooltip() {
|
2025-04-22 18:59:06 +08:00
|
|
|
|
const tooltips = document.querySelectorAll('.tooltip');
|
|
|
|
|
|
tooltips.forEach(t => t.remove());
|
2025-04-18 13:20:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', (e) => {
|
2025-04-21 14:12:26 +08:00
|
|
|
|
const data = {
|
|
|
|
|
|
container: {
|
|
|
|
|
|
length: parseFloat(document.getElementById('conLen').value),
|
|
|
|
|
|
width: parseFloat(document.getElementById('conWid').value),
|
|
|
|
|
|
height: parseFloat(document.getElementById('conHei').value),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
switch (e.key) {
|
2025-04-23 11:13:26 +08:00
|
|
|
|
case '1':
|
2025-04-22 18:59:06 +08:00
|
|
|
|
camera.position.set(
|
|
|
|
|
|
data.container.length * 2,
|
|
|
|
|
|
data.container.height / 2,
|
|
|
|
|
|
data.container.width / 2
|
|
|
|
|
|
);
|
2025-04-18 13:20:01 +08:00
|
|
|
|
break;
|
2025-04-23 11:13:26 +08:00
|
|
|
|
case '2':
|
2025-04-22 18:59:06 +08:00
|
|
|
|
camera.position.set(
|
|
|
|
|
|
data.container.length / 2,
|
|
|
|
|
|
data.container.height * 2,
|
|
|
|
|
|
data.container.width / 2
|
|
|
|
|
|
);
|
2025-04-18 13:20:01 +08:00
|
|
|
|
break;
|
2025-04-23 11:13:26 +08:00
|
|
|
|
case '3':
|
2025-04-22 18:59:06 +08:00
|
|
|
|
camera.position.set(
|
|
|
|
|
|
data.container.length / 2,
|
|
|
|
|
|
data.container.height / 2,
|
|
|
|
|
|
data.container.width * 2
|
|
|
|
|
|
);
|
2025-04-18 13:20:01 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-04-21 14:12:26 +08:00
|
|
|
|
controls.update();
|
2025-04-18 13:20:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-21 14:12:26 +08:00
|
|
|
|
window.onload = function() {
|
|
|
|
|
|
initThree();
|
|
|
|
|
|
};
|
2025-04-18 13:20:01 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|