const props = defineProps({ stackerData: { type: Object, required: true }, modelData: { type: Object, required: true } }); let modelState = reactive({ modelData: {} }); let stackerColorP = ref(0); let stackerColorN = ref(0); watch( () => props.stackerData, (newVal) => { props.stackerData.forEach((item) => { if (item.deviceCode === 'SC01') { if (item.model === 0) { stackerColorP.value = 4; } else { stackerColorP.value = item.status; } } else { if (item.model == 0) { stackerColorN.value = 4; } else { stackerColorN.value = item.status; } } }); }, { deep: true, immediate: true } ); watch( () => props.modelData, (newVal) => { newVal.map((item) => { modelState.modelData[item.storeAreaCode] = item.status; }); }, { deep: true, immediate: true } );
// 由于设计模型的不规则性,需要提前识别定义生成模型的起始位置,模型大小,中间间距等 const INIT_PARAMS: Surface.InitParamsType = { 'P-4': { name: 'P-1-4', x: -35.27, y: 2.7, z: 65.47, w: 2.45, h: 2.4, d: 1.6, gx: 0.36, gy: 0.36 }, 'P-3': { name: 'P-1-3', x: -34.38, y: 2.7, z: 49.55, w: 2.45, h: 2.4, d: 1.6, gx: 0.36, gy: 0.36 }, 'P-2': { name: 'P-1-2', x: -33.0, y: 2.7, z: 32.6, w: 2.45, h: 2.4, d: 1.6, gx: 0.36, gy: 0.36 }, 'P-1': { name: 'P-1-1', x: -32.74, y: 2.7, z: 14, w: 2.45, h: 2.4, d: 1.6, gx: 0.36, gy: 0.36 }, 'N-1': { name: 'N-1-1', x: -30.39, y: 2.7, z: -6.92, w: 2.63, h: 2.4, d: 1.6, gx: 0.415, gy: 0.36 }, 'N-2': { name: 'N-1-2', x: -31.55, y: 2.7, z: -29.42, w: 2.63, h: 2.4, d: 1.6, gx: 0.412, gy: 0.36 }, 'N-3': { name: 'N-1-3', x: -30.34, y: 2.7, z: -53.44, w: 2.89, h: 2.4, d: 1.6, gx: 0.38, gy: 0.36 }, 'N-4': { name: 'N-1-4', x: -29.16, y: 2.7, z: -81.74, w: 2.89, h: 2.4, d: 1.6, gx: 0.38, gy: 0.36 } };
// 创建货架数据 // P极22列4层,N极21列4层 const allData: { [key in Surface.ShelfKey]: (number[] | null)[][]; } = { 'P-1': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-2': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-3': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-4': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'N-1': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-2': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-3': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-4': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))) }; // 存储货架数据模型 const models: { [key in Surface.ShelfKey]: (any[] | null)[][]; } = { 'P-1': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-2': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-3': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'P-4': Array.from({ length: 4 }, () => Array.from({ length: 22 }, () => Array(2).fill(0))), 'N-1': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-2': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-3': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))), 'N-4': Array.from({ length: 4 }, () => Array.from({ length: 21 }, () => Array(2).fill(0))) };
const pHole = ['1-1', '1-13']; // 不能放东西的格子 const nHole = ['1-10', '1-21']; // 不能放东西的格子
// 将空的位置置空 pHole.forEach((item) => { const [p, q] = item.split('-'); const [i, j] = [parseInt(p) - 1, parseInt(q) - 1]; allData['P-3'][i][j] = null; allData['P-4'][i][j] = null; models['P-3'][i][j] = null; models['P-4'][i][j] = null; }); nHole.forEach((item) => { const [p, q] = item.split('-'); const [i, j] = [parseInt(p) - 1, parseInt(q) - 1]; allData['N-1'][i][j] = null; allData['N-2'][i][j] = null; models['N-1'][i][j] = null; models['N-2'][i][j] = null; });
const stackerColors: number[] = stackerStatus.map((item) => item.color.startsWith('#') ? hexifyColor(item.color) : 0xdfdfdf ); const storeColors: number[] = storeStatus.map((item) => item.color.startsWith('#') ? hexifyColor(item.color) : 0x8ac8fc ); // console.log(storeColors, 'color');
let stackerP: THREE.Mesh< THREE.BoxGeometry, THREE.MeshPhongMaterial, THREE.Object3DEventMap
| null = null; let stackerN: THREE.Mesh< THREE.BoxGeometry, THREE.MeshPhongMaterial, THREE.Object3DEventMap | null = null;
const webglRef = ref(); const devicePixelRatio: number = window.devicePixelRatio; let innerWidth: number = window.innerWidth, innerHeight: number = window.innerHeight;
//创建stats对象 const stats = new Stats();
// 创建GLTF加载器对象 const loader = new GLTFLoader(); // 创建解压缩加载器 const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('/draco/'); loader.setDRACOLoader(dracoLoader);
let renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera, controls: OrbitControls;
// 创建一个射线投射器 const raycaster: THREE.Raycaster = new THREE.Raycaster();
// 创建一个鼠标位置向量 const mouse: THREE.Vector2 = new THREE.Vector2();
let tooltip: HTMLDivElement = document.createElement('div');
function createTooltip() { // 创建一个容器div const tooltipContainer = document.createElement('div'); tooltipContainer.style.width = 'auto'; tooltipContainer.style.position = 'absolute'; tooltipContainer.style.left = '0px'; tooltipContainer.style.top = '0px'; tooltipContainer.style.background = '#ffffff'; tooltipContainer.style.color = '#000'; tooltipContainer.style.fontSize = '14px'; tooltipContainer.style.padding = '4px 8px'; tooltipContainer.style.borderRadius = '4px'; tooltipContainer.style.pointerEvents = 'none'; tooltipContainer.style.display = 'flex'; tooltipContainer.style.flexDirection = 'column'; // 默认为列布局,用于管理每个数据项
tooltipContainer.style.display = 'none';
document.body.appendChild(tooltipContainer);
tooltip = tooltipContainer; } // 防抖 function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => { func(...args); }, wait); }; }
async function showTooltip(event: MouseEvent, text: string) {
const params = { storeAreaCode: text };
let tooltipText = <div style="display: flex;;">;
try {
const response = await fetchData(params);
if (response && response.result && response.result.length > 0) {
if (response.result.length === 2) {
tooltipText += <div style="flex: 1; margin-right: 14px; padding-right:10px; border-right: 3px solid #D3D3D3;">;
response.result.forEach((item, index) => {
if (index === 0) {
tooltipText += <div style="margin-bottom: 4px;">;
tooltipText += <div>质检状态: ${item.qualityName}</div>;
tooltipText += <div>存储时间: ${formatTimeDiff(item.createTime)}</div>;
tooltipText += <div>库位编码: ${text}</div>;
tooltipText += <div>托盘条码: ${item.trayCode}</div>;
tooltipText += <div>入库时间: ${item.createTime}</div>;
tooltipText += <div>过期时间: ${item.expiringDate}</div>;
tooltipText += <div style="display: flex; justify-content: space-between;">物料条码1: ${item.materialCode}</div>;
tooltipText += </div>;
}
});
tooltipText += </div>;
tooltipText += `<div style="flex: 1;">`;
response.result.forEach((item, index) => {
if (index === 1) {
tooltipText += `<div>质检状态: ${item.qualityName}</div>`;
tooltipText += `<div>存储时间: ${formatTimeDiff(item.createTime)}</div>`;
tooltipText += `<div>库位编码: ${text}</div>`;
tooltipText += `<div>托盘条码: ${item.trayCode}</div>`;
tooltipText += `<div>入库时间: ${item.createTime}</div>`;
tooltipText += `<div>过期时间: ${item.expiringDate}</div>`;
tooltipText += `<div style="width:247px; display: flex; justify-content: space-between;" >物料条码2: ${item.materialCode}</div>`;
tooltipText += `</div>`;
}
});
tooltipText += `</div>`;
} else {
response.result.forEach((item, index) => {
tooltipText += `<div style="margin-bottom: 4px;">`;
tooltipText += `<div>质检状态: ${item.qualityName}</div>`;
tooltipText += `<div>存储时间: ${formatTimeDiff(item.createTime)}</div>`;
tooltipText += `<div>库位编码: ${text}</div>`;
tooltipText += `<div>托盘条码: ${item.trayCode}</div>`;
tooltipText += `<div>入库时间: ${item.createTime}</div>`;
tooltipText += `<div>过期时间: ${item.expiringDate}</div>`;
tooltipText += `<div style="display: flex; justify-content: space-between;">物料条码: ${item.materialCode}</div>`;
tooltipText += `</div>`;
});
}
} else {
tooltipText += `<div>库位编码:${text}</div>`;
}
} catch (error) {
console.error('在获取数据时发生错误:', error);
tooltipText += <div>加载数据失败</div>;
}
tooltipText += </div>; // 结束flex容器
// 清除旧内容并设置新内容 tooltip.innerHTML = tooltipText; tooltip.style.display = 'block';
// 更新位置
const { width, height } = tooltip.getBoundingClientRect();
tooltip.style.transform = translate(${event.clientX - width / 2}px, ${ event.clientY - height - 6 }px);
}
// 使用防抖包装 showTooltip
const debouncedShowTooltip = debounce(showTooltip, 100);
// 辅助函数来格式化时间差
function formatTimeDiff(createTime) {
const now = new Date();
const date1 = dayjs(createTime);
const date2 = dayjs(now);
const hoursDiff = date2.diff(date1, 'hour');
const fullDays = Math.floor(hoursDiff / 24);
const remainingHours = hoursDiff % 24;
return ${fullDays}天 ${remainingHours}小时;
}
function destroyTooltip() {
document.body.removeChild(tooltip);
}
function hideTooltip() {
tooltip.style.display = 'none';
tooltip.innerText = '';
}
function updateSize() { const { width, height } = webglRef.value.getBoundingClientRect(); innerWidth = width ?? window.innerWidth; innerHeight = height ?? window.innerWidth; } // 创建渲染器 function createRenderer() { renderer = new THREE.WebGLRenderer({ antialias: true, // 设置抗锯齿 logarithmicDepthBuffer: true // 是否使用对数深度缓存 }); renderer.setPixelRatio(devicePixelRatio); // 设置设备像素比 renderer.setSize(innerWidth, innerHeight); //解决加载gltf格式模型纹理贴图和原图不一样问题 默认为THREE.SRGBColorSpace, 新版本,加载gltf,不需要执行下面代码解决颜色偏差 renderer.outputColorSpace = THREE.SRGBColorSpace; webglRef.value!.appendChild(renderer.domElement); } // 创建场景 function createScene() { scene = new THREE.Scene(); // 创建3D场景对象Scene scene.background = new THREE.Color(0x08141c); // 设置背景 } // 创建相机 function createCamera() { camera = new THREE.PerspectiveCamera(22, innerWidth / innerHeight, 0.1, 1000); camera.position.set(194, 146, 206); // camera.lookAt(0, 0, 0); // 设置相机视觉中心点 }
function updateProjectionMatrix() { // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源) // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵 camera.updateProjectionMatrix(); } // 创建控制轨道 function createControls() { controls = new OrbitControls(camera, renderer.domElement); controls.enabled = false; // 禁止控制 controls.enableDamping = true; // 开启阻尼平滑效果 controls.dampingFactor = 0.2; // 阻尼系数 controls.enableZoom = true; // 控制缩放 controls.maxPolarAngle = Math.PI / 2; // 设置相机最大极角 controls.minPolarAngle = Math.PI / 3; // 设置相机最小极角 controls.enablePan = true; // 控制平移 controls.target.set(0, 0, 0); controls.update(); // 必须在手动更改摄像机的变换后调用 controls.update() } // 创建灯光 function createAmbientLight() { const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // 柔和的白光 ambientLight.visible = true; scene.add(ambientLight); } // 创建辅助对象 function createHelper() { // 创建坐标格辅助对象 const gridHelper = new THREE.GridHelper(500, 10); // 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴. const axesHelper = new THREE.AxesHelper(250); scene.add(gridHelper); scene.add(axesHelper); }
/**
- 创建货架盒子(以两个盒子一起的设计表示一个大托或两个小托的场景)
- @param name 盒子名称
- @param x 坐标x
- @param y 坐标y
- @param z 坐标z
- @param width 盒子宽度
- @param height 盒子高度
- @param depth 盒子深度 */ function createDoubleBox( name: string, x: number = 0, y: number = 0, z: number = 0, width: number = 2.45, height: number = 2.4, depth: number = 1.6 ) { const lBox = createBox(name + '_l', width, height, depth); lBox.position.set(x, y, z); const rBox = createBox(name + '_r', width, height, depth); rBox.position.set(x + width / 2, y, z); return { lBox, rBox }; }
// 创建盒子 function createBox(name: string, width: number = 1, height: number = 1, depth: number = 1) { const boxGeometry = new THREE.BoxGeometry(width / 2, height, depth); const boxMaterial = new THREE.MeshBasicMaterial({ color: 0x8ac8fc, side: THREE.FrontSide }); const box = new THREE.Mesh(boxGeometry, boxMaterial); box.name = name; return box; }
function animate() { requestAnimationFrame(animate); //requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间 stats.update(); controls.update();
render(); } function render() { renderer.render(scene, camera); } // 加载GLB function loadGlbfModel(url: string) { return new Promise((resolve, reject) => { loader.load(url, resolve, undefined, reject); }); }
// 生成模型
function generativeModel() {
Object.keys(INIT_PARAMS).forEach((key) => {
const _KEY = key as Surface.ShelfKey;
const obj = INIT_PARAMS[_KEY];
if (obj) {
for (let i = 0; i < allData[_KEY].length; i++) {
const _COL = allData[_KEY][i];
for (let j = 0; j < _COL.length; j++) {
if (_COL[j]) {
const _NAME = _KEY.startsWith('P')
? ${INIT_PARAMS[_KEY].name}-${j + 1}-${i + 1}
: ${INIT_PARAMS[_KEY].name}-${_COL.length - j}-${i + 1};
// let params = {
// storeAreaCode: _NAME
// }
// fetchData(params)
const { lBox, rBox } = createDoubleBox(
_NAME,
INIT_PARAMS[_KEY].x + (INIT_PARAMS[_KEY].w + INIT_PARAMS[_KEY].gx) * j,
INIT_PARAMS[_KEY].y + (INIT_PARAMS[_KEY].h + INIT_PARAMS[_KEY].gy) * i,
INIT_PARAMS[_KEY].z,
INIT_PARAMS[_KEY].w,
INIT_PARAMS[_KEY].h,
INIT_PARAMS[_KEY].d
);
scene.add(lBox);
scene.add(rBox);
models[_KEY][i][j]![0] = lBox;
models[_KEY][i][j]![1] = rBox;
}
}
}
}
}); }
// 更新数据,修改模型 根据allData数据的状态值修改对应models的模型数据 function updateData() { // 根据接口返回的数据,更新allData,并且刷新model, 注意P区是从-X轴方向开始,N区是从+X轴方向开始 Object.keys(allData).forEach((key) => { const _KEY = key as Surface.ShelfKey; const shelfData = allData[_KEY]; for (let i = 0; i < shelfData.length; i++) { const _COL = shelfData[i]; for (let j = 0; j < _COL.length; j++) { if (_COL[j]) { // 根据返回的数据是一个大托,则将child的两个格子都置为同样的数据 // 根据返回的数据是两个小托,则将child的两个格子都对应各自的数据 // TODO 根据返回的N区的数据应该需要将j索引换算一下 const x = _KEY.startsWith('P') ? j : j; const modelChild = models[_KEY][i][x]!; const dataChild = allData[_KEY][i][x]!;
// TODO 根据返回的数据更新状态值
let ModelColor;
ModelColor = modelChild[0].name.split('_')[0];
// console.log(ModelColor,"w22");
// 更新模型颜色
updateBoxColor(modelState.modelData[ModelColor], modelChild);
}
}
}
}); }
function updateBoxColor(state: number = 0, modelChild: any[]) { modelChild[0].material.color.set(storeColors[state]); modelChild[1].material.color.set(storeColors[state]); } // 修改推垛机模型 function changeStacker(material: THREE.MeshPhongMaterial, color: number) { material.color.set(color); material.emissive.set(color); }
// 初始化 async function init() { updateSize(); createRenderer(); createScene(); createCamera(); createControls(); createAmbientLight(); // createHelper(); const gltf: any = await loadGlbfModel('/model/shelf.glb'); gltf.scene.rotation.y = -Math.PI; gltf.scene.position.set(140, 0, -70); // 移动到世界坐标原点 // gltf.scene.scale.set(0.5,0.5, 0.5); // 缩放 // 返回的场景对象gltf.scene插入到threejs场景中 scene.add(gltf.scene); gltf.scene.traverse((child: any) => { if (child.isMesh) { if (child.name === '堆垛机_P') { stackerP = child; } if (child.name === '堆垛机_N') { stackerN = child; } } }); generativeModel();
run(); } // 测试 function run() { setInterval(() => { // 修改推垛机 changeStacker(stackerP!.material, stackerColors[stackerColorP.value]); changeStacker(stackerN!.material, stackerColors[stackerColorN.value]); // 修改货架物品
updateData();
}, 2000); }
function hexifyColor(color: string) { // 移除#符号 const hex = color.slice(1); // 转换为16进制并返回结果 return parseInt(hex, 16); } // 监听鼠标移动 function onDocumentMouseMove(event: MouseEvent) { // 将浏览器的2D鼠标位置转换为Three.js的标准设备坐标(-1到+1) mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 使用鼠标位置和相机进行射线投射 raycaster.setFromCamera(mouse, camera);
// 计算物体和射线的交点 const intersects = raycaster.intersectObjects(scene.children);
// 如果有交点 if (intersects.length > 0) { // 取第一个交点的对象 const intersection = intersects[0];
// 你可以在这里处理交点的对象
if (intersection.object.name.startsWith('P')) {
debouncedShowTooltip(event, intersection.object.name.split('_')[0]);
} else if (intersection.object.name.startsWith('N')) {
debouncedShowTooltip(event, intersection.object.name.split('_')[0]);
} else {
hideTooltip();
}
} } // 监听屏幕变化 function onWindowResize() { updateSize(); // 重置渲染器输出画布canvas尺寸 renderer.setSize(innerWidth, innerHeight); // 全屏情况下:设置观察范围长宽比aspect为窗口宽高比 camera.aspect = innerWidth / innerHeight; // 更新摄像机的投影矩阵 updateProjectionMatrix(); } // 页面挂载 onMounted(() => { init(); animate(); createTooltip(); window.addEventListener('resize', onWindowResize, false); document.addEventListener('mousemove', onDocumentMouseMove, false); }); onUnmounted(() => { window.removeEventListener('resize', onWindowResize, false); document.removeEventListener('mousemove', onDocumentMouseMove, false); destroyTooltip(); renderer.dispose(); renderer.forceContextLoss(); });