@ -0,0 +1,94 @@ |
|||||
|
## OBB 包围盒快速查找算法 |
||||
|
|
||||
|
### 物品数据结构 |
||||
|
|
||||
|
```typescript |
||||
|
items = ItemJson[] |
||||
|
|
||||
|
interface ItemJson { |
||||
|
// 物体ID, 唯一标识 |
||||
|
id: string |
||||
|
|
||||
|
/** |
||||
|
* 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前。右手坐标系 |
||||
|
*/ |
||||
|
tf: [ |
||||
|
/** |
||||
|
* 平移向量 position, 三维坐标, [0]=x, [1]=高度值,在2D下忽略, [2]=z |
||||
|
*/ |
||||
|
[number, number, number], |
||||
|
|
||||
|
/** |
||||
|
* 旋转向量 rotation, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 [0]=X轴逆向旋转角度, [1]=Y轴逆向旋转角度, [2]=Z轴逆向旋转角度 |
||||
|
*/ |
||||
|
[number, number, number], |
||||
|
|
||||
|
/** |
||||
|
* 缩放向量 scale, 三维缩放比例, [0]=X宽度, [1]=Y高度,在2D下忽略, [2]=Z长度 |
||||
|
*/ |
||||
|
[number, number, number], |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
### add(...items: ItemJson[]) |
||||
|
|
||||
|
添加点位 |
||||
|
|
||||
|
add(...items: ItemJson[]) |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### update(item: ItemJson) |
||||
|
|
||||
|
更新点位 |
||||
|
|
||||
|
update(itemJson) |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### deleteItem(id: string) |
||||
|
|
||||
|
删除点位 |
||||
|
|
||||
|
deleteItem(id: string) |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### getItemsByPosition(x:number, z:number): ItemJson[] |
||||
|
|
||||
|
根据位置,获取命中的物品集合 |
||||
|
|
||||
|
getItemsByPosition({x:number, z:number}): ItemJson[] |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### getItemsByPositionDistance(x, z, distance): {item, distance}[] |
||||
|
|
||||
|
getItemsByPositionDistance(x:number, z:number, distance:number): {item:ItemJson, distance:number}[] |
||||
|
|
||||
|
根据位置,获取周边单位距离内的所有物品集合,及距离 |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
(选择框) |
||||
|
### getItemsByRect(x1,y1,x2,y2): ItemJson[] |
||||
|
|
||||
|
根据矩形,获取与矩形有碰撞的所有物品集合 |
||||
|
|
||||
|
getItemsByPosition({x:number, z:number}): ItemJson[] |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### getItemsByRect2(x1,y1,x2,y2): ItemJson[] |
||||
|
|
||||
|
根据矩形,获取矩形内的所有物品,物品OBB包围盒必须完全在矩形内 |
||||
|
|
||||
|
getItemsByPosition({x:number, z:number}): ItemJson[] |
||||
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,828 @@ |
|||||
|
<template> |
||||
|
<div class="model3d-view"> |
||||
|
<el-space :gutter="10" class="toolbar"> |
||||
|
<el-button type="primary" @click="test1">测试1</el-button> |
||||
|
<el-button type="primary" @click="test2">测试2</el-button> |
||||
|
<el-button type="primary" @click="test3">测试3</el-button> |
||||
|
<div class="demo-color-block"> |
||||
|
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span> |
||||
|
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span> |
||||
|
<span class="demonstration"> 三角形:<el-text type="danger">{{ restate.faces }}</el-text></span> |
||||
|
<span class="demonstration"> 标签:<el-text type="danger">{{ restate.viewLabelCount }}</el-text></span> |
||||
|
</div> |
||||
|
</el-space> |
||||
|
<div class="main-content"> |
||||
|
<div class="canvas-container" ref="canvasContainer" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup lang="ts"> |
||||
|
import * as THREE from 'three' |
||||
|
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' |
||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' |
||||
|
import Stats from 'three/examples/jsm/libs/stats.module' |
||||
|
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry' |
||||
|
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial' |
||||
|
import { Line2 } from 'three/examples/jsm/lines/Line2' |
||||
|
import MeasureRenderer from '@/modules/measure/MeasureRenderer.ts' |
||||
|
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry' |
||||
|
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2' |
||||
|
import { Text } from 'troika-three-text' |
||||
|
import SimSunTTF from '@/assets/fonts/simsunb.ttf' |
||||
|
|
||||
|
const canvasContainer = ref(null) |
||||
|
let resizeObserver: ResizeObserver | null = null |
||||
|
let scene: THREE.Scene | null = null |
||||
|
let renderer: THREE.WebGLRenderer | null = null |
||||
|
let viewerDom: HTMLElement | null = null |
||||
|
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera | null = null |
||||
|
let controls: OrbitControls | null = null |
||||
|
let axesHelper: THREE.AxesHelper | null = null |
||||
|
let gridHelper: THREE.GridHelper | null = null |
||||
|
let statsControls: Stats | null = null |
||||
|
let animationFrameId: number | null = null |
||||
|
let modelGroup = new THREE.Group() |
||||
|
|
||||
|
const pointMaterial = new THREE.SpriteMaterial({ |
||||
|
color: 0xFFFF99, // 0x303133, |
||||
|
transparent: true, |
||||
|
side: THREE.DoubleSide, |
||||
|
opacity: 1, |
||||
|
sizeAttenuation: true |
||||
|
}) |
||||
|
// const lineMaterial = new LineMaterial({ |
||||
|
// color: 0xFF8C00, |
||||
|
// linewidth: 5, |
||||
|
// vertexColors: false, |
||||
|
// dashed: false |
||||
|
// }) |
||||
|
const lineMaterial = new LineMaterial({ |
||||
|
color: 0xFF8C00, |
||||
|
linewidth: 5 |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* 测试1:创建平面标识 |
||||
|
*/ |
||||
|
function test1() { |
||||
|
// cleanupThree() |
||||
|
|
||||
|
const [x, y, z] = [5, 0, 5] |
||||
|
|
||||
|
// 宽度 |
||||
|
const width = 1.5 |
||||
|
// 深度 |
||||
|
const depth = 2.0 |
||||
|
// 线条宽度 |
||||
|
const strokeWidth = 0.1 |
||||
|
// 每一组货架有几节 |
||||
|
const sectionCount = 1 |
||||
|
|
||||
|
const material = new THREE.MeshBasicMaterial({ |
||||
|
color: 0xd8dad0, |
||||
|
transparent: true, |
||||
|
opacity: 1, |
||||
|
side: THREE.BackSide |
||||
|
}) |
||||
|
|
||||
|
const mesh = new THREE.Mesh( |
||||
|
createGroundMarking(strokeWidth, width, depth, sectionCount, x, y, z), |
||||
|
material) |
||||
|
scene.add(mesh) |
||||
|
|
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建平面标识 |
||||
|
*/ |
||||
|
function createGroundMarking(weight: number, width: number, depth: number, sectionCount: number, x: number, y: number, z: number, anchor?: THREE.Vector3) { |
||||
|
const sectionDepth = (depth - (sectionCount + 1) * weight) / sectionCount |
||||
|
|
||||
|
const shape = buildRectangle(width, depth) |
||||
|
for (let i = 0; i < sectionCount; i++) { |
||||
|
shape.holes.push(buildRectangle(width - 2 * weight, sectionDepth, 0, weight, weight + i * (weight + sectionDepth))) |
||||
|
} |
||||
|
|
||||
|
return createExtrudeItem(shape, 0.1, BasePlane.BOTTOM, x, y, -z, null, anchor) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据自定义的 Shape,通过放样得到一个实体。默认实体的样式是在 front 面放样的。 |
||||
|
* |
||||
|
* @param {*} shape 自定义的型状 |
||||
|
* @param {*} depth 放样深度(放样后的物体厚度) |
||||
|
* @param {*} basePlane 放样的基准面。 |
||||
|
* @param {*} x 目标定位x |
||||
|
* @param {*} y 目标定位y |
||||
|
* @param {*} z 目标定位z |
||||
|
* @param {*} euler 旋转到目标位 |
||||
|
* @param {*} anchor 锚点 |
||||
|
* @returns THREE.ExtrudeGeometry |
||||
|
*/ |
||||
|
function createExtrudeItem(shape, depth, basePlane, x = 0, y = 0, z = 0, euler = null, anchor = new THREE.Vector3(0, 0, 0)) { |
||||
|
const geometry = new THREE.ExtrudeGeometry(shape, { |
||||
|
steps: 1, |
||||
|
depth: -depth, |
||||
|
bevelEnabled: false, |
||||
|
bevelThickness: 0, |
||||
|
bevelSize: 0, |
||||
|
bevelOffset: 0, |
||||
|
bevelSegments: 0 |
||||
|
}) |
||||
|
|
||||
|
geometry.center() |
||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3()) |
||||
|
|
||||
|
let dx, dy, dz |
||||
|
|
||||
|
switch (basePlane) { |
||||
|
case BasePlane.LEFT: |
||||
|
geometry.rotateY(Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.RIGHT: |
||||
|
geometry.rotateY(-Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.BEHIND: |
||||
|
geometry.translate(0, 0, size.z) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
case BasePlane.TOP: |
||||
|
geometry.rotateZ(Math.PI) |
||||
|
geometry.rotateX(Math.PI / 2) |
||||
|
geometry.translate(size.x, -size.z, 0) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
case BasePlane.BOTTOM: |
||||
|
geometry.rotateX(-Math.PI / 2) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
default: |
||||
|
//BasePlane.FRONT: |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if (euler != null && euler.isEuler) { |
||||
|
// 注意,需要先旋转,再平移。 |
||||
|
geometry.rotateX(euler.x) |
||||
|
geometry.rotateY(euler.y) |
||||
|
geometry.rotateZ(euler.z) |
||||
|
} |
||||
|
|
||||
|
geometry.translate(dx + x + anchor.x, dy + y + anchor.y, -dz + z + anchor.z) |
||||
|
|
||||
|
return geometry |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建一个矩形的 Shape |
||||
|
*/ |
||||
|
function buildRectangle(width: number, height: number, padding: number[] | number = 0, offsetX = 0, offsetY = 0) { |
||||
|
let pl = 0, |
||||
|
pt = 0, |
||||
|
pr = 0, |
||||
|
pb = 0 |
||||
|
|
||||
|
if (typeof padding == 'number') { |
||||
|
pl = padding |
||||
|
pt = padding |
||||
|
pr = padding |
||||
|
pb = padding |
||||
|
} else if (Array.isArray(padding)) { |
||||
|
if (padding.length == 1) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[0] |
||||
|
pr = padding[0] |
||||
|
pb = padding[0] |
||||
|
} else if (padding.length > 1 && padding.length < 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[0] |
||||
|
pb = padding[1] |
||||
|
} else if (padding.length >= 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[2] |
||||
|
pb = padding[3] |
||||
|
} |
||||
|
} |
||||
|
const shape = new THREE.Shape() |
||||
|
shape.moveTo(0 + pl + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, height - pt + offsetY) |
||||
|
shape.lineTo(0 + pl + offsetX, height - pt + offsetY) |
||||
|
shape.closePath() |
||||
|
return shape |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 平面产量 |
||||
|
*/ |
||||
|
const BasePlane = { |
||||
|
LEFT: 0b000010, |
||||
|
RIGHT: 0b000001, |
||||
|
FRONT: 0b001000, |
||||
|
BEHIND: 0b000100, |
||||
|
TOP: 0b100000, |
||||
|
BOTTOM: 0b010000, |
||||
|
TRANSVERSE: 0b10000000, |
||||
|
LONGITUDINAL: 0b01000000, |
||||
|
THROW: 0b00100000, |
||||
|
toArray: () => { |
||||
|
return [BasePlane.LEFT, BasePlane.BEHIND, BasePlane.RIGHT, BasePlane.FRONT, BasePlane.BOTTOM, BasePlane.TOP, BasePlane.TRANSVERSE, BasePlane.LONGITUDINAL, BasePlane.THROW] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LineSegments2 |
||||
|
*/ |
||||
|
function test2() { |
||||
|
const count = 10000 // 要创建的实例数量 |
||||
|
|
||||
|
const material = new THREE.MeshBasicMaterial({ |
||||
|
color: '#038217', |
||||
|
transparent: true, |
||||
|
opacity: 1, |
||||
|
side: THREE.BackSide |
||||
|
}) |
||||
|
|
||||
|
// 创建 InstancedMesh |
||||
|
const instancedMesh = new THREE.InstancedMesh(groundMarkingGeometry, material, count) |
||||
|
|
||||
|
// 设置所有实例的世界变换矩阵, 10列 列间距0.3, |
||||
|
for (let i = 0; i < count; i++) { |
||||
|
const x = (i % 10) * 3 |
||||
|
const y = 0 |
||||
|
const z = Math.floor(i / 10) * 3 |
||||
|
console.log(`Setting instance [${i}]=(${x}, ${z})`) |
||||
|
|
||||
|
const matrix = new THREE.Matrix4() |
||||
|
matrix.setPosition(x, y, z) // 可以加旋转/缩放 |
||||
|
instancedMesh.setMatrixAt(i, matrix) |
||||
|
} |
||||
|
|
||||
|
scene.add(instancedMesh) |
||||
|
|
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
const groundMarkingGeometry = createGroundMarkingGeometry() // 共享几何体 |
||||
|
|
||||
|
function createGroundMarkingGeometry() { |
||||
|
const weight = 0.1 |
||||
|
const width = 1.5 |
||||
|
const depth = 2.0 |
||||
|
const sectionCount = 1 |
||||
|
|
||||
|
const sectionDepth = (depth - (sectionCount + 1) * weight) / sectionCount |
||||
|
|
||||
|
const shape = buildRectangle(width, depth) |
||||
|
for (let i = 0; i < sectionCount; i++) { |
||||
|
shape.holes.push( |
||||
|
buildRectangle( |
||||
|
width - 2 * weight, |
||||
|
sectionDepth, |
||||
|
0, |
||||
|
weight, |
||||
|
weight + i * (weight + sectionDepth) |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return createExtrudeItem(shape, 0.1, BasePlane.BOTTOM, 0, 0, 0) |
||||
|
} |
||||
|
|
||||
|
function isLabelInView(label, frustum) { |
||||
|
const pos = new THREE.Vector3() |
||||
|
label.getWorldPosition(pos) |
||||
|
|
||||
|
// 正交相机的视锥体范围 |
||||
|
if (frustum.containsPoint(pos)) { |
||||
|
// 检查标签是否在相机位置的最大距离内 |
||||
|
if (shouldShowLabel(label)) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const labels: Text[] = [] |
||||
|
|
||||
|
function getLabelPixelSize(fontSize, cameraZoom) { |
||||
|
const pixelRatio = renderer.getPixelRatio() |
||||
|
const referenceZoom = 1 |
||||
|
const referenceFontSize = 0.2 |
||||
|
const referencePixelSize = 16 // fontSize=0.2, zoom=1 时显示为 16px |
||||
|
|
||||
|
const scale = (fontSize / referenceFontSize) * (cameraZoom / referenceZoom) |
||||
|
return referencePixelSize * scale * pixelRatio |
||||
|
} |
||||
|
|
||||
|
function shouldShowLabel(label, minPixelSize = 700) { |
||||
|
const pixelSize = getLabelPixelSize(label.fontSize, camera.zoom) |
||||
|
return pixelSize >= minPixelSize |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* InstanceMesh(Point) + BufferGeometry + Label |
||||
|
*/ |
||||
|
function test3() { |
||||
|
cleanupThree() // 清空画布 |
||||
|
const xcount = 300 |
||||
|
const zcount = 100 |
||||
|
const dist = 1.25 |
||||
|
const spacing = dist |
||||
|
const y = 0.1 |
||||
|
|
||||
|
const noShaderMaterial = new THREE.MeshBasicMaterial({ |
||||
|
color: 0xFFFF99, |
||||
|
transparent: true, |
||||
|
depthWrite: false, |
||||
|
side: THREE.DoubleSide |
||||
|
}) |
||||
|
const planeGeometry = new THREE.PlaneGeometry(0.25, 0.25) |
||||
|
|
||||
|
// 使用 InstancedMesh 网格优化 |
||||
|
const instancedMesh = new THREE.InstancedMesh(planeGeometry, noShaderMaterial, zcount * xcount) |
||||
|
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) |
||||
|
const dummy = new THREE.Object3D() |
||||
|
|
||||
|
const points = [] |
||||
|
|
||||
|
// 创建所有点 |
||||
|
for (let z = 0; z < zcount; z++) { |
||||
|
for (let x = 0; x < xcount; x++) { |
||||
|
const px = x * dist |
||||
|
const pz = z * dist |
||||
|
|
||||
|
dummy.position.set(px, y, pz) |
||||
|
dummy.rotation.set(-Math.PI / 2, 0, 0) |
||||
|
dummy.updateMatrix() |
||||
|
instancedMesh.setMatrixAt(z * xcount + x, dummy.matrix) |
||||
|
|
||||
|
points.push(new THREE.Vector3(px, y, pz)) |
||||
|
} |
||||
|
} |
||||
|
scene.add(instancedMesh) |
||||
|
|
||||
|
const positions = [] |
||||
|
labels.length = 0 |
||||
|
|
||||
|
function createTextLabel(text, position): Text { |
||||
|
const label = new Text() |
||||
|
label.text = text |
||||
|
label.font = SimSunTTF |
||||
|
label.fontSize = 0.2 |
||||
|
label.color = '#333333' |
||||
|
label.opacity = 0.8 |
||||
|
label.padding = 0.2 |
||||
|
label.anchorX = 'center' |
||||
|
label.anchorY = 'middle' |
||||
|
label.depthOffset = 1 |
||||
|
label.backgroundColor = '#000000' |
||||
|
label.backgroundOpacity = 0.6 |
||||
|
label.material.depthTest = false |
||||
|
label.position.copy(position) |
||||
|
label.name = MeasureRenderer.LABEL_NAME |
||||
|
|
||||
|
label.position.set(position.x, position.y + 0.3, position.z - 0.2) |
||||
|
label.quaternion.copy(camera.quaternion) |
||||
|
label.visible = false |
||||
|
// label.sync() |
||||
|
|
||||
|
return label |
||||
|
} |
||||
|
|
||||
|
// 横向连接(右) |
||||
|
for (let z = 0; z < zcount; z++) { |
||||
|
for (let x = 0; x < xcount - 1; x++) { |
||||
|
const x1 = x * spacing |
||||
|
const z1 = z * spacing |
||||
|
const x2 = (x + 1) * spacing |
||||
|
const z2 = z * spacing |
||||
|
positions.push(x1, y, z1, x2, y, z2) |
||||
|
|
||||
|
// 计算中点和长度 |
||||
|
const midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2) |
||||
|
const length = Math.hypot(x2 - x1, y - y, z2 - z1) |
||||
|
const label = createTextLabel(length.toFixed(2) + 'm', midPoint) |
||||
|
labels.push(label) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 纵向连接(下) |
||||
|
for (let z = 0; z < zcount - 1; z++) { |
||||
|
for (let x = 0; x < xcount; x++) { |
||||
|
const x1 = x * spacing |
||||
|
const z1 = z * spacing |
||||
|
const x2 = x * spacing |
||||
|
const z2 = (z + 1) * spacing |
||||
|
positions.push(x1, y, z1, x2, y, z2) |
||||
|
|
||||
|
// 计算中点和长度 |
||||
|
const midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2) |
||||
|
const length = Math.hypot(x2 - x1, y - y, z2 - z1) |
||||
|
const label = createTextLabel(length.toFixed(2) + 'm', midPoint) |
||||
|
labels.push(label) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const positionNums = new Float32Array(positions) |
||||
|
const geometry = new THREE.BufferGeometry() |
||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(positionNums, 3)) |
||||
|
const material = new THREE.LineBasicMaterial({ color: 0xFF8C00 }) |
||||
|
const lineSegments = new THREE.LineSegments(geometry, material) |
||||
|
lineSegments.name = 'grid-lines' |
||||
|
scene.add(lineSegments) |
||||
|
|
||||
|
labels.forEach(label => scene.add(label)) |
||||
|
|
||||
|
// 统计总数量 |
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const restate = reactive({ |
||||
|
targetColor: '#ff0000', |
||||
|
loadScale: 1, |
||||
|
viewLabelCount: 0, |
||||
|
mode: 'translate', |
||||
|
objects: 0, |
||||
|
vertices: 0, |
||||
|
faces: 0 |
||||
|
}) |
||||
|
|
||||
|
// 状态变量 |
||||
|
const state = { |
||||
|
showAxesHelper: true, |
||||
|
showGridHelper: true, |
||||
|
camera: { |
||||
|
position: { x: 0, y: 5, z: 10 }, |
||||
|
rotation: { x: 0, y: 0, z: 0 } |
||||
|
} |
||||
|
} |
||||
|
onMounted(() => { |
||||
|
nextTick(() => { |
||||
|
initThree() |
||||
|
|
||||
|
const viewerDom = canvasContainer.value |
||||
|
if (resizeObserver) { |
||||
|
resizeObserver.unobserve(viewerDom) |
||||
|
} |
||||
|
resizeObserver = new ResizeObserver(handleResize) |
||||
|
resizeObserver.observe(viewerDom) |
||||
|
|
||||
|
window['cp'] = getCurrentInstance() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
onBeforeUnmount(() => { |
||||
|
if (animationFrameId !== null) { |
||||
|
cancelAnimationFrame(animationFrameId) |
||||
|
animationFrameId = null |
||||
|
} |
||||
|
|
||||
|
cleanupThree() |
||||
|
|
||||
|
const viewerDom = canvasContainer.value |
||||
|
if (resizeObserver) { |
||||
|
resizeObserver.unobserve(viewerDom) |
||||
|
} |
||||
|
|
||||
|
window['cp'] = null |
||||
|
}) |
||||
|
|
||||
|
function initThree() { |
||||
|
viewerDom = canvasContainer.value |
||||
|
if (!viewerDom) { |
||||
|
console.error('Viewer DOM element not found') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 场景 |
||||
|
scene = new THREE.Scene() |
||||
|
scene.background = new THREE.Color(0xeeeeee) |
||||
|
|
||||
|
// 渲染器 |
||||
|
renderer = new THREE.WebGLRenderer({ |
||||
|
antialias: true, |
||||
|
alpha: true, |
||||
|
powerPreference: 'high-performance' |
||||
|
}) |
||||
|
renderer.clearDepth() |
||||
|
renderer.setPixelRatio(window.devicePixelRatio) |
||||
|
renderer.setSize(viewerDom.clientWidth, viewerDom.clientHeight) |
||||
|
viewerDom.appendChild(renderer.domElement) |
||||
|
|
||||
|
// 摄像机 |
||||
|
// initMode3DCamera() |
||||
|
initMode2DCamera() |
||||
|
|
||||
|
// 辅助线 |
||||
|
axesHelper = new THREE.AxesHelper(5) |
||||
|
scene.add(axesHelper) |
||||
|
|
||||
|
gridHelper = new THREE.GridHelper(1000, 1000) |
||||
|
scene.add(gridHelper) |
||||
|
gridHelper.visible = false |
||||
|
|
||||
|
// 光照 |
||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5) |
||||
|
scene.add(ambientLight) |
||||
|
|
||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) |
||||
|
directionalLight.position.set(5, 5, 5).multiplyScalar(3) |
||||
|
directionalLight.castShadow = true |
||||
|
scene.add(directionalLight) |
||||
|
|
||||
|
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) |
||||
|
scene.add(hemisphereLight) |
||||
|
|
||||
|
|
||||
|
// 性能监控 |
||||
|
statsControls = new Stats() |
||||
|
statsControls.showPanel(0) |
||||
|
statsControls.dom.style.right = '0' |
||||
|
statsControls.dom.style.left = 'auto' |
||||
|
viewerDom.appendChild(statsControls.dom) |
||||
|
|
||||
|
renderer.setAnimationLoop(animate) |
||||
|
renderer.setClearColor(0x000000, 0) |
||||
|
|
||||
|
// animate() |
||||
|
} |
||||
|
|
||||
|
// 动画循环 |
||||
|
let frameCount = 0 |
||||
|
|
||||
|
function animate() { |
||||
|
// animationFrameId = requestAnimationFrame(animate) |
||||
|
renderView() |
||||
|
|
||||
|
if (frameCount++ % 60 === 0) { // 每 60 帧更新一次文本 |
||||
|
const frustum = new THREE.Frustum() |
||||
|
const cameraCopy = camera.clone() |
||||
|
|
||||
|
// 必须更新相机的世界矩阵 |
||||
|
cameraCopy.updateMatrixWorld() |
||||
|
|
||||
|
// 构造投影矩阵 |
||||
|
const projScreenMatrix = new THREE.Matrix4().multiplyMatrices( |
||||
|
cameraCopy.projectionMatrix, |
||||
|
cameraCopy.matrixWorldInverse |
||||
|
) |
||||
|
|
||||
|
// 设置视锥体 |
||||
|
frustum.setFromProjectionMatrix(projScreenMatrix) |
||||
|
|
||||
|
let viewLabelCount = 0 |
||||
|
labels.forEach((label: Text) => { |
||||
|
// label.quaternion.copy(camera.quaternion) // billboard 效果保持朝向相机 |
||||
|
const isvis = isLabelInView(label, frustum, camera.position) |
||||
|
if (isvis) { |
||||
|
viewLabelCount++ |
||||
|
} |
||||
|
if (isvis && label.visible === false) { |
||||
|
label.visible = true |
||||
|
label.sync() |
||||
|
} else if (!isvis && label.visible === true) { |
||||
|
label.visible = false |
||||
|
label.sync() |
||||
|
} |
||||
|
}) |
||||
|
restate.viewLabelCount = viewLabelCount |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleResize(entries) { |
||||
|
for (let entry of entries) { |
||||
|
const width = entry.contentRect.width |
||||
|
const height = entry.contentRect.height |
||||
|
|
||||
|
if (camera instanceof THREE.PerspectiveCamera) { |
||||
|
camera.aspect = width / height |
||||
|
camera.updateProjectionMatrix() |
||||
|
|
||||
|
} else if (camera instanceof THREE.OrthographicCamera) { |
||||
|
camera.left = width / -2 |
||||
|
camera.right = width / 2 |
||||
|
camera.top = height / 2 |
||||
|
camera.bottom = height / -2 |
||||
|
camera.updateProjectionMatrix() |
||||
|
} |
||||
|
renderer.setPixelRatio(window.devicePixelRatio) |
||||
|
renderer.setSize(width, height) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 初始化2D相机 |
||||
|
*/ |
||||
|
function initMode2DCamera() { |
||||
|
if (camera) { |
||||
|
scene.remove(camera) |
||||
|
} |
||||
|
|
||||
|
// ============================ 创建正交相机 |
||||
|
const cameraNew = new THREE.OrthographicCamera( |
||||
|
viewerDom.clientWidth / -2, |
||||
|
viewerDom.clientWidth / 2, |
||||
|
viewerDom.clientHeight / 2, |
||||
|
viewerDom.clientHeight / -2, |
||||
|
1, |
||||
|
500 |
||||
|
) |
||||
|
cameraNew.position.set(0, 60, 0) |
||||
|
cameraNew.lookAt(0, 0, 0) |
||||
|
cameraNew.zoom = 60 |
||||
|
camera = cameraNew |
||||
|
scene.add(camera) |
||||
|
|
||||
|
// ============================ 创建控制器 |
||||
|
const controlsNew = new OrbitControls( |
||||
|
camera, |
||||
|
renderer.domElement |
||||
|
) |
||||
|
controlsNew.enableDamping = false |
||||
|
controlsNew.enableZoom = true |
||||
|
controlsNew.enableRotate = false |
||||
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN } // 鼠标中键平移 |
||||
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
||||
|
controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件 |
||||
|
controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } |
||||
|
controlsNew.panSpeed = 1 |
||||
|
controlsNew.keyPanSpeed = 20 // normal 7 |
||||
|
controlsNew.minDistance = 0.1 |
||||
|
controlsNew.maxDistance = 1000 |
||||
|
controls = controlsNew |
||||
|
|
||||
|
cameraNew.updateProjectionMatrix() |
||||
|
} |
||||
|
|
||||
|
function initMode3DCamera() { |
||||
|
if (camera) { |
||||
|
scene.remove(camera) |
||||
|
} |
||||
|
|
||||
|
const viewerDom = canvasContainer.value |
||||
|
|
||||
|
// ============================ 创建透视相机 |
||||
|
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000) |
||||
|
cameraNew.position.set(5, 5, 5) |
||||
|
cameraNew.lookAt(0, 0, 0) |
||||
|
|
||||
|
camera = cameraNew |
||||
|
scene.add(camera) |
||||
|
|
||||
|
const controlsNew = new OrbitControls(camera, viewerDom) |
||||
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移 |
||||
|
controlsNew.enableDamping = false |
||||
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
||||
|
controlsNew.minDistance = 2 |
||||
|
controlsNew.addEventListener('change', syncCameraState) |
||||
|
controls = controlsNew |
||||
|
controls.update() |
||||
|
|
||||
|
camera.updateProjectionMatrix() |
||||
|
|
||||
|
syncCameraState() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重新加载相机状态到全局状态 |
||||
|
*/ |
||||
|
function syncCameraState() { |
||||
|
if (camera) { |
||||
|
state.camera.position.x = camera.position.x |
||||
|
state.camera.position.y = camera.position.y |
||||
|
state.camera.position.z = camera.position.z |
||||
|
|
||||
|
state.camera.rotation.x = camera.rotation.x |
||||
|
state.camera.rotation.y = camera.rotation.y |
||||
|
state.camera.rotation.z = camera.rotation.z |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function renderView() { |
||||
|
statsControls?.update() |
||||
|
renderer?.render(scene, camera) |
||||
|
} |
||||
|
|
||||
|
function refreshCount() { |
||||
|
// 遍历场景中的所有对象 |
||||
|
let totalObjects = 0 |
||||
|
let totalVertices = 0 |
||||
|
let totalFaces = 0 |
||||
|
scene.traverse(function(child) { |
||||
|
if (child.isMesh) { |
||||
|
totalObjects++ |
||||
|
|
||||
|
// 获取几何体 |
||||
|
const geometry = child.geometry |
||||
|
|
||||
|
// 如果几何体是 BufferGeometry 类型 |
||||
|
if (geometry.isBufferGeometry) { |
||||
|
// 计算顶点数 |
||||
|
if (geometry.attributes.position) { |
||||
|
totalVertices += geometry.attributes.position.count |
||||
|
} |
||||
|
|
||||
|
// 计算面数(假设每个面都是由三个顶点组成的三角形) |
||||
|
if (geometry.index) { |
||||
|
totalFaces += geometry.index.count / 3 |
||||
|
} else if (geometry.attributes.position) { |
||||
|
// 如果没有索引,计算非索引几何体的面数 |
||||
|
totalFaces += geometry.attributes.position.count / 3 |
||||
|
} |
||||
|
} |
||||
|
// 如果几何体是 Geometry 类型(较旧的版本使用) |
||||
|
else if (geometry.isGeometry) { |
||||
|
// 计算顶点数 |
||||
|
totalVertices += geometry.vertices.length |
||||
|
|
||||
|
// 计算面数 |
||||
|
totalFaces += geometry.faces.length |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
restate.objects = totalObjects |
||||
|
restate.vertices = totalVertices |
||||
|
restate.faces = totalFaces |
||||
|
} |
||||
|
|
||||
|
function cleanupThree() { |
||||
|
// 移除旧模型 |
||||
|
if (scene) { |
||||
|
scene.traverse((obj: THREE.Mesh) => { |
||||
|
// 释放几何体 |
||||
|
if (obj.geometry) { |
||||
|
obj.geometry.dispose() |
||||
|
} |
||||
|
|
||||
|
// 释放材质 |
||||
|
if (obj.material) { |
||||
|
if (Array.isArray(obj.material)) { |
||||
|
obj.material.forEach(m => m.dispose()) |
||||
|
} else { |
||||
|
obj.material.dispose() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 释放纹理 |
||||
|
if (obj.texture) { |
||||
|
obj.texture.dispose() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 清空场景 |
||||
|
scene.children = [] |
||||
|
modelGroup = null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
</script> |
||||
|
<style scoped lang="less"> |
||||
|
.model3d-view { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex-grow: 1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.main-content { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.model3d-content { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.canvas-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,130 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import { BasePlane } from '@/types/ModelTypes.ts' |
||||
|
|
||||
|
/** |
||||
|
* 创建一个矩形的 Shape |
||||
|
*/ |
||||
|
export function buildRectangle(width: number, height: number, padding: number[] | number = 0, offsetX = 0, offsetY = 0): THREE.Shape { |
||||
|
let pl = 0, |
||||
|
pt = 0, |
||||
|
pr = 0, |
||||
|
pb = 0 |
||||
|
|
||||
|
if (typeof padding == 'number') { |
||||
|
pl = padding |
||||
|
pt = padding |
||||
|
pr = padding |
||||
|
pb = padding |
||||
|
} else if (Array.isArray(padding)) { |
||||
|
if (padding.length == 1) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[0] |
||||
|
pr = padding[0] |
||||
|
pb = padding[0] |
||||
|
} else if (padding.length > 1 && padding.length < 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[0] |
||||
|
pb = padding[1] |
||||
|
} else if (padding.length >= 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[2] |
||||
|
pb = padding[3] |
||||
|
} |
||||
|
} |
||||
|
const shape = new THREE.Shape() |
||||
|
shape.moveTo(0 + pl + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, height - pt + offsetY) |
||||
|
shape.lineTo(0 + pl + offsetX, height - pt + offsetY) |
||||
|
shape.closePath() |
||||
|
return shape |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 根据自定义的 Shape,通过放样得到一个实体。默认实体的样式是在 front 面放样的。 |
||||
|
* |
||||
|
* @param {*} shape 自定义的型状 |
||||
|
* @param {*} depth 放样深度(放样后的物体厚度) |
||||
|
* @param {*} basePlane 放样的基准面。 |
||||
|
* @param {*} x 目标定位x |
||||
|
* @param {*} y 目标定位y |
||||
|
* @param {*} z 目标定位z |
||||
|
* @param {*} euler 旋转到目标位 |
||||
|
* @param {*} anchor 锚点 |
||||
|
* @returns THREE.ExtrudeGeometry |
||||
|
*/ |
||||
|
export function createExtrudeItem(shape: THREE.Shape, depth: number, basePlane: number, |
||||
|
x = 0, y = 0, z = 0, euler = null, |
||||
|
anchor = new THREE.Vector3(0, 0, 0)) { |
||||
|
const geometry = new THREE.ExtrudeGeometry(shape, { |
||||
|
steps: 1, |
||||
|
depth: -depth, |
||||
|
bevelEnabled: false, |
||||
|
bevelThickness: 0, |
||||
|
bevelSize: 0, |
||||
|
bevelOffset: 0, |
||||
|
bevelSegments: 0 |
||||
|
}) |
||||
|
|
||||
|
geometry.center() |
||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3()) |
||||
|
|
||||
|
let dx, dy, dz |
||||
|
|
||||
|
switch (basePlane) { |
||||
|
case BasePlane.LEFT: |
||||
|
geometry.rotateY(Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.RIGHT: |
||||
|
geometry.rotateY(-Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.BEHIND: |
||||
|
geometry.translate(0, 0, size.z) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
case BasePlane.TOP: |
||||
|
geometry.rotateZ(Math.PI) |
||||
|
geometry.rotateX(Math.PI / 2) |
||||
|
geometry.translate(size.x, -size.z, 0) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
case BasePlane.BOTTOM: |
||||
|
geometry.rotateX(-Math.PI / 2) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
default: |
||||
|
//BasePlane.FRONT:
|
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if (euler != null && euler.isEuler) { |
||||
|
// 注意,需要先旋转,再平移。
|
||||
|
geometry.rotateX(euler.x) |
||||
|
geometry.rotateY(euler.y) |
||||
|
geometry.rotateZ(euler.z) |
||||
|
} |
||||
|
|
||||
|
geometry.translate(dx + x + anchor.x, dy + y + anchor.y, -dz + z + anchor.z) |
||||
|
geometry.center() |
||||
|
return geometry |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -1,33 +0,0 @@ |
|||||
import type { ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts' |
|
||||
|
|
||||
/** |
|
||||
* "点"对象类型的,基础元数据 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_POINT: ItemTypeMeta = [ |
|
||||
{ field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true }, |
|
||||
{ field: 'name', editor: 'TextInput', label: '名称' }, |
|
||||
{ field: 'userData.label', editor: 'TextInput', label: '标签' }, |
|
||||
{ editor: 'Transform' }, |
|
||||
{ field: 'color', editor: 'Color', label: '颜色' }, |
|
||||
{ editor: '-' }, |
|
||||
{ editor: 'IN_OUT_CENTER' } |
|
||||
] |
|
||||
|
|
||||
/** |
|
||||
* "物流运输单元"对象类型的,基础元数据, 排在后面的 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_POINT2: ItemTypeMeta = [ |
|
||||
{ field: 'userData.selectable', editor: 'Switch', label: '可选中' }, |
|
||||
{ field: 'userData.protected', editor: 'Switch', label: '受保护' }, |
|
||||
{ field: 'visible', editor: 'Switch', label: '可见' } |
|
||||
] |
|
||||
|
|
||||
/** |
|
||||
* "线"对象类型的,基础元数据 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_LINE: ItemTypeMeta = [] |
|
||||
|
|
||||
/** |
|
||||
* "线"对象类型的,基础元数据, 排在后面的 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_LINE2: ItemTypeMeta = [] |
|
||||
@ -0,0 +1,74 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import type Viewport from '@/core/engine/Viewport.ts' |
||||
|
|
||||
|
export default class InstanceMeshBlock { |
||||
|
public readonly name: string |
||||
|
public readonly blockIndex: number |
||||
|
public readonly viewport: Viewport |
||||
|
public readonly instancedMesh: THREE.InstancedMesh |
||||
|
public readonly freeIndices: number[] = [] |
||||
|
|
||||
|
// instanceId -> itemId
|
||||
|
public readonly __indexIdMap = new Map<number, string>() |
||||
|
|
||||
|
getFreeMeshIndex(): number { |
||||
|
if (this.freeIndices.length === 0) { |
||||
|
return -1 // No free index available
|
||||
|
} |
||||
|
return this.freeIndices.pop() // Return the last free index
|
||||
|
} |
||||
|
|
||||
|
constructor(itemTypeName: string, allowSelect: boolean, allowDrag: boolean, viewport: Viewport, |
||||
|
geometry: THREE.BufferGeometry, material: THREE.Material, |
||||
|
blockIndex: number, capacity: number) { |
||||
|
this.name = itemTypeName |
||||
|
this.blockIndex = blockIndex |
||||
|
this.viewport = viewport |
||||
|
this.instancedMesh = new THREE.InstancedMesh(geometry, material, capacity) |
||||
|
this.instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) |
||||
|
|
||||
|
console.log('createBlock: ' + itemTypeName + '[' + blockIndex + '] capacity:', capacity) |
||||
|
viewport.scene.add(this.instancedMesh) |
||||
|
if (allowSelect) { |
||||
|
this.viewport.entityManager._selectableObjects.push(this.instancedMesh) |
||||
|
} |
||||
|
if (allowDrag) { |
||||
|
this.viewport.entityManager._draggableObjects.push(this.instancedMesh) |
||||
|
} |
||||
|
|
||||
|
this.instancedMesh.userData.t = itemTypeName |
||||
|
this.instancedMesh.userData.entityId = 'InstanceMeshBlock_' + itemTypeName + '_' + blockIndex |
||||
|
_.extend(this.instancedMesh.userData,{ |
||||
|
t: itemTypeName, |
||||
|
blockIndex: blockIndex |
||||
|
}) |
||||
|
|
||||
|
const dummy = new THREE.Object3D() |
||||
|
dummy.scale.set(0, 0, 0) |
||||
|
dummy.updateMatrix() |
||||
|
|
||||
|
for (let i = 0; i < capacity; i++) { |
||||
|
this.instancedMesh.setMatrixAt(i, dummy.matrix) |
||||
|
this.freeIndices.push(i) |
||||
|
} |
||||
|
|
||||
|
this.instancedMesh.instanceMatrix.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
dispose() { |
||||
|
this.viewport.scene.remove(this.instancedMesh) |
||||
|
this.instancedMesh.geometry.dispose() |
||||
|
|
||||
|
if (this.instancedMesh.material) { |
||||
|
if (Array.isArray(this.instancedMesh.material)) { |
||||
|
this.instancedMesh.material.forEach(mat => mat.dispose()) |
||||
|
} else { |
||||
|
this.instancedMesh.material.dispose() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.instancedMesh.dispose() |
||||
|
this.__indexIdMap.clear() |
||||
|
this.freeIndices.length = 0 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,181 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import type Viewport from '@/core/engine/Viewport.ts' |
||||
|
import InstanceMeshBlock from '@/core/manager/InstanceMeshBlock.ts' |
||||
|
import { PointManageWrap } from '@/core/manager/InstancePointManager.ts' |
||||
|
import { Matrix4 } from 'three/src/math/Matrix4' |
||||
|
|
||||
|
export default class InstanceMeshManager { |
||||
|
private __uuidMap = new Map<string, InstanceMeshWrap>() |
||||
|
|
||||
|
public readonly name: string |
||||
|
public readonly viewport: Viewport |
||||
|
public readonly allowSelect: boolean |
||||
|
public readonly allowDrag: boolean |
||||
|
public readonly blockCapacity: number |
||||
|
|
||||
|
public readonly blocks: InstanceMeshBlock[] = [] |
||||
|
private readonly geometry: THREE.BufferGeometry |
||||
|
private readonly material: THREE.Material |
||||
|
|
||||
|
private readonly dummy: THREE.Object3D = new THREE.Object3D() |
||||
|
|
||||
|
constructor(name: string, viewport: Viewport, |
||||
|
geometry: THREE.BufferGeometry, material: THREE.Material, |
||||
|
allowSelect: boolean, allowDrag: boolean, blockCapacity: number = 1000) { |
||||
|
this.name = name |
||||
|
this.viewport = viewport |
||||
|
this.allowSelect = allowSelect |
||||
|
this.allowDrag = allowDrag |
||||
|
this.geometry = geometry |
||||
|
this.material = material |
||||
|
this.blockCapacity = blockCapacity |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取点实例数据 |
||||
|
*/ |
||||
|
findByMeshInstanceId(blockIndex: number, instanceId: number): InstanceMeshWrap { |
||||
|
if (!this.blocks[blockIndex]) { |
||||
|
console.error('InstancePointManager: Invalid blockIndex', blockIndex) |
||||
|
return null |
||||
|
} |
||||
|
const uuid = this.blocks[blockIndex].__indexIdMap.get(instanceId) |
||||
|
if (!uuid) return |
||||
|
return this.__uuidMap.get(uuid) |
||||
|
} |
||||
|
|
||||
|
create(entityId: string, userData?: any): InstanceMeshWrap { |
||||
|
let meshIndex = -1 |
||||
|
let blockIndex = -1 |
||||
|
for (const block of this.blocks) { |
||||
|
meshIndex = block.getFreeMeshIndex() |
||||
|
if (meshIndex >= 0) { |
||||
|
blockIndex = block.blockIndex |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
// 所有 block 都没有空闲索引,创建新的 block
|
||||
|
if (meshIndex < 0) { |
||||
|
const block = this.createBlock() |
||||
|
meshIndex = block.getFreeMeshIndex() |
||||
|
blockIndex = block.blockIndex |
||||
|
} |
||||
|
if (meshIndex < 0) { |
||||
|
system.showErrorDialog('InstancePointManager: No free index available after creating new block') |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
return new InstanceMeshWrap(entityId, this, blockIndex, meshIndex, userData) |
||||
|
} |
||||
|
|
||||
|
delete(wrapOrId: InstanceMeshWrap | string) { |
||||
|
let wrap: InstanceMeshWrap |
||||
|
if (typeof wrapOrId === 'string') { |
||||
|
wrap = this.__uuidMap.get(wrapOrId) |
||||
|
if (!wrap) return |
||||
|
|
||||
|
} else { |
||||
|
wrap = wrapOrId |
||||
|
if (!wrap || !this.__uuidMap.has(wrap.uuid)) { |
||||
|
console.warn(`InstanceMeshManager: Wrap ${wrap.uuid} not found`) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const block = this.blocks[wrap.blockIndex] |
||||
|
if (!block) { |
||||
|
console.warn(`InstanceMeshManager: Block ${wrap.blockIndex} not found for wrap ${wrap.uuid}`) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 隐藏实例
|
||||
|
this.dummy.scale.set(0, 0, 0) |
||||
|
this.dummy.updateMatrix() |
||||
|
block.instancedMesh.setMatrixAt(wrap.meshIndex, this.dummy.matrix) |
||||
|
block.instancedMesh.instanceMatrix.needsUpdate = true |
||||
|
|
||||
|
// 回收索引
|
||||
|
block.freeIndices.push(wrap.meshIndex) |
||||
|
this.__uuidMap.delete(wrap.uuid) |
||||
|
block.__indexIdMap.delete(wrap.meshIndex) |
||||
|
|
||||
|
wrap.dispose() |
||||
|
} |
||||
|
|
||||
|
// 创建新的 InstanceMeshBlock
|
||||
|
createBlock(): InstanceMeshBlock { |
||||
|
const blockIndex = this.blocks.length |
||||
|
const block = new InstanceMeshBlock(this.name, this.allowSelect, this.allowDrag, |
||||
|
this.viewport, this.geometry, this.material, |
||||
|
blockIndex, this.blockCapacity) |
||||
|
this.blocks.push(block) |
||||
|
return block |
||||
|
} |
||||
|
|
||||
|
setBlockMatrixAt(wrap: InstanceMeshWrap, matrix: THREE.Matrix4) { |
||||
|
const block = this.blocks[wrap.blockIndex] |
||||
|
if (!block) { |
||||
|
console.warn(`InstanceMeshManager: Block ${wrap.blockIndex} not found!`) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!block.__indexIdMap.has(wrap.meshIndex)) { |
||||
|
this.__uuidMap.set(wrap.uuid, wrap) |
||||
|
wrap.parent = block.instancedMesh |
||||
|
} |
||||
|
block.instancedMesh.setMatrixAt(wrap.meshIndex, matrix) |
||||
|
block.instancedMesh.instanceMatrix.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
dispose() { |
||||
|
for (const block of this.blocks) { |
||||
|
block.dispose() |
||||
|
} |
||||
|
this.blocks.length = 0 // 清空 blocks 数组
|
||||
|
this.geometry.dispose() // 释放几何体资源
|
||||
|
this.material.dispose() // 释放材质资源
|
||||
|
console.log(`InstanceMeshManager ${this.name} disposed.`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class InstanceMeshWrap { |
||||
|
readonly entityId: string |
||||
|
readonly manager: InstanceMeshManager |
||||
|
readonly blockIndex: number |
||||
|
readonly meshIndex: number |
||||
|
//@ts-ignore
|
||||
|
userData: UserData = {} |
||||
|
|
||||
|
uuid: string |
||||
|
parent: THREE.Object3D | null = null |
||||
|
name: string |
||||
|
visible: boolean |
||||
|
|
||||
|
get position(): THREE.Vector3 { |
||||
|
const m = new THREE.Matrix4() |
||||
|
this.manager.blocks[this.blockIndex].instancedMesh.getMatrixAt(this.meshIndex, m) |
||||
|
|
||||
|
const pos = new THREE.Vector3() |
||||
|
m.decompose(pos, new THREE.Quaternion(), new THREE.Vector3()) |
||||
|
return pos |
||||
|
} |
||||
|
|
||||
|
constructor(entityId: string, manager: InstanceMeshManager, blockIndex: number, meshIndex: number, userData?: any) { |
||||
|
this.uuid = system.createUUID() |
||||
|
this.entityId = entityId |
||||
|
this.manager = manager |
||||
|
this.meshIndex = meshIndex |
||||
|
this.blockIndex = blockIndex |
||||
|
this.userData = userData |
||||
|
} |
||||
|
|
||||
|
setMatrix4(matrix: THREE.Matrix4): InstanceMeshWrap { |
||||
|
this.manager.setBlockMatrixAt(this, matrix) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
dispose() { |
||||
|
this.manager.delete(this) |
||||
|
this.parent = null |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { reactive } from "vue"; |
||||
|
import DataForm from "@/components/data-form/DataForm.vue"; |
||||
|
import type { FormField } from "@/components/data-form/DataFormTypes.ts"; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'BulkCopy', |
||||
|
}); |
||||
|
|
||||
|
interface BulkCopyConfig { |
||||
|
/** 行数 */ |
||||
|
numberOfRows?: number; |
||||
|
/** 列数 */ |
||||
|
numberOfColumns?: number; |
||||
|
/** 行间距 */ |
||||
|
rowSpace?: number; |
||||
|
/** 列间距 */ |
||||
|
columnSpacing?: number; |
||||
|
/** 双排 */ |
||||
|
doubleRow?: boolean; |
||||
|
} |
||||
|
|
||||
|
// 定义 Props 类型 |
||||
|
interface BulkCopyProps { |
||||
|
config: BulkCopyConfig; |
||||
|
} |
||||
|
|
||||
|
// 读取组件 props 属性 |
||||
|
const props = withDefaults(defineProps<BulkCopyProps>(), {}); |
||||
|
|
||||
|
// 定义 State 类型 |
||||
|
interface BulkCopyState { |
||||
|
} |
||||
|
|
||||
|
// state 属性 |
||||
|
const state = reactive<BulkCopyState>({}); |
||||
|
|
||||
|
// 定义 Data 类型 |
||||
|
interface BulkCopyData { |
||||
|
formFields?: Array<FormField>; |
||||
|
} |
||||
|
|
||||
|
// 内部数据 |
||||
|
const data: BulkCopyData = { |
||||
|
formFields: [ |
||||
|
{ |
||||
|
dataPath: 'numberOfRows', label: '行数', input: 'InputNumber', |
||||
|
inputProps: { |
||||
|
placeholder: '请输入', |
||||
|
controlsPosition: 'right', |
||||
|
min: 1, |
||||
|
precision: 0, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'numberOfColumns', label: '列数', input: 'InputNumber', |
||||
|
inputProps: { |
||||
|
placeholder: '请输入', |
||||
|
controlsPosition: 'right', |
||||
|
min: 1, |
||||
|
precision: 0, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'rowSpace', label: '行间距', input: 'InputNumber', |
||||
|
inputProps: { |
||||
|
placeholder: '请输入', |
||||
|
controlsPosition: 'right', |
||||
|
min: 0, |
||||
|
// precision: |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'columnSpacing', label: '列间距', input: 'InputNumber', |
||||
|
inputProps: { |
||||
|
placeholder: '请输入', |
||||
|
controlsPosition: 'right', |
||||
|
min: 0, |
||||
|
// precision: |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'doubleRow', label: '单双排', input: 'Switch', |
||||
|
inputProps: { |
||||
|
// trueValue: true, |
||||
|
// falseValue: false, |
||||
|
inlinePrompt: true, |
||||
|
inactiveText: "单排", |
||||
|
activeText: "双排", |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
interface BulkCopyExpose { |
||||
|
state: BulkCopyState; |
||||
|
data: BulkCopyData; |
||||
|
} |
||||
|
|
||||
|
const expose: BulkCopyExpose = { |
||||
|
state, |
||||
|
data, |
||||
|
}; |
||||
|
// 定义组件公开内容 |
||||
|
defineExpose(expose); |
||||
|
|
||||
|
export type { |
||||
|
BulkCopyConfig, |
||||
|
BulkCopyProps, |
||||
|
BulkCopyState, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<DataForm |
||||
|
:data="props.config" |
||||
|
:formFields="data.formFields" |
||||
|
:columnCount="2" |
||||
|
layout="onlyLabelFixed" |
||||
|
labelWidth="80px" |
||||
|
inputWidth="" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
|
||||
|
</style> |
||||
@ -1,77 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import type WorldModel from '@/model/WorldModel.ts' |
|
||||
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' |
|
||||
import type { ItemJson } from '@/model/WorldModelType.ts' |
|
||||
import type Viewport from '@/designer/Viewport.ts' |
|
||||
import type Toolbox from '@/model/itemType/Toolbox.ts' |
|
||||
|
|
||||
export default abstract class ItemType { |
|
||||
|
|
||||
/** |
|
||||
* 所有点的数组 |
|
||||
*/ |
|
||||
pointArray: THREE.Object3D[] = [] |
|
||||
|
|
||||
name: string |
|
||||
option: ItemTypeDefineOption |
|
||||
worldModel: WorldModel |
|
||||
|
|
||||
public init(worldModel: WorldModel) { |
|
||||
this.worldModel = worldModel |
|
||||
|
|
||||
// 初始化方法,子类可以重写
|
|
||||
return Promise.resolve() |
|
||||
} |
|
||||
|
|
||||
abstract loadFromJson(item: ItemJson): undefined | THREE.Object3D |
|
||||
|
|
||||
abstract createToolbox(viewport: Viewport): Toolbox |
|
||||
|
|
||||
abstract getDefaultScale(): THREE.Vector3 |
|
||||
|
|
||||
abstract getDefaultRotation(): THREE.Vector3 |
|
||||
|
|
||||
abstract createPoint(position: THREE.Vector3, itemJson: ItemJson): THREE.Object3D |
|
||||
|
|
||||
beforeLoad(): THREE.Object3D[] { |
|
||||
return [] |
|
||||
} |
|
||||
|
|
||||
afterLoadPoint(point: THREE.Object3D): void { |
|
||||
} |
|
||||
|
|
||||
afterUpdatePoint(point: THREE.Object3D, updatedLines: THREE.Object3D[]): void { |
|
||||
} |
|
||||
|
|
||||
afterLoadGroup(group: THREE.Group): void { |
|
||||
} |
|
||||
|
|
||||
afterLoadComplete(objects: THREE.Object3D[]): THREE.Object3D[] { |
|
||||
return [] |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 添加到 scene 后的回调 |
|
||||
*/ |
|
||||
afterAddScene(viewport: Viewport, scene: THREE.Scene, objects: THREE.Object3D[]): void { |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 添加到 viewport 后的回调 |
|
||||
*/ |
|
||||
afterAddViewport(viewport: Viewport): void { |
|
||||
//viewport.dragControl.setDragObjects(this.pointArray, 'push')
|
|
||||
const toolbox = this.createToolbox(viewport) |
|
||||
viewport.toolbox[this.name] = toolbox |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* EsDragControls 开始拖拽某个点时的回调 |
|
||||
*/ |
|
||||
abstract dragPointStart(viewport: Viewport, point: THREE.Mesh) |
|
||||
|
|
||||
/** |
|
||||
* EsDragControls 拖拽完成(放开鼠标)时的回调 |
|
||||
*/ |
|
||||
abstract dragPointComplete(viewport: Viewport) |
|
||||
} |
|
||||
@ -1,106 +0,0 @@ |
|||||
import _ from 'lodash' |
|
||||
import type ItemType from '@/model/itemType/ItemType.ts' |
|
||||
import * as THREE from 'three' |
|
||||
|
|
||||
const itemTypes: Record<string, ItemTypeDefineOption> = {} |
|
||||
window['itemTypes'] = itemTypes |
|
||||
|
|
||||
/** |
|
||||
* 定义一个 物流单元 |
|
||||
*/ |
|
||||
export function defineItemType(option: ItemTypeDefineOption) { |
|
||||
itemTypes[option.name] = option |
|
||||
option.clazz.name = option.name |
|
||||
option.clazz.option = option |
|
||||
return option |
|
||||
} |
|
||||
|
|
||||
export function getItemTypeByName(type: string): ItemTypeDefineOption { |
|
||||
const itemType = _.get(itemTypes, type) |
|
||||
if (!itemType) { |
|
||||
console.warn(`未找到物流单元类型定义: ${type}`) |
|
||||
return |
|
||||
} |
|
||||
return itemType |
|
||||
} |
|
||||
|
|
||||
export function getAllItemTypes(): ItemTypeDefineOption[] { |
|
||||
return Object.values(itemTypes) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* "点"对象类型的,基础元数据 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_POINT: ItemTypeMeta = [ |
|
||||
{ field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true }, |
|
||||
{ field: 'name', editor: 'TextInput', label: '名称' }, |
|
||||
{ field: 'userData.label', editor: 'TextInput', label: '标签' }, |
|
||||
{ editor: 'Transform' }, |
|
||||
{ field: 'color', editor: 'Color', label: '颜色' }, |
|
||||
{ editor: '-' }, |
|
||||
{ editor: 'IN_OUT_CENTER' } |
|
||||
] |
|
||||
|
|
||||
/** |
|
||||
* "物流运输单元"对象类型的,基础元数据, 排在后面的 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_POINT2: ItemTypeMeta = [ |
|
||||
{ field: 'userData.selectable', editor: 'Switch', label: '可选中' }, |
|
||||
{ field: 'userData.protected', editor: 'Switch', label: '受保护' }, |
|
||||
{ field: 'visible', editor: 'Switch', label: '可见' } |
|
||||
] |
|
||||
|
|
||||
/** |
|
||||
* "线"对象类型的,基础元数据 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_LINE: ItemTypeMeta = [] |
|
||||
|
|
||||
/** |
|
||||
* "线"对象类型的,基础元数据, 排在后面的 |
|
||||
*/ |
|
||||
export const BASIC_META_OF_LINE2: ItemTypeMeta = [] |
|
||||
|
|
||||
|
|
||||
export type ActionType = |
|
||||
/** |
|
||||
* 线类型 |
|
||||
*/ |
|
||||
'ln' | |
|
||||
/** |
|
||||
* 点类型 |
|
||||
*/ |
|
||||
'pt' | |
|
||||
/** |
|
||||
* 物流运输单元 |
|
||||
*/ |
|
||||
'fl' | |
|
||||
/** |
|
||||
* 分组单元,仅用于分组 |
|
||||
*/ |
|
||||
'gp' |
|
||||
|
|
||||
export interface ItemTypeDefineOption { |
|
||||
name: string |
|
||||
label: string |
|
||||
actionType: ActionType |
|
||||
clazz: ItemType |
|
||||
|
|
||||
getMeta(object: THREE.Object3D): ItemTypeMeta |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 单元类型元数据 |
|
||||
*/ |
|
||||
export type ItemTypeMeta = ItemTypeMetaItem[] |
|
||||
|
|
||||
export type ItemTypeMetaItem = { |
|
||||
field?: string |
|
||||
label?: string |
|
||||
labelWidth?: number |
|
||||
readonly?: boolean |
|
||||
editor: '-' | |
|
||||
'TextInput' | 'Number' | 'Switch' | 'Select' | 'ButtonGroup' | |
|
||||
'UUID' | 'Color' | 'Transform' | 'IN_OUT_CENTER' |
|
||||
} |
|
||||
@ -1,256 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import ItemType from '@/model/itemType/ItemType.ts' |
|
||||
import type { ItemJson } from '@/model/WorldModelType.ts' |
|
||||
import type WorldModel from '@/model/WorldModel.ts' |
|
||||
import type Viewport from '@/designer/Viewport.ts' |
|
||||
import { findObject3DByCondition, findObject3DById } from '@/model/ModelUtils.ts' |
|
||||
|
|
||||
let pmFn |
|
||||
|
|
||||
/** |
|
||||
* ILineType 接口定义了线类型的基本方法 |
|
||||
* 用于创建点和线, 以及处理拖拽事件, 以及交互事件 |
|
||||
*/ |
|
||||
export default abstract class ItemTypeLine extends ItemType { |
|
||||
private relationPoints: THREE.Mesh[] = [] |
|
||||
private dragViewport: Viewport | undefined |
|
||||
private dragPoint: THREE.Mesh | undefined |
|
||||
|
|
||||
abstract createPointBasic(position: THREE.Vector3): THREE.Object3D |
|
||||
|
|
||||
abstract createLineBasic(isTemplate?: boolean): THREE.Mesh |
|
||||
|
|
||||
public init(worldModel: WorldModel) { |
|
||||
return super.init(worldModel).then(() => { |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
afterCreateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { |
|
||||
} |
|
||||
|
|
||||
afterUpdateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { |
|
||||
} |
|
||||
|
|
||||
createLine(viewport: Viewport, scene: THREE.Scene, startPoint: THREE.Object3D, endPoint: THREE.Object3D): THREE.Mesh { |
|
||||
const line = this.createLineBasic() |
|
||||
const geom = line.geometry |
|
||||
geom.setFromPoints([startPoint.position, endPoint.position]) |
|
||||
|
|
||||
if (!line.userData) { |
|
||||
line.userData = {} |
|
||||
} |
|
||||
line.userData.lineStartId = startPoint.uuid |
|
||||
line.userData.lineEndId = endPoint.uuid |
|
||||
|
|
||||
if (startPoint.parent) { |
|
||||
startPoint.parent.add(line) |
|
||||
} else { |
|
||||
scene.add(line) |
|
||||
} |
|
||||
|
|
||||
if (!startPoint.userData.lines) { |
|
||||
startPoint.userData.lines = [] |
|
||||
} |
|
||||
startPoint.userData.lines.push(line.uuid) |
|
||||
|
|
||||
if (!endPoint.userData.lines) { |
|
||||
endPoint.userData.lines = [] |
|
||||
} |
|
||||
endPoint.userData.lines.push(line.uuid) |
|
||||
|
|
||||
this.afterCreateLine(line, startPoint, endPoint) |
|
||||
//@ts-ignore
|
|
||||
if (typeof line.computeLineDistances === 'function') { |
|
||||
// const canvas = viewport.renderer.domElement
|
|
||||
|
|
||||
//@ts-ignore
|
|
||||
// this.lineMaterial.resolution.set(canvas.width, canvas.height)
|
|
||||
|
|
||||
//@ts-ignore
|
|
||||
line.computeLineDistances() |
|
||||
} |
|
||||
return line |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 所有点数据加载完成后,添加进场景之后,需要根据 center 数组创建连接线 |
|
||||
*/ |
|
||||
afterAddScene(viewport: Viewport, scene: THREE.Scene, objects: THREE.Object3D[]) { |
|
||||
super.afterAddScene(viewport, scene, objects) |
|
||||
|
|
||||
// 为所有的 pointArray 连接线
|
|
||||
for (let i = 0; i < this.pointArray.length; i++) { |
|
||||
const startPoint = this.pointArray[i] |
|
||||
|
|
||||
// 找到这个元素的 userData.center 数组
|
|
||||
const linkArray: string[] = startPoint.userData.center || [] |
|
||||
|
|
||||
for (let j = 0; j < linkArray.length; j++) { |
|
||||
const linkId = linkArray[j] |
|
||||
// 在 pointArray 中查找对应的点
|
|
||||
const endPoint = findObject3DById(scene, linkId) |
|
||||
if (!endPoint) { |
|
||||
console.warn('not found link point uuid=${}', linkId) |
|
||||
continue |
|
||||
} |
|
||||
|
|
||||
const line = this.createLine(viewport, scene, startPoint, endPoint) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 读取地图数据, 创建点单元 |
|
||||
*/ |
|
||||
createPoint(position: THREE.Vector3, item: ItemJson): THREE.Object3D { |
|
||||
const point = this.createPointBasic(position) |
|
||||
if (item.name) { |
|
||||
point.name = item.name |
|
||||
} |
|
||||
point.uuid = item.id || THREE.MathUtils.generateUUID() |
|
||||
point.userData = _.cloneDeep(item.dt) || {} |
|
||||
_.extend(point.userData, { |
|
||||
type: item.t, |
|
||||
actionType: item.a, |
|
||||
label: item.l, |
|
||||
color: item.c, |
|
||||
selectable: true, |
|
||||
protected: false |
|
||||
}) |
|
||||
|
|
||||
point.rotation.set( |
|
||||
THREE.MathUtils.degToRad(item.tf[1][0]), |
|
||||
THREE.MathUtils.degToRad(item.tf[1][1]), |
|
||||
THREE.MathUtils.degToRad(item.tf[1][2]) |
|
||||
) |
|
||||
|
|
||||
point.scale.set(item.tf[2][0], item.tf[2][1], item.tf[2][2]) |
|
||||
this.pointArray.push(point) |
|
||||
|
|
||||
this.afterLoadPoint(point) |
|
||||
return point |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 从 Json 某个 item 加载数据 |
|
||||
*/ |
|
||||
override loadFromJson(item: ItemJson): undefined | THREE.Object3D { |
|
||||
if (item.a === 'gp') { |
|
||||
// gp 是为了分组而存在的
|
|
||||
const group = new THREE.Group() |
|
||||
group.name = item.name |
|
||||
group.uuid = item.id || THREE.MathUtils.generateUUID() |
|
||||
group.userData = _.cloneDeep(item.dt) || {} |
|
||||
group.userData.type = item.t |
|
||||
group.userData.actionType = item.a |
|
||||
group.userData.label = item.l |
|
||||
group.userData.color = item.c |
|
||||
|
|
||||
this.afterLoadGroup(group) |
|
||||
return group |
|
||||
} |
|
||||
|
|
||||
// 其他情况都是 ln
|
|
||||
else if (item.a === 'ln') { |
|
||||
const position = new THREE.Vector3( |
|
||||
item.tf[0][0], |
|
||||
item.tf[0][1], |
|
||||
item.tf[0][2] |
|
||||
) |
|
||||
|
|
||||
return this.createPoint(position, item) |
|
||||
} |
|
||||
|
|
||||
console.error('ItemTypeLineBase.loadFromJson: Unsupported', item) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 被 DragControls 拖拽时触发这个方法 |
|
||||
*/ |
|
||||
dragPointStart(viewport: Viewport, point: THREE.Mesh) { |
|
||||
console.log('dragPoint:', point) |
|
||||
this.dragViewport = viewport |
|
||||
this.dragPoint = point |
|
||||
const canvas = viewport.renderer.domElement |
|
||||
|
|
||||
// 收集相关联的线段和标签
|
|
||||
const relationPoints = new Set<THREE.Object3D>() |
|
||||
|
|
||||
// 找到与 point 有关联的节点, 无论是 a->b 还是 b->a 都需要收集
|
|
||||
findObject3DByCondition(viewport.scene, (targetObj: THREE.Object3D) => { |
|
||||
if (targetObj.uuid === point.uuid) { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 无论 a->b 还是 b->a 都需要收集
|
|
||||
if (_.includes(targetObj.userData?.center, point.uuid) || _.includes(point.userData?.center, targetObj.uuid)) { |
|
||||
relationPoints.add(targetObj) |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
//@ts-ignore
|
|
||||
this.relationPoints = Array.from(relationPoints) |
|
||||
|
|
||||
// 监听move事件
|
|
||||
pmFn = this.redrawMousemove.bind(this) |
|
||||
canvas.addEventListener('pointermove', pmFn) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* DragControls 拖拽过程中鼠标移动时触发这个方法 |
|
||||
*/ |
|
||||
redrawMousemove(e: MouseEvent) { |
|
||||
if (!this.dragViewport || !this.dragPoint) return |
|
||||
|
|
||||
const updateLines: THREE.Object3D[] = [] |
|
||||
// 更新所有相关线段
|
|
||||
_.forEach(this.relationPoints, (targetPoint, idx) => { |
|
||||
if (targetPoint.uuid === this.dragPoint.uuid) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 判断谁是起点,谁是终点
|
|
||||
let startPoint: THREE.Object3D |
|
||||
let endPoint: THREE.Object3D |
|
||||
if (_.includes(targetPoint.userData?.center, this.dragPoint.uuid)) { |
|
||||
startPoint = targetPoint |
|
||||
endPoint = this.dragPoint |
|
||||
|
|
||||
} else { |
|
||||
startPoint = this.dragPoint |
|
||||
endPoint = targetPoint |
|
||||
} |
|
||||
|
|
||||
// 找到 startPoint 与 this.dragPoint 之间的线段
|
|
||||
const line = findObject3DByCondition(this.dragViewport.scene, (obj) => { |
|
||||
return obj.userData.lineStartId === startPoint.uuid && obj.userData.lineEndId === endPoint.uuid |
|
||||
})[0] as THREE.Mesh |
|
||||
|
|
||||
if (!line) { |
|
||||
// line = this.createLine(this.dragViewport.scene, startPoint, endPoint)
|
|
||||
console.warn('Line not found between points:', startPoint.uuid, endPoint.uuid) |
|
||||
debugger |
|
||||
} |
|
||||
|
|
||||
// 更新线段几何体
|
|
||||
const geometry = line.geometry as THREE.BufferGeometry |
|
||||
geometry.setFromPoints([startPoint.position, endPoint.position]) |
|
||||
geometry.attributes.position.needsUpdate = true |
|
||||
this.afterUpdateLine(line, startPoint, endPoint) |
|
||||
|
|
||||
updateLines.push(line) |
|
||||
}) |
|
||||
|
|
||||
this.afterUpdatePoint(this.dragPoint, updateLines) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* DragControls 拖拽完成后触发这个方法 |
|
||||
*/ |
|
||||
dragPointComplete(viewport: Viewport) { |
|
||||
// 删除鼠标事件监听
|
|
||||
viewport.renderer.domElement.removeEventListener('pointermove', pmFn) |
|
||||
pmFn = undefined |
|
||||
} |
|
||||
} |
|
||||
@ -1,323 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import type Viewport from '@/designer/Viewport.ts' |
|
||||
import type ItemType from '@/model/itemType/ItemType.ts' |
|
||||
import type { ItemJson } from '@/model/WorldModelType.ts' |
|
||||
|
|
||||
let pdFn, pmFn, puFn |
|
||||
|
|
||||
/** |
|
||||
* 单元类型工具箱 |
|
||||
*/ |
|
||||
export default abstract class Toolbox { |
|
||||
/** |
|
||||
* 测量工具所在的视图窗口,从这里可以取到 所有 Three.js 相关的对象. |
|
||||
* 比如: |
|
||||
* - viewport.scene 场景 |
|
||||
* - viewport.renderer 渲染器 |
|
||||
* - viewport.controls 控制器 |
|
||||
* - viewport.camera 摄像机 |
|
||||
* - viewport.raycaster 射线投射器 |
|
||||
* - viewport.dragControl 拖拽控制器 |
|
||||
* - viewport.measure 测量工具 |
|
||||
*/ |
|
||||
viewport: Viewport |
|
||||
|
|
||||
/** |
|
||||
* 是否完成工具箱操作 |
|
||||
*/ |
|
||||
isCompleted = false |
|
||||
|
|
||||
/** |
|
||||
* 是否鼠标移动事件 |
|
||||
*/ |
|
||||
mouseMoved = false |
|
||||
|
|
||||
/** |
|
||||
* 当前鼠标所在的画布, 对应 viewport.renderer.domElement |
|
||||
*/ |
|
||||
canvas: HTMLCanvasElement |
|
||||
|
|
||||
/** |
|
||||
* 用于存储临时点 |
|
||||
*/ |
|
||||
tempPointMarker?: THREE.Mesh |
|
||||
|
|
||||
/** |
|
||||
* 测量起始点 |
|
||||
*/ |
|
||||
startPoint?: THREE.Object3D = undefined |
|
||||
|
|
||||
/** |
|
||||
* 保存上次点击时间,以便检测双击事件 |
|
||||
* @protected |
|
||||
*/ |
|
||||
lastClickTime: number = 0 |
|
||||
|
|
||||
/** |
|
||||
* 上次鼠标移动位置 |
|
||||
*/ |
|
||||
lastMovePosition: THREE.Vector3 | undefined = undefined |
|
||||
|
|
||||
_itemType: any |
|
||||
|
|
||||
get itemType(): ItemType { |
|
||||
return this._itemType |
|
||||
} |
|
||||
|
|
||||
mode: string |
|
||||
|
|
||||
static TMP_TYPE = '_TMP' |
|
||||
|
|
||||
/** |
|
||||
* 获取临时点的名称 |
|
||||
*/ |
|
||||
abstract getTempPointName(): string |
|
||||
|
|
||||
addToScene(object: THREE.Object3D) { |
|
||||
this.viewport.scene.add(object) |
|
||||
} |
|
||||
|
|
||||
deletePoint(point: THREE.Object3D): void { |
|
||||
const deletedPoints = _.remove(this.itemType.pointArray, (p) => p.uuid === point.uuid) |
|
||||
if (!deletedPoints || deletedPoints.length !== 1) { |
|
||||
console.warn('没有找到要删除的点:', point.uuid) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 删除点
|
|
||||
this.removeFromScene(point) |
|
||||
|
|
||||
// 如果是起始点,则清除起始点
|
|
||||
if (this.startPoint === point) { |
|
||||
this.startPoint = undefined |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
removeFromScene(object: THREE.Object3D) { |
|
||||
if (object?.parent) { |
|
||||
object.parent.remove(object) |
|
||||
} else { |
|
||||
this.viewport.scene.remove(object) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 单元工具初始化 |
|
||||
*/ |
|
||||
init(viewport: any, itemType: ItemType): void { |
|
||||
this.viewport = viewport |
|
||||
this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement |
|
||||
this._itemType = itemType |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 测量工具开始, 监听鼠标事件, 变量初始化等 |
|
||||
*/ |
|
||||
start(startPoint?: THREE.Object3D) { |
|
||||
pdFn = this.mousedown.bind(this) |
|
||||
this.canvas.addEventListener('pointerdown', pdFn) |
|
||||
pmFn = this.mousemove.bind(this) |
|
||||
this.canvas.addEventListener('pointermove', pmFn) |
|
||||
puFn = this.mouseup.bind(this) |
|
||||
this.canvas.addEventListener('pointerup', puFn) |
|
||||
|
|
||||
this.isCompleted = false |
|
||||
this.viewport.viewerDom.style.cursor = 'crosshair' |
|
||||
|
|
||||
this.mode = this.viewport.state.cursorMode |
|
||||
this.startPoint = startPoint |
|
||||
|
|
||||
system.msg('新建 [' + this.itemType.name + '] 模式') |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 停止测量模式, 清除所有临时点、线、标签. 停止所有鼠标事件监听 |
|
||||
*/ |
|
||||
stop(): void { |
|
||||
system.msg('退出新建模式') |
|
||||
|
|
||||
const viewerDom = this.viewport.viewerDom |
|
||||
|
|
||||
this.isCompleted = true |
|
||||
viewerDom.style.cursor = '' |
|
||||
|
|
||||
this.canvas.removeEventListener('pointerdown', pdFn) |
|
||||
pdFn = undefined |
|
||||
this.canvas.removeEventListener('pointermove', pmFn) |
|
||||
pmFn = undefined |
|
||||
this.canvas.removeEventListener('pointerup', puFn) |
|
||||
puFn = undefined |
|
||||
|
|
||||
// 清空所有临时点
|
|
||||
this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) |
|
||||
this.tempPointMarker = undefined |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 创建临时点标记 |
|
||||
*/ |
|
||||
createTempPointMarker(position?: THREE.Vector3): THREE.Mesh { |
|
||||
const p = position |
|
||||
const scale = this.itemType.getDefaultScale() |
|
||||
const rotation = this.itemType.getDefaultRotation() |
|
||||
|
|
||||
const tt = new THREE.BoxGeometry(1, 1, 1) |
|
||||
const t2 = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) |
|
||||
const obj = new THREE.Mesh(tt, t2) |
|
||||
obj.scale.set(scale.x, scale.y, scale.x) |
|
||||
obj.rotation.set( |
|
||||
THREE.MathUtils.degToRad(rotation.x), |
|
||||
THREE.MathUtils.degToRad(rotation.y), |
|
||||
THREE.MathUtils.degToRad(rotation.z) |
|
||||
) |
|
||||
if (p) { |
|
||||
obj.position.set(p.x, p.y, p.z) |
|
||||
} |
|
||||
|
|
||||
obj.name = this.getTempPointName() |
|
||||
obj.userData = { |
|
||||
mode: this.mode, |
|
||||
type: Toolbox.TMP_TYPE |
|
||||
} |
|
||||
return obj |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 鼠标按下事件 |
|
||||
*/ |
|
||||
mousedown() { |
|
||||
this.mouseMoved = false |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 鼠标移动,创建对应的临时点与线 |
|
||||
*/ |
|
||||
mousemove(e: MouseEvent): THREE.Vector3 | undefined { |
|
||||
if (this.isCompleted) return |
|
||||
|
|
||||
this.mouseMoved = true |
|
||||
|
|
||||
// 当前鼠标所在的点
|
|
||||
const point = this.viewport.getClosestIntersection(e) |
|
||||
if (!point) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 如果按下了 shift 键,则 point 的位置必须位于 startPoint 正上下方,或者正左右方
|
|
||||
if (this.startPoint && e.shiftKey) { |
|
||||
const startPos = this.startPoint.position |
|
||||
const dx = Math.abs(point.x - startPos.x) |
|
||||
const dz = Math.abs(point.z - startPos.z) |
|
||||
if (dx > dz) { |
|
||||
point.z = startPos.z |
|
||||
} else { |
|
||||
point.x = startPos.x |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
this.lastMovePosition = point |
|
||||
|
|
||||
// 在鼠标移动时绘制临时点
|
|
||||
if (this.tempPointMarker) { |
|
||||
this.tempPointMarker.position.set(point.x, point.y, point.z) |
|
||||
} else { |
|
||||
this.tempPointMarker = this.createTempPointMarker(point) |
|
||||
this.viewport.scene.add(this.tempPointMarker) |
|
||||
} |
|
||||
|
|
||||
// this.viewport.dispatchSignal('sceneGraphChanged')
|
|
||||
return point |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 鼠标松开事件 |
|
||||
*/ |
|
||||
mouseup(e: MouseEvent) { |
|
||||
// 如果mouseMoved是true,那么它可能在移动,而不是点击
|
|
||||
if (!this.mouseMoved) { |
|
||||
|
|
||||
if (e.button === 2) { |
|
||||
// 右键点击, 完成绘图操作
|
|
||||
this.viewport.state.cursorMode = 'normal' |
|
||||
|
|
||||
} else if (e.button === 0) { |
|
||||
// 左键点击, 添加点
|
|
||||
this.onMouseClicked(e) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { |
|
||||
if (this.isCompleted) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 获取鼠标点击位置的三维坐标
|
|
||||
const point = this.lastMovePosition |
|
||||
if (!point) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 双击触发两次点击事件,我们需要避免这里的第二次点击
|
|
||||
const now = Date.now() |
|
||||
if (this.lastClickTime && (now - this.lastClickTime < 50)) { |
|
||||
return |
|
||||
} |
|
||||
this.lastClickTime = now |
|
||||
|
|
||||
const defaultScale = this.itemType.getDefaultScale() |
|
||||
const defaultRotation = this.itemType.getDefaultRotation() |
|
||||
|
|
||||
// 添加正式点
|
|
||||
const itemJson = { |
|
||||
t: this.itemType.name, |
|
||||
a: 'ln', |
|
||||
tf: [ |
|
||||
[point.x, point.y, point.z], |
|
||||
[defaultRotation.x, defaultRotation.y, defaultRotation.z], |
|
||||
[defaultScale.x, defaultScale.y, defaultScale.z] |
|
||||
], |
|
||||
dt: { |
|
||||
in: [] as string[], |
|
||||
out: [] as string[], |
|
||||
center: [] as string[] |
|
||||
} |
|
||||
} as ItemJson |
|
||||
const marker = this.itemType.createPoint(point, itemJson) |
|
||||
this.addToScene(marker) |
|
||||
|
|
||||
// 把点加入拖拽控制器
|
|
||||
//this.viewport.dragControl.setDragObjects([marker], 'push')
|
|
||||
|
|
||||
if (this.startPoint) { |
|
||||
this.afterAddPoint(this.startPoint, marker) |
|
||||
} |
|
||||
|
|
||||
// 更新起始点为新添加的点
|
|
||||
this.startPoint = marker |
|
||||
|
|
||||
// 删除临时线
|
|
||||
this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker) |
|
||||
this.tempPointMarker = undefined |
|
||||
|
|
||||
return point |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 当用户点击某个点,从临时点转换为正式点后调用 |
|
||||
* 子类可以重写此方法来处理添加点后的逻辑 |
|
||||
*/ |
|
||||
afterAddPoint(startPoint: THREE.Object3D, endPoint: THREE.Object3D): void { |
|
||||
// 默认实现不做任何操作
|
|
||||
// 子类可以重写此方法来处理添加点后的逻辑
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 销毁测量工具, 当视图窗口被销毁时调用 |
|
||||
*/ |
|
||||
destory() { |
|
||||
} |
|
||||
} |
|
||||
@ -1,149 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import Toolbox from '@/model/itemType/Toolbox.ts' |
|
||||
import type ItemTypeLine from '@/model/itemType/ItemTypeLine.ts' |
|
||||
import { findObject3DById, getAllControlPoints } from '@/model/ModelUtils.ts' |
|
||||
import EventBus from '@/runtime/EventBus' |
|
||||
|
|
||||
/** |
|
||||
* 线条工具箱 |
|
||||
*/ |
|
||||
export default class ToolboxLine extends Toolbox { |
|
||||
/** |
|
||||
* 临时线条 |
|
||||
*/ |
|
||||
tempLine?: THREE.Mesh |
|
||||
|
|
||||
get itemType(): ItemTypeLine { |
|
||||
return this._itemType |
|
||||
} |
|
||||
|
|
||||
getTempPointName(): string { |
|
||||
return '_measure_temp_point' |
|
||||
} |
|
||||
|
|
||||
afterMoveTemplateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { |
|
||||
} |
|
||||
|
|
||||
afterDeleteLine(line: THREE.Object3D, point: THREE.Object3D) { |
|
||||
} |
|
||||
|
|
||||
stop() { |
|
||||
super.stop() |
|
||||
|
|
||||
this.tempLine && this.removeFromScene(this.tempLine) |
|
||||
this.tempLine = undefined |
|
||||
} |
|
||||
|
|
||||
afterAddPoint(startPoint: THREE.Object3D, point: THREE.Object3D) { |
|
||||
// 如果起始点存在,则将新点添加到起始点的链接中
|
|
||||
startPoint.userData.center.push(point.uuid) |
|
||||
this.itemType.createLine(this.viewport, this.viewport.scene, this.startPoint, point) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 删除点 |
|
||||
*/ |
|
||||
deletePoint(point: THREE.Object3D) { |
|
||||
const allPoints = getAllControlPoints() |
|
||||
|
|
||||
const deletedPoints = _.remove(getAllControlPoints(), (p) => p.uuid === point.uuid) |
|
||||
if (!deletedPoints || deletedPoints.length !== 1) { |
|
||||
console.warn('没有找到要删除的点:', point.uuid) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
if (this.viewport.state.selectedObject === point) { |
|
||||
// 如果当前选中的对象是要删除的点,则清除选中状态
|
|
||||
this.viewport.state.selectedObject = undefined |
|
||||
EventBus.dispatch('objectChanged', { |
|
||||
viewport: this, |
|
||||
object: null |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// 找出与这个点相关的其他点
|
|
||||
allPoints.forEach(p => { |
|
||||
if (p.userData.center) { |
|
||||
_.remove(p.userData.center, i => i === point.uuid) |
|
||||
_.remove(p.userData.in, i => i === point.uuid) |
|
||||
_.remove(p.userData.out, i => i === point.uuid) |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// 找出与点相关的所有线
|
|
||||
_.forEach(point.userData.lines, (line) => { |
|
||||
const lineObject = findObject3DById(this.viewport.scene, line) |
|
||||
this.removeFromScene(lineObject) |
|
||||
this.afterDeleteLine(lineObject, point) |
|
||||
}) |
|
||||
|
|
||||
// 从场景中删除点
|
|
||||
this.removeFromScene(point) |
|
||||
|
|
||||
// 如果是起始点,则清除起始点
|
|
||||
if (this.startPoint === point) { |
|
||||
this.startPoint = undefined |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
mousemove(e: MouseEvent): THREE.Vector3 | undefined { |
|
||||
const point = super.mousemove(e) |
|
||||
if (!point) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 移动时绘制临时线
|
|
||||
if (this.startPoint) { |
|
||||
// 获取最后一个点
|
|
||||
if (!this.tempLine) { |
|
||||
this.tempLine = this.itemType.createLineBasic(true) |
|
||||
this.viewport.scene.add(this.tempLine) |
|
||||
} |
|
||||
|
|
||||
const p0 = this.startPoint.position |
|
||||
const line = this.tempLine |
|
||||
const geom = line.geometry |
|
||||
geom.setFromPoints([p0, point]) |
|
||||
|
|
||||
this.afterMoveTemplateLine(line, this.startPoint, this.tempPointMarker) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { |
|
||||
|
|
||||
const point = this.lastMovePosition |
|
||||
// 如果正式的点命中到同类型的节点上,则不添加新的点,只牵线到该点
|
|
||||
if (point) { |
|
||||
let catchPoint = null |
|
||||
const vv = this.itemType.pointArray.some(p => { |
|
||||
if (p.position.x === point.x && p.position.z === point.z) { |
|
||||
catchPoint = p |
|
||||
return true |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
if (catchPoint) { |
|
||||
if (this.startPoint === catchPoint) { |
|
||||
// 自己连接自己,忽略
|
|
||||
return |
|
||||
} |
|
||||
// 如果捕获到点,则将线条连接到该点
|
|
||||
if (this.startPoint) { |
|
||||
this.afterAddPoint(this.startPoint, catchPoint) |
|
||||
} |
|
||||
this.tempLine && this.removeFromScene(this.tempLine) |
|
||||
this.tempLine = undefined |
|
||||
return |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const r = super.onMouseClicked(e) |
|
||||
if (!r) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
this.tempLine && this.removeFromScene(this.tempLine) |
|
||||
this.tempLine = undefined |
|
||||
return r |
|
||||
} |
|
||||
} |
|
||||
@ -1,7 +0,0 @@ |
|||||
import { defineItem } from '@/runtime/DefineItem.ts' |
|
||||
|
|
||||
export default defineItem({ |
|
||||
name: 'line', |
|
||||
label: '辅助线', |
|
||||
category: 'line' |
|
||||
}) |
|
||||
@ -1,124 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import ItemTypeLine from '@/model/itemType/ItemTypeLine.ts' |
|
||||
import WorldModel from '@/model/WorldModel.ts' |
|
||||
import Viewport from '@/designer/Viewport.ts' |
|
||||
import ToolboxLine from '@/model/itemType/ToolboxLine.ts' |
|
||||
import ConveyorToolbox from './ConveyorToolbox.ts' |
|
||||
import { Line2 } from 'three/examples/jsm/lines/Line2.js' |
|
||||
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' |
|
||||
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' |
|
||||
import _ from 'lodash' |
|
||||
|
|
||||
export default class Conveyor extends ItemTypeLine { |
|
||||
defaultScale: THREE.Vector3 = new THREE.Vector3(0.25, 0.1, 0.25) |
|
||||
defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) |
|
||||
|
|
||||
pointMaterial!: THREE.Material |
|
||||
|
|
||||
lineMaterial!: LineMaterial |
|
||||
lineMaterialTemplate!: LineMaterial |
|
||||
lineMaterialOutline!: LineMaterial |
|
||||
|
|
||||
static POINT_NAME = 'conveyor_point' |
|
||||
static LINE_NAME = 'conveyor_line' |
|
||||
|
|
||||
async init(worldModel: WorldModel): Promise<void> { |
|
||||
await super.init(worldModel) |
|
||||
try { |
|
||||
this.pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) |
|
||||
|
|
||||
this.lineMaterial = new LineMaterial({ |
|
||||
alphaToCoverage: true, |
|
||||
side: THREE.DoubleSide, |
|
||||
color: 0x0088ff, |
|
||||
dashed: true, |
|
||||
resolution: new THREE.Vector2(1, 1), // 需要在afterAddScene中设置
|
|
||||
dashOffset: 0, |
|
||||
linewidth: 0.8, |
|
||||
dashScale: 1, |
|
||||
dashSize: 0.2, |
|
||||
gapSize: 0.2, |
|
||||
worldUnits: true |
|
||||
// linewidth: 10,
|
|
||||
// worldUnits: false,
|
|
||||
// dashSize: 0.2,
|
|
||||
// gapSize: 0.2,
|
|
||||
// dashScale: 1
|
|
||||
}) |
|
||||
|
|
||||
this.lineMaterialTemplate = new LineMaterial({ |
|
||||
color: 0x0088ff, |
|
||||
linewidth: 0.8, |
|
||||
worldUnits: true, |
|
||||
opacity: 0.5, |
|
||||
transparent: true, |
|
||||
alphaToCoverage: true, |
|
||||
depthWrite: false, // 避免深度冲突
|
|
||||
blending: THREE.NormalBlending |
|
||||
}) |
|
||||
|
|
||||
window['lineMaterial'] = this.lineMaterial // 方便调试查看
|
|
||||
|
|
||||
} catch (error) { |
|
||||
system.showErrorDialog('Texture loading failed:' + error) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
afterAddScene(viewport: Viewport, scene: THREE.Scene, objects: THREE.Object3D[]) { |
|
||||
super.afterAddScene(viewport, scene, objects) |
|
||||
_.defer(() => { |
|
||||
const canvas = viewport.renderer.domElement |
|
||||
// this.lineMaterial.resolution.set(canvas.width, canvas.height)
|
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
getDefaultScale(): THREE.Vector3 { |
|
||||
return this.defaultScale |
|
||||
} |
|
||||
|
|
||||
getDefaultRotation(): THREE.Vector3 { |
|
||||
return this.defaultRotation |
|
||||
} |
|
||||
|
|
||||
createToolbox(viewport: Viewport): ToolboxLine { |
|
||||
const toolbox = new ConveyorToolbox() |
|
||||
toolbox.init(viewport, this) |
|
||||
//@ts-ignore
|
|
||||
return toolbox |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 创建测量点 |
|
||||
*/ |
|
||||
createPointBasic(position?: THREE.Vector3): THREE.Object3D { |
|
||||
const p = position |
|
||||
const scale = 0.25 |
|
||||
|
|
||||
const tt = new THREE.BoxGeometry(1, 1, 1) |
|
||||
const obj = new THREE.Mesh(tt, this.pointMaterial) |
|
||||
obj.scale.set(scale, 0.1, scale) |
|
||||
if (p) { |
|
||||
obj.position.set(p.x, p.y, p.z) |
|
||||
} |
|
||||
obj.name = Conveyor.POINT_NAME |
|
||||
return obj |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 创建测量线 |
|
||||
*/ |
|
||||
createLineBasic(isTemplate?: boolean): THREE.Mesh { |
|
||||
const geom = new LineGeometry() |
|
||||
let obj: THREE.Mesh |
|
||||
if (isTemplate) { |
|
||||
obj = new Line2(geom, this.lineMaterialTemplate) |
|
||||
} else { |
|
||||
obj = new Line2(geom, this.lineMaterial) |
|
||||
} |
|
||||
obj.frustumCulled = false |
|
||||
obj.name = Conveyor.LINE_NAME |
|
||||
obj.uuid = THREE.MathUtils.generateUUID() |
|
||||
|
|
||||
return obj |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
import { defineItemType } from '@/model/itemType/ItemTypeDefine.ts' |
|
||||
import Conveyor from './Conveyor.ts' |
|
||||
|
|
||||
export default defineItemType({ |
|
||||
name: 'conveyor', |
|
||||
label: '输送线', |
|
||||
actionType: 'ln', |
|
||||
clazz: new Conveyor() |
|
||||
}) |
|
||||
@ -1,16 +0,0 @@ |
|||||
import ToolboxLine from '@/model/itemType/ToolboxLine.ts' |
|
||||
import type Conveyor from './Conveyor.ts' |
|
||||
|
|
||||
/** |
|
||||
* 测量工具箱,用于处理测量相关的操作 |
|
||||
*/ |
|
||||
export default class ConveyorToolbox extends ToolboxLine { |
|
||||
|
|
||||
constructor() { |
|
||||
super() |
|
||||
} |
|
||||
|
|
||||
get conveyor(): Conveyor { |
|
||||
return this._itemType |
|
||||
} |
|
||||
} |
|
||||
@ -1,202 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import { Material } from 'three' |
|
||||
import ItemTypeLine from '@/model/itemType/ItemTypeLine.ts' |
|
||||
import WorldModel from '@/model/WorldModel.ts' |
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' |
|
||||
import { numberToString } from '@/utils/webutils.ts' |
|
||||
import { findObject3DById } from '@/model/ModelUtils.ts' |
|
||||
import Viewport from '@/designer/Viewport.ts' |
|
||||
import ToolboxLine from '@/model/itemType/ToolboxLine.ts' |
|
||||
import MeasureToolbox from '@/model/itemType/measure/MeasureToolbox.ts' |
|
||||
import Toolbox from '@/model/itemType/Toolbox.ts' |
|
||||
import { Line2 } from 'three/examples/jsm/lines/Line2.js' |
|
||||
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' |
|
||||
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' |
|
||||
|
|
||||
export default class Measure extends ItemTypeLine { |
|
||||
/** |
|
||||
* 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线 |
|
||||
*/ |
|
||||
group: THREE.Group |
|
||||
|
|
||||
defaultScale: THREE.Vector3 = new THREE.Vector3(0.25, 0.1, 0.25) |
|
||||
defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) |
|
||||
|
|
||||
pointMaterial!: Material |
|
||||
|
|
||||
lineMaterial!: LineMaterial |
|
||||
|
|
||||
static GROUP_NAME = 'measure-group' |
|
||||
static LABEL_NAME = 'measure_label' |
|
||||
static POINT_NAME = 'measure_point' |
|
||||
static LINE_NAME = 'measure_line' |
|
||||
|
|
||||
override init(worldModel: WorldModel): Promise<void> { |
|
||||
super.init(worldModel) |
|
||||
|
|
||||
// this.lineMaterial = new THREE.LineBasicMaterial({
|
|
||||
// color: 0xE63C17,
|
|
||||
// linewidth: 2,
|
|
||||
// opacity: 0.9,
|
|
||||
// transparent: true,
|
|
||||
// side: THREE.DoubleSide,
|
|
||||
// depthWrite: false,
|
|
||||
// depthTest: false
|
|
||||
// })
|
|
||||
this.lineMaterial = new LineMaterial({ |
|
||||
color: 0xE63C17, // 主颜色
|
|
||||
linewidth: 2, // 实际可用的线宽
|
|
||||
vertexColors: true, // 启用顶点颜色
|
|
||||
dashed: false, |
|
||||
alphaToCoverage: true |
|
||||
}) |
|
||||
|
|
||||
this.pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) |
|
||||
|
|
||||
return Promise.resolve() |
|
||||
} |
|
||||
|
|
||||
getDefaultScale(): THREE.Vector3 { |
|
||||
return this.defaultScale |
|
||||
} |
|
||||
|
|
||||
getDefaultRotation(): THREE.Vector3 { |
|
||||
return this.defaultRotation |
|
||||
} |
|
||||
|
|
||||
beforeLoad(): THREE.Object3D[] { |
|
||||
this.group = null |
|
||||
return [] |
|
||||
} |
|
||||
|
|
||||
createToolbox(viewport: Viewport): ToolboxLine { |
|
||||
const toolbox = new MeasureToolbox(this.group) |
|
||||
toolbox.init(viewport, this) |
|
||||
//@ts-ignore
|
|
||||
return toolbox |
|
||||
} |
|
||||
|
|
||||
afterLoadGroup(group: THREE.Group) { |
|
||||
super.afterLoadGroup(group) |
|
||||
if (this.group) { |
|
||||
// 如果已经有 group,则忽略
|
|
||||
return |
|
||||
} |
|
||||
this.group = group |
|
||||
} |
|
||||
|
|
||||
|
|
||||
afterLoadComplete(objects: THREE.Object3D[]): THREE.Object3D[] { |
|
||||
// 如果没有 group,则创建一个新的 group
|
|
||||
if (!this.group) { |
|
||||
this.group = new THREE.Group() |
|
||||
this.group.name = Measure.GROUP_NAME |
|
||||
} |
|
||||
|
|
||||
return [this.group] |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 创建测量点 |
|
||||
*/ |
|
||||
createPointBasic(position?: THREE.Vector3): THREE.Object3D { |
|
||||
const p = position |
|
||||
const scale = 0.25 |
|
||||
|
|
||||
const tt = new THREE.BoxGeometry(1, 1, 1) |
|
||||
const obj = new THREE.Mesh(tt, this.pointMaterial) |
|
||||
obj.scale.set(scale, 0.1, scale) |
|
||||
if (p) { |
|
||||
obj.position.set(p.x, p.y, p.z) |
|
||||
} |
|
||||
obj.name = Measure.POINT_NAME |
|
||||
return obj |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 创建测量线 |
|
||||
*/ |
|
||||
createLineBasic(): Line2 { |
|
||||
const geom = new LineGeometry() |
|
||||
const obj = new Line2(geom, this.lineMaterial) |
|
||||
obj.frustumCulled = false |
|
||||
obj.name = Measure.LINE_NAME |
|
||||
obj.uuid = THREE.MathUtils.generateUUID() |
|
||||
return obj |
|
||||
} |
|
||||
|
|
||||
// 创建完线之后,创建 label
|
|
||||
afterCreateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { |
|
||||
super.afterCreateLine(line, startPoint, endPoint) |
|
||||
if (!startPoint.userData.center) { |
|
||||
startPoint.userData.center = [] |
|
||||
} |
|
||||
if (!startPoint.userData.center.includes(endPoint.uuid)) { |
|
||||
startPoint.userData.center.push(endPoint.uuid) |
|
||||
} |
|
||||
|
|
||||
const p0 = startPoint.position |
|
||||
const p1 = endPoint.position |
|
||||
|
|
||||
const dist = p0.distanceTo(p1) |
|
||||
const label = `${numberToString(dist)} m` |
|
||||
|
|
||||
const position = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5) |
|
||||
const labelObj = this.createLabel(label) |
|
||||
labelObj.name = Measure.LABEL_NAME |
|
||||
labelObj.position.set(position.x, position.y, position.z) |
|
||||
labelObj.element.innerHTML = label |
|
||||
|
|
||||
line.userData.labelId = labelObj.uuid |
|
||||
this.group.add(labelObj) |
|
||||
} |
|
||||
|
|
||||
afterUpdateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { |
|
||||
super.afterUpdateLine(line, startPoint, endPoint) |
|
||||
|
|
||||
const p0 = startPoint.position |
|
||||
const p1 = endPoint.position |
|
||||
|
|
||||
const dist = p0.distanceTo(p1) |
|
||||
const label = `${numberToString(dist)} 米` |
|
||||
const position = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5) |
|
||||
|
|
||||
const labelObj: CSS2DObject = findObject3DById(this.group, line.userData.labelId) as CSS2DObject |
|
||||
if (labelObj) { |
|
||||
labelObj.position.set(position.x, position.y, position.z) |
|
||||
labelObj.element.innerHTML = label |
|
||||
|
|
||||
} else { |
|
||||
// 如果没有找到,则创建新的标签
|
|
||||
const newLabelObj = this.createLabel(label) |
|
||||
newLabelObj.position.set(position.x, position.y, position.z) |
|
||||
newLabelObj.element.innerHTML = label |
|
||||
line.userData.labelId = labelObj.uuid |
|
||||
this.group.add(newLabelObj) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 创建标签 |
|
||||
*/ |
|
||||
createLabel(text: string): CSS2DObject { |
|
||||
const div = document.createElement('div') |
|
||||
div.className = 'css2dObjectLabel' |
|
||||
div.innerHTML = text |
|
||||
div.style.padding = '5px 8px' |
|
||||
div.style.color = '#fff' |
|
||||
div.style.fontSize = '14px' |
|
||||
div.style.position = 'absolute' |
|
||||
div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' |
|
||||
div.style.borderRadius = '12px' |
|
||||
div.style.top = '0px' |
|
||||
div.style.left = '0px' |
|
||||
// div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件
|
|
||||
const obj = new CSS2DObject(div) |
|
||||
obj.name = MeasureToolbox.TMP_LABEL_NAME |
|
||||
obj.userData = { |
|
||||
type: Toolbox.TMP_TYPE |
|
||||
} |
|
||||
return obj |
|
||||
} |
|
||||
} |
|
||||
@ -1,36 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import Measure from '@/model/itemType/measure/Measure.ts' |
|
||||
import { |
|
||||
defineItemType, |
|
||||
BASIC_META_OF_POINT, |
|
||||
BASIC_META_OF_POINT2, |
|
||||
BASIC_META_OF_LINE, |
|
||||
BASIC_META_OF_LINE2, |
|
||||
type ItemTypeMeta |
|
||||
} from '@/model/itemType/ItemTypeDefine.ts' |
|
||||
|
|
||||
export default defineItemType({ |
|
||||
name: 'measure', |
|
||||
label: '测量距离', |
|
||||
actionType: 'ln', |
|
||||
clazz: new Measure(), |
|
||||
|
|
||||
/** |
|
||||
* 获取单元类型的元数据 |
|
||||
*/ |
|
||||
getMeta(object: THREE.Object3D): ItemTypeMeta { |
|
||||
if (object.name === Measure.LINE_NAME) { |
|
||||
return [ |
|
||||
...BASIC_META_OF_LINE, |
|
||||
...BASIC_META_OF_LINE2 |
|
||||
] |
|
||||
|
|
||||
} else if (object.name === Measure.POINT_NAME) { |
|
||||
return [ |
|
||||
...BASIC_META_OF_POINT, |
|
||||
...BASIC_META_OF_POINT2 |
|
||||
] |
|
||||
} |
|
||||
return [] |
|
||||
} |
|
||||
}) |
|
||||
@ -1,94 +0,0 @@ |
|||||
import * as THREE from 'three' |
|
||||
import ToolboxLine from '@/model/itemType/ToolboxLine.ts' |
|
||||
import { numberToString } from '@/utils/webutils.ts' |
|
||||
import type Measure from './Measure.ts' |
|
||||
import type { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' |
|
||||
import Toolbox from '@/model/itemType/Toolbox.ts' |
|
||||
import { findObject3DById } from '@/model/ModelUtils.ts' |
|
||||
|
|
||||
/** |
|
||||
* 测量工具箱,用于处理测量相关的操作 |
|
||||
*/ |
|
||||
export default class MeasureToolbox extends ToolboxLine { |
|
||||
|
|
||||
group: THREE.Group |
|
||||
|
|
||||
/** |
|
||||
* 临时标签对象, 用于在鼠标移动时显示距离 |
|
||||
*/ |
|
||||
tempLabel?: CSS2DObject |
|
||||
|
|
||||
static TMP_LABEL_NAME = '_measure_temp_label' |
|
||||
|
|
||||
constructor(group: THREE.Group) { |
|
||||
super() |
|
||||
this.group = group |
|
||||
} |
|
||||
|
|
||||
stop() { |
|
||||
super.stop() |
|
||||
|
|
||||
// 清除临时标签
|
|
||||
this.tempLabel && this.removeFromScene(this.tempLabel) |
|
||||
this.tempLabel = undefined |
|
||||
} |
|
||||
|
|
||||
onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined { |
|
||||
const r = super.onMouseClicked(e) |
|
||||
if (!r) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
this.tempLabel && this.removeFromScene(this.tempLabel) |
|
||||
this.tempLabel = undefined |
|
||||
} |
|
||||
|
|
||||
get measure(): Measure { |
|
||||
return this._itemType |
|
||||
} |
|
||||
|
|
||||
addToScene(object: THREE.Object3D) { |
|
||||
this.measure.group.add(object) |
|
||||
} |
|
||||
|
|
||||
afterMoveTemplateLine(line: THREE.Mesh, startPoint: THREE.Object3D, endPoint: THREE.Object3D) { |
|
||||
super.afterMoveTemplateLine(line, startPoint, endPoint) |
|
||||
|
|
||||
const p0 = startPoint.position |
|
||||
const point = endPoint.position |
|
||||
|
|
||||
const dist = p0.distanceTo(point) |
|
||||
const label = `${numberToString(dist)} m` |
|
||||
const position = new THREE.Vector3().addVectors(p0, point).multiplyScalar(0.5) |
|
||||
this.addOrUpdateTempLabel(line, label, position) |
|
||||
} |
|
||||
|
|
||||
afterDeleteLine(line: THREE.Object3D, point: THREE.Object3D) { |
|
||||
super.afterDeleteLine(line, point) |
|
||||
|
|
||||
// 删除临时标签
|
|
||||
if (line?.userData?.labelId) { |
|
||||
const label = findObject3DById(this.measure.group, line.userData.labelId) |
|
||||
this.removeFromScene(label) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 添加或更新临时标签和位置 |
|
||||
*/ |
|
||||
addOrUpdateTempLabel(line: THREE.Mesh, label: string, position: THREE.Vector3) { |
|
||||
if (!this.tempLabel) { |
|
||||
this.tempLabel = this.measure.createLabel(label) |
|
||||
this.tempLabel.name = MeasureToolbox.TMP_LABEL_NAME |
|
||||
this.tempLabel.uuid = THREE.MathUtils.generateUUID() |
|
||||
this.tempLabel.userData = { |
|
||||
mode: this.mode, |
|
||||
type: Toolbox.TMP_TYPE |
|
||||
} |
|
||||
line.userData.labelId = this.tempLabel.uuid |
|
||||
this.addToScene(this.tempLabel) |
|
||||
} |
|
||||
this.tempLabel.position.set(position.x, position.y, position.z) |
|
||||
this.tempLabel.element.innerHTML = label |
|
||||
} |
|
||||
} |
|
||||
@ -1,7 +0,0 @@ |
|||||
import { defineItem } from '@/runtime/DefineItem.ts' |
|
||||
|
|
||||
export default defineItem({ |
|
||||
name: 'point', |
|
||||
label: '辅助点', |
|
||||
category: 'point' |
|
||||
}) |
|
||||
@ -1,7 +0,0 @@ |
|||||
import { defineItem } from '@/runtime/DefineItem.ts' |
|
||||
|
|
||||
export default defineItem({ |
|
||||
name: 'queue', |
|
||||
label: '暂存区', |
|
||||
category: 'store' |
|
||||
}) |
|
||||
@ -0,0 +1,5 @@ |
|||||
|
import BaseEntity from '@/core/base/BaseItemEntity.ts' |
||||
|
|
||||
|
export default class PalletEntity extends BaseEntity { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import BaseInteraction from '@/core/base/BaseInteraction.ts' |
||||
|
import * as THREE from 'three' |
||||
|
|
||||
|
export default class PalletInteraction extends BaseInteraction { |
||||
|
|
||||
|
get isSinglePointMode(): boolean { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
constructor(itemTypeName: string) { |
||||
|
super(itemTypeName) |
||||
|
} |
||||
|
|
||||
|
createPointOfItem(item: ItemJson, point: THREE.Vector3): ItemJson { |
||||
|
item = super.createPointOfItem(item, point) |
||||
|
|
||||
|
// 创建一个地堆货架
|
||||
|
item.dt.palletWidth = 1 // 宽度
|
||||
|
item.dt.palletDepth = 1.2 // 深度
|
||||
|
return item |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import type { PropertySetter } from "@/core/base/PropertyTypes.ts"; |
||||
|
import { basicFieldsSetter } from "@/editor/widgets/property/PropertyPanelConstant.ts"; |
||||
|
|
||||
|
const propertySetter: PropertySetter = { |
||||
|
flatten: { |
||||
|
fields: [ |
||||
|
...basicFieldsSetter, |
||||
|
{ |
||||
|
dataPath: 'dt.palletWidth', label: '托盘宽度', input: 'InputNumber', |
||||
|
inputProps: {}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'dt.palletDepth', label: '托盘深度', input: 'InputNumber', |
||||
|
inputProps: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
export default propertySetter; |
||||
@ -0,0 +1,87 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import BaseRenderer from '@/core/base/BaseRenderer.ts' |
||||
|
import Constract from '@/core/Constract.ts' |
||||
|
import InstancePointManager from '@/core/manager/InstancePointManager.ts' |
||||
|
import type { Object3DLike } from '@/types/ModelTypes.ts' |
||||
|
import MODULE_GLB_File from '@/assets/Models/carton.glb?url' |
||||
|
import MODULE_3DS_TEX from '@/assets/Models/carton.jpg?url' |
||||
|
import { load3DModule, loadByUrl, loadGlbModule, loadTexture } from '@/core/ModelUtils.ts' |
||||
|
|
||||
|
/** |
||||
|
* 货架货位渲染器 |
||||
|
*/ |
||||
|
export default class PalletRenderer extends BaseRenderer { |
||||
|
static POINT_NAME = 'carton_point' |
||||
|
|
||||
|
/** |
||||
|
* 默认点的高度, 防止和地面重合 |
||||
|
*/ |
||||
|
readonly defulePositionY: number = Constract.HEIGHT_WAY |
||||
|
readonly defaultScale: THREE.Vector3 = new THREE.Vector3(1, 1, 1) |
||||
|
readonly defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) |
||||
|
readonly defaultUserData = { |
||||
|
color: 0xc29a70 |
||||
|
} |
||||
|
|
||||
|
cartonGeometry: THREE.BufferGeometry |
||||
|
cartonMaterial: THREE.Material |
||||
|
|
||||
|
init() { |
||||
|
return Promise.all([ |
||||
|
super.init(), |
||||
|
loadGlbModule(MODULE_GLB_File), |
||||
|
loadTexture(MODULE_3DS_TEX) |
||||
|
|
||||
|
]).then(([_, glbGroup, cartonTexture]) => { |
||||
|
const mesh = glbGroup.children[0] as THREE.Mesh |
||||
|
this.cartonGeometry = mesh.geometry |
||||
|
this.cartonMaterial = new THREE.MeshPhongMaterial({ color: 0xc29a70 }) // mesh.material as THREE.Material
|
||||
|
this.cartonGeometry.scale(0.01, 0.01, 0.01) |
||||
|
// this.cartonGeometry.rotateX(-Math.PI / 2)
|
||||
|
this.cartonGeometry.center() |
||||
|
this.cartonGeometry.translate(0, 0.3, 0) |
||||
|
|
||||
|
cartonTexture.flipY = true |
||||
|
cartonTexture.wrapS = THREE.RepeatWrapping |
||||
|
cartonTexture.wrapT = THREE.RepeatWrapping |
||||
|
cartonTexture.repeat.set(1, -1) |
||||
|
cartonTexture.offset.y = 1 |
||||
|
//@ts-ignore
|
||||
|
this.cartonMaterial.map = cartonTexture |
||||
|
//@ts-ignore
|
||||
|
this.cartonMaterial.color.set(this.defaultUserData.color) |
||||
|
|
||||
|
this.cartonMaterial.needsUpdate = true |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
createPointBasic(item: ItemJson, option?: RendererCudOption): Object3DLike { |
||||
|
return this.pointManager.createPoint(item) |
||||
|
} |
||||
|
|
||||
|
get pointManager(): InstancePointManager { |
||||
|
if (!this.tempViewport) { |
||||
|
throw new Error('tempViewport is not set.') |
||||
|
} |
||||
|
return this.tempViewport.getOrCreatePointManager(this.itemTypeName, () => |
||||
|
// 构建 InstanceMesh 代理对象
|
||||
|
InstancePointManager.create(this.itemTypeName, |
||||
|
this.tempViewport, |
||||
|
this.cartonGeometry, |
||||
|
this.cartonMaterial, |
||||
|
true, true) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
dispose() { |
||||
|
super.dispose() |
||||
|
if (this.cartonGeometry) { |
||||
|
this.cartonGeometry.dispose() |
||||
|
this.cartonGeometry = undefined |
||||
|
} |
||||
|
if (this.cartonMaterial) { |
||||
|
this.cartonMaterial.dispose() |
||||
|
this.cartonMaterial = undefined |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { defineModule } from '@/core/manager/ModuleManager.ts' |
||||
|
import CartonRenderer from './CartonRenderer.ts' |
||||
|
import CartonEntity from './CartonEntity.ts' |
||||
|
import CartonInteraction from './CartonInteraction.ts' |
||||
|
import propertySetter from "./CartonPropertySetter.ts"; |
||||
|
|
||||
|
export const ITEM_TYPE_NAME = 'carton' |
||||
|
|
||||
|
export default defineModule({ |
||||
|
name: ITEM_TYPE_NAME, |
||||
|
renderer: new CartonRenderer(ITEM_TYPE_NAME), |
||||
|
interaction: new CartonInteraction(ITEM_TYPE_NAME), |
||||
|
setter: propertySetter, |
||||
|
entity: CartonEntity, |
||||
|
}) |
||||
@ -1,12 +1,20 @@ |
|||||
import type { PropertySetter } from "@/core/base/PropertyTypes.ts"; |
import type { PropertySetter } from '@/core/base/PropertyTypes.ts' |
||||
import { basicFieldsSetter } from "@/editor/widgets/property/PropertyPanelConstant.ts"; |
import { basicFieldsSetter } from '@/editor/widgets/property/PropertyPanelConstant.ts' |
||||
|
|
||||
const propertySetter: PropertySetter = { |
const propertySetter: PropertySetter = { |
||||
flatten: { |
flatten: { |
||||
fields: [ |
fields: [ |
||||
...basicFieldsSetter, |
...basicFieldsSetter, |
||||
], |
{ |
||||
}, |
dataPath: 'dt.strokeColor', label: '边线颜色', input: 'ColorPicker', |
||||
}; |
inputProps: {} |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'dt.strokeWidth', label: '边线宽度', input: 'InputNumber', |
||||
|
inputProps: {} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
|
||||
export default propertySetter; |
export default propertySetter |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
import BaseEntity from '@/core/base/BaseItemEntity.ts' |
||||
|
|
||||
|
export default class PalletEntity extends BaseEntity { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import BaseInteraction from '@/core/base/BaseInteraction.ts' |
||||
|
import * as THREE from 'three' |
||||
|
|
||||
|
export default class PalletInteraction extends BaseInteraction { |
||||
|
|
||||
|
get isSinglePointMode(): boolean { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
constructor(itemTypeName: string) { |
||||
|
super(itemTypeName) |
||||
|
} |
||||
|
|
||||
|
createPointOfItem(item: ItemJson, point: THREE.Vector3): ItemJson { |
||||
|
item = super.createPointOfItem(item, point) |
||||
|
|
||||
|
// 创建一个地堆货架
|
||||
|
item.dt.palletWidth = 1 // 宽度
|
||||
|
item.dt.palletDepth = 1.2 // 深度
|
||||
|
return item |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import type { PropertySetter } from "@/core/base/PropertyTypes.ts"; |
||||
|
import { basicFieldsSetter } from "@/editor/widgets/property/PropertyPanelConstant.ts"; |
||||
|
|
||||
|
const propertySetter: PropertySetter = { |
||||
|
flatten: { |
||||
|
fields: [ |
||||
|
...basicFieldsSetter, |
||||
|
{ |
||||
|
dataPath: 'dt.palletWidth', label: '托盘宽度', input: 'InputNumber', |
||||
|
inputProps: {}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'dt.palletDepth', label: '托盘深度', input: 'InputNumber', |
||||
|
inputProps: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
export default propertySetter; |
||||
@ -0,0 +1,77 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import BaseRenderer from '@/core/base/BaseRenderer.ts' |
||||
|
import Constract from '@/core/Constract.ts' |
||||
|
import InstancePointManager from '@/core/manager/InstancePointManager.ts' |
||||
|
import type { Object3DLike } from '@/types/ModelTypes.ts' |
||||
|
import MODULE_3DS_File from '@/assets/Models/Tote.3ds?url' |
||||
|
import MODULE_3DS_TEX from '@/assets/Models/ToteTex.png?url' |
||||
|
import { load3DModule, loadByUrl, loadTexture } from '@/core/ModelUtils.ts' |
||||
|
|
||||
|
/** |
||||
|
* 货架货位渲染器 |
||||
|
*/ |
||||
|
export default class PalletRenderer extends BaseRenderer { |
||||
|
static POINT_NAME = 'pallet_point' |
||||
|
|
||||
|
/** |
||||
|
* 默认点的高度, 防止和地面重合 |
||||
|
*/ |
||||
|
readonly defulePositionY: number = Constract.HEIGHT_WAY |
||||
|
readonly defaultScale: THREE.Vector3 = new THREE.Vector3(1, 1, 1) |
||||
|
readonly defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) |
||||
|
readonly defaultUserData = { |
||||
|
color: 0x4559A0 |
||||
|
} |
||||
|
|
||||
|
toteGeometry: THREE.BufferGeometry |
||||
|
toteMaterial: THREE.Material |
||||
|
|
||||
|
init() { |
||||
|
return Promise.all([ |
||||
|
super.init(), |
||||
|
loadByUrl(MODULE_3DS_File), |
||||
|
loadTexture(MODULE_3DS_TEX) |
||||
|
|
||||
|
]).then(([_, { data: queue3dsFile }, queueTexture]) => { |
||||
|
const mesh = load3DModule(queue3dsFile, '.3ds').children[0] as THREE.Mesh |
||||
|
this.toteGeometry = mesh.geometry.rotateX(-Math.PI / 2) |
||||
|
this.toteGeometry.scale(1, 1, 1) |
||||
|
this.toteGeometry.center() |
||||
|
this.toteMaterial = mesh.material as THREE.Material |
||||
|
//@ts-ignore
|
||||
|
this.toteMaterial.map = queueTexture |
||||
|
//@ts-ignore
|
||||
|
this.toteMaterial.color.set(this.defaultUserData.color) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
createPointBasic(item: ItemJson, option?: RendererCudOption): Object3DLike { |
||||
|
return this.pointManager.createPoint(item) |
||||
|
} |
||||
|
|
||||
|
get pointManager(): InstancePointManager { |
||||
|
if (!this.tempViewport) { |
||||
|
throw new Error('tempViewport is not set.') |
||||
|
} |
||||
|
return this.tempViewport.getOrCreatePointManager(this.itemTypeName, () => |
||||
|
// 构建 InstanceMesh 代理对象
|
||||
|
InstancePointManager.create(this.itemTypeName, |
||||
|
this.tempViewport, |
||||
|
this.toteGeometry, |
||||
|
this.toteMaterial, |
||||
|
true, true) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
dispose() { |
||||
|
super.dispose() |
||||
|
if (this.toteGeometry) { |
||||
|
this.toteGeometry.dispose() |
||||
|
this.toteGeometry = undefined |
||||
|
} |
||||
|
if (this.toteMaterial) { |
||||
|
this.toteMaterial.dispose() |
||||
|
this.toteMaterial = undefined |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { defineModule } from '@/core/manager/ModuleManager.ts' |
||||
|
import ToteRenderer from './ToteRenderer.ts' |
||||
|
import ToteEntity from './ToteEntity.ts' |
||||
|
import ToteInteraction from './ToteInteraction.ts' |
||||
|
import propertySetter from './TotePropertySetter.ts' |
||||
|
|
||||
|
export const ITEM_TYPE_NAME = 'tote' |
||||
|
|
||||
|
export default defineModule({ |
||||
|
name: ITEM_TYPE_NAME, |
||||
|
renderer: new ToteRenderer(ITEM_TYPE_NAME), |
||||
|
interaction: new ToteInteraction(ITEM_TYPE_NAME), |
||||
|
setter: propertySetter, |
||||
|
entity: ToteEntity |
||||
|
}) |
||||