使用three,three-orbitcontrols写一个3D的模型,显示货物在集装箱中的分布情况,点击对应的模型会显示模型的信息
/* eslint-disable react/no-array-index-key */
/* eslint-disable no-plusplus */
import React from 'react';
import * as THREE from 'three'
import OrbitControls from 'three-orbitcontrols';
import { Form, Col, Row, Input, Icon, Button, InputNumber } from 'antd';
import { notify } from '../../utils/utils'
// import { formatMessage } from 'umi-plugin-locale';
// const { Option } = Select
let camera;
let scene;
let renderer;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const temporary = []
let colors = ''
@Form.create()
class Box extends React.Component {
state = {
number: 1,
list: [],
// spinning: false,
color: ['#f47920'],
box: [{
id: 1,
name: '20gp',
width: 2350,
long: 5900,
height: 2655,
weight: 28000
}, {
id: 2,
name: '20hq',
width: 2350,
long: 5900,
height: 2390,
weight: 28200
}, {
id: 3,
name: '30gp',
width: 2340,
long: 8940,
height: 2655,
weight: 28200
}, {
id: 4,
name: '30hq',
width: 2340,
long: 8900,
height: 2360,
weight: 28400
}, {
id: 5,
name: '40gp',
width: 2350,
long: 12030,
height: 2655,
weight: 28600
}, {
id: 6,
name: '40hq',
width: 2350,
long: 12030,
height: 2390,
weight: 28800
}, {
id: 7,
name: '45gp',
width: 2340,
long: 13550,
height: 2655,
weight: 28000
}, {
id: 8,
name: '45hq',
width: 2340,
long: 13550,
height: 2360,
weight: 27800
}],
}
componentDidMount() {
// this.init();
this.initScene() // 初始化场景
}
// onresize 事件会在窗口被调整大小时发生
onresize = () => {
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix();
};
init = async (list) => {
this.containners(list) // 生成箱子排列顺序
this.containner() // 创建最外层的箱子
this.renender() // 渲染页面
}
// 初始化场景
initScene = () => {
scene = new THREE.Scene();
/**
* 光源设置
*/
// 点光源
const point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); // 点光源位置
scene.add(point); // 点光源添加到场景中
// 环境光
const ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
/**
* 相机设置
*/
const width = window.innerWidth; // 窗口宽度
const height = window.innerHeight; // 窗口高度
const k = width / height; // 窗口宽高比
const s = 200; // 三维场景显示范围控制系数,系数越大,显示的范围越大
// 创建相机对象
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
// camera = new THREE.PerspectiveCamera(70, width / height, 1, 1000);
camera.position.set(100, 100, 300); // 设置相机位置
camera.lookAt(scene.position); // 设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);// 设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); // 设置背景颜色
document.getElementById("webgl-output").appendChild(renderer.domElement); // body元素中插入canvas对象
}
// 创建最外层的箱子
containner = () => {
// 点
const geometry1 = new THREE.BufferGeometry(); // 创建一个Buffer类型几何体对象
// 类型数组创建顶点数据
const vertices = new Float32Array([
// 线
100, 50, -50,
100, 50, 50,
100, 50, -50,
100, -50, -50,
-100, -50, -50,
-100, 50, -50,
100, 50, -50,
100, 50, 50,
-100, 50, 50,
-100, -50, 50,
-100, -50, -50,
-100, 50, -50,
-100, 50, 50,
-100, -50, 50,
100, -50, 50,
100, -50, 50,
100, -50, -50,
100, -50, 50,
100, 50, 50
]);
// 创建属性缓冲区对象
const attribue = new THREE.BufferAttribute(vertices, 3); // 3个为一组,表示一个顶点的xyz坐标
// 设置几何体attributes属性的位置属性
geometry1.attributes.position = attribue;
const material1 = new THREE.LineBasicMaterial({
color: 0xff0000 // 线条颜色
});// 材质对象
// material1.name = '集装箱'
const points = new THREE.Line(geometry1, material1); // 网格模型对象Mesh
scene.add(points); // 点对象添加到场景中
}
// 渲染页面
renender = () => {
this.divRender()
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);// 执行渲染操作
}
render();
const controls = new OrbitControls(camera, renderer.domElement);// 创建控件对象
controls.addEventListener('change', render);// 监听鼠标、键盘事件
window.addEventListener('mousemove', this.onMouseMove, false);
window.addEventListener('click', this.onMouseClick, true);
window.addEventListener('resize', this.onresize)
}
// 生成箱子
containners = (list) => {
const { box } = this.state
// // 冒泡排序,以货物的长
for (let index = 0; index < list.length - 1; index++) {
for (let indexs = 0; indexs < list.length - 1 - index; indexs++) {
if (list[indexs].long < list[indexs + 1].long) {
const temp = list[indexs];
list[indexs] = list[indexs + 1];
list[indexs + 1] = temp;
}
}
}
let long = 0
let width = 0
let height = 0
let longs = 0 // 长
let widths = 0 // 宽
let col = [] // 列中最宽
let row = 0 // 行中最长
let con = 0
let weights = 0 // 总重量
let longNum = 0 // 总长度
// let cargo = [] // 货物
// let personage = 0 // 箱子下标
for (let index = 0; index < list.length; index++) {
const item = list[index];
long = (200 / box[0].long) * item.long
width = (100 / box[0].width) * item.width
height = (100 / box[0].height) * item.height
let num = 0
if (index === 0) {
longNum = long
}
// 循环货物信息
for (let indexs = 0; indexs < item.number; indexs++) {
// 判断重量是否换箱
if (weights >= 28000 || longNum >= 5900) {
// 换箱
// personage++
}
// 判断货物是否要换列
if (((num + 1) * height) + con > 100) {
num = 0
con = 0
// widths += width
if (col.length > 0) {
widths += col[0]
col = []
} else {
widths += width
}
}
// 判断货物是否要换行
if (widths + width > 100) {
widths = 0
longNum += long
if (row !== 0) {
longs += row
row = 0
} else {
longs += long
}
}
weights += item.weight
// cargo[personage].push({ long, height, width, longs, heights: (num * height) + con, widths: -widths, color: item.color, id: item.id })
this.box(long, height, width, longs, (num * height) + con, -widths, item.color, item.id)
// 该货物以摞完,记录一下
// 刚好换列
if (indexs === item.number - 1) {
con = num * height + height + con
col.push(width)
row = long
}
num++
}
}
// console.log(cargo);
}
onMouseClick = (event) => {
if (temporary.length) {
temporary[temporary.length - 1].object.material.color.set(colors)
}
// 通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length && intersects[0].object.material.name) {
// div.style.display = "";
temporary.push(intersects[0])
colors = JSON.parse(JSON.stringify(intersects[0].object.material.color))
intersects[0].object.material.color.set(0xff0000)
const { list } = this.state
let lists = {}
for (let index = 0; index < list.length; index++) {
const item = list[index];
if (item.id === intersects[0].object.material.name) lists = item
}
document.getElementById('title').innerHTML = '名称:'
document.getElementById('titles').innerHTML = lists.name;
document.getElementById('long').innerHTML = `长:${lists.long}/mm`;
document.getElementById('weight').innerHTML = `宽:${lists.width}/mm`;
document.getElementById('height').innerHTML = `高:${lists.height}/mm`;
document.getElementById('volume').innerHTML = `体积:${lists.weight}/KG`;
} else {
document.getElementById('title').innerHTML = '箱型:'
document.getElementById('titles').innerHTML = '20GP';
document.getElementById('long').innerHTML = '长:5900/mm';
document.getElementById('weight').innerHTML = '宽:2350/mm';
document.getElementById('height').innerHTML = '高:2655/mm';
document.getElementById('volume').innerHTML = '体积:28000/KG';
}
}
// 创建货物模型
/**
*
* @param {货物的长} long == x
* @param {货物的高} height == z
* @param {货物的宽} width == y
* @param {货物偏移X坐标的长度} x
* @param {货物偏移y坐标的长度} y
* @param {货物偏移z坐标的长度} z
* @param {货物颜色} color
*/
box = (long, height, width, x = 0, y = 0, z = 0, color, name) => {
const geometry = new THREE.BoxGeometry(long, height, width)
const material = new THREE.MeshBasicMaterial({
color
})
material.name = name
const rect = new THREE.Mesh(geometry, material)
rect.position.set(-(100 - (long / 2)) + x, -(50 - (height / 2)) + y, (50 - (width / 2)) + z);// 设置mesh3模型对象的xyz坐标为120,0,0
scene.add(rect)
const geometry1 = new THREE.BufferGeometry(); // 创建一个Buffer类型几何体对象
// 类型数组创建顶点数据
const vertices = new Float32Array([
// 线
(long / 2), (height / 2), -(width / 2),
(long / 2), (height / 2), (width / 2),
(long / 2), -(height / 2), (width / 2),
(long / 2), -(height / 2), -(width / 2),
(long / 2), (height / 2), -(width / 2),
-(long / 2), (height / 2), -(width / 2),
-(long / 2), -(height / 2), -(width / 2),
(long / 2), -(height / 2), -(width / 2),
(long / 2), -(height / 2), (width / 2),
-(long / 2), -(height / 2), (width / 2),
-(long / 2), -(height / 2), -(width / 2),
-(long / 2), -(height / 2), (width / 2),
-(long / 2), (height / 2), (width / 2),
-(long / 2), (height / 2), -(width / 2),
-(long / 2), (height / 2), (width / 2),
(long / 2), (height / 2), (width / 2)
]);
// 创建属性缓冲区对象
const attribue = new THREE.BufferAttribute(vertices, 3); // 3个为一组,表示一个顶点的xyz坐标
// 设置几何体attributes属性的位置属性
geometry1.attributes.position = attribue;
const material1 = new THREE.LineBasicMaterial({
color: 0xffffff, // 线条颜色
});// 材质对象
// material1.name = '线'
const points = new THREE.Line(geometry1, material1); // 网格模型对象Mesh
points.position.set(-(100 - (long / 2)) + x, -(50 - (height / 2)) + y, (50 - (width / 2)) + z);// 设置mesh3模型对象的xyz坐标为120,0,0
scene.add(points); // 点对象添加到场景中
}
onMouseMove = (event) => {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}
// 计算三维坐标对应的屏幕坐标
divRender = () => {
// 计算三维坐标对应的屏幕坐标
// const position = new THREE.Vector3(-280, 0, 0);
// const windowPosition = this.transPosition(position);
// const left = windowPosition.x;
// const top = windowPosition.y;
// 设置div屏幕位置
const div = document.getElementById('webgl-output');
div.style.display = '';
const form = document.getElementById('form');
form.style.display = 'none';
}
// 三位坐标转屏幕坐标的方法
transPosition = (position) => {
const worldVector = new THREE.Vector3(position.x, position.y, position.z);
const vector = worldVector.project(camera);
const halfWidth = window.innerWidth / 2
const halfHeight = window.innerHeight / 2
return {
x: Math.round(vector.x * halfWidth + halfWidth),
y: Math.round(-vector.y * halfHeight + halfHeight)
};
}
add = () => {
const { form } = this.props;
const { number, color } = this.state;
this.setState({ number: number + 1 });
const keys = form.getFieldValue('keys');
const nextKeys = keys.concat(number + 1);
form.setFieldsValue({
keys: nextKeys,
});
let c = "#";
const cArray = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
for (let i = 0; i < 6; i++) {
const cIndex = Math.round(Math.random() * 15);
c += cArray[cIndex];
}
color.push(c)
this.setState({ color })
};
remove = k => {
const { form } = this.props;
// const { color } = this.state
const keys = form.getFieldValue('keys');
if (keys.length === 1) {
return;
}
form.setFieldsValue({
keys: keys.filter(key => key !== k),
});
// this.setState({ color: color.filter(key => key !== k) })
};
// 提交表单
submit = () => {
const { box } = this.state
const { form } = this.props
form.validateFields(async (err, value) => {
if (err) return err;
// this.setState({ spinning: true })
// 判断用户输入的货物信息是否正确
const list = []
for (let index = 0; index < value.list.length; index++) {
const item = value.list[index];
if (item && item.name) {
list.push(item)
// 拿货物的重量和集装箱的最大载重量比较
if (+item.weight > 28800) {
return notify({ message: `第${index}个货物重量超过集装箱的最大载重` })
}
// let num = 0
// 一个货物有4中摆放方式,拿4中摆放方式和箱子的长宽高比较,看是否可以塞进箱子里
// for (let indexs = 0; indexs < box.length; indexs++) {
// const element = box[indexs];
// if ((+item.long <= +element.long && +item.width <= +element.width && +item.height <= +element.height) || (+item.width <= +element.long && +item.long <= +element.width && +item.height <= +element.height) || (+item.height <= +element.long && +item.long <= +element.width && +item.width <= +element.height) || (+item.height <= +element.long && +item.width <= +element.width && +item.long <= +element.height)) {
// num++
// }
// }
// if (num === 0) {
// return notify({ message: `第${index + 1}个货物没有合适的箱型` })
// }
// 按一种摆放方式来算,箱子也按第一个来,如果货物的长宽高大于集装箱的长宽高,提醒用户
if (item.long > box[0].long || item.width > box[0].width || item.height > box[0].height) {
return notify({ message: `第${index}个货物没有合适的箱型` })
}
}
}
console.log(list);
// 进入模型页面
this.setState({ list })
document.getElementById('form').style.display = 'none'
await this.init(list);
// this.setState({ spinning: false })
})
}
onClane = () => {
const div = document.getElementById('webgl-output');
div.style.display = 'none';
const form = document.getElementById('form');
form.style.display = '';
console.log(scene.children.length);
scene.children = []
}
render() {
const { color } = this.state
const { form } = this.props
const { getFieldDecorator, getFieldValue } = form;
const formItemLayout = {
labelCol: {
lg: 10,
sm: 24,
},
wrapperCol: {
lg: 14,
sm: 24,
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
lg: 17,
sm: 24,
offset: 7,
},
};
getFieldDecorator('keys', { initialValue: [] });
const keys = getFieldValue('keys');
if (keys.length === 0) {
keys.push(1)
}
const formItems = keys.map((k, index) => (
<div key={index}>
<Row>
<Col style={{ display: 'none' }}>
<Form.Item
{...formItemLayout}
label='id'
>
{getFieldDecorator(`list[${k}]id`, {
initialValue: k
})(
<Input
size="small"
style={{ marginRight: 8 }}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='名字'
>
{getFieldDecorator(`list[${k}]name`, {
rules: [
{
required: true,
message: '请输入单个货物的名字'
},
],
})(
<Input
size="small"
style={{ marginRight: 8 }}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='长/mm'
>
{getFieldDecorator(`list[${k}]long`, {
initialValue: 100,
rules: [
{
required: true,
message: '请输入单个货物的长度'
},
],
})(
<InputNumber
size="small"
style={{ width: '60%', marginRight: 8 }}
min={0}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='宽/mm'
>
{getFieldDecorator(`list[${k}]width`, {
initialValue: 100,
rules: [
{
required: true,
message: '请输入单个货物的宽'
},
],
})(
<InputNumber
size="small"
style={{ width: '60%', marginRight: 8 }}
min={0}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='高/mm'
>
{getFieldDecorator(`list[${k}]height`, {
initialValue: 100,
rules: [
{
required: true,
message: '请输入单个货物的高',
},
],
})(
<InputNumber
size="small"
style={{ width: '60%', marginRight: 8 }}
min={0}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='重量/KG'
>
{getFieldDecorator(`list[${k}]weight`, {
initialValue: 100,
rules: [
{
required: true,
message: '请输入单个货物的重量',
},
],
})(
<InputNumber
size="small"
style={{ width: '60%', marginRight: 8 }}
min={0}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='总件数/件'
>
{getFieldDecorator(`list[${k}]number`, {
initialValue: 1,
rules: [
{
required: true,
message: '请输入货物的总件数',
},
],
})(
<InputNumber
size="small"
style={{ width: '60%', marginRight: 8 }}
min={0}
/>
)}
</Form.Item>
</Col>
<Col span={3}>
<Form.Item
{...formItemLayout}
label='颜色'
key={k}
>
{getFieldDecorator(`list[${k}]color`, {
initialValue: color[k - 1]
})(
<Input type='color' style={{ width: '60%' }} />
)}
{keys.length > 1 ? (
<Icon
style={{ marginLeft: '15px', marginBottom: '5px' }}
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.remove(k)}
/>
) : null}
</Form.Item>
</Col>
</Row>
</div>
));
return (
<>
{/* */}
<div id='form' style={{ display: 'block' }}>
<Form className="ant-advanced-search-form">
<Col>{formItems}</Col>
<Col>
<Form.Item {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={this.add} style={{ width: '60%' }}>
<Icon type="plus" />添加货物
</Button>
</Form.Item>
<Form.Item {...formItemLayoutWithOutLabel} />
</Col>
<Col align='center'>
<Button type='primary' onClick={this.submit}>提交</Button>
</Col>
</Form>
</div>
<div id="webgl-output" style={{ display: 'none' }}>
<div id='tag' style={{ position: 'absolute', left: '20px', top: '20px', backgroundColor: 'rgba(0,10,40)', borderRadius: '10px', opacity: '0.5', fontSize: '4px', color: 'aqua', width: '200px', height: '150px', padding: '5px' }}>
<span id='title' style={{ padding: '5px', color: 'white', fontSize: '10px' }}>箱型:</span>
<span id='titles' style={{ fontSize: '11px', fontWeight: 'bold' }}>20GP</span>
<p id='long' style={{ padding: '5px', marginTop: '3px' }}> 长:5900/mm</p>
<p id='weight' style={{ padding: '5px', marginTop: '-3px' }}> 宽:2350/mm</p>
<p id='height' style={{ padding: '5px', marginTop: '-3px' }}> 高:2655/mm</p>
<p id='volume' style={{ padding: '5px', marginTop: '-3px' }}> 容量:28000/KG</p>
</div>
<div id='btn' style={{ position: 'absolute', float: 'right', right: '20px', top: '20px' }}>
<Button type='primary' style={{ borderRadius: '10px' }} onClick={this.onClane}>关闭</Button>
</div>
</div>
</>
)
}
}
export default Box;