集装箱货物装箱3D模型

使用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;

集装箱货物装箱3D模型_第1张图片

集装箱货物装箱3D模型_第2张图片
集装箱货物装箱3D模型_第3张图片

你可能感兴趣的:(3d,javascript,react.js)