Initial commit: 大陆架零部件智能生产线数字孪生系统

- 实现TV1和TV2双大屏展示
- 集成ECharts数据可视化
- 实现实时数据刷新机制
- 添加刀具寿命管理模块
- 添加工单加工时长统计模块
- 添加加工中心7日工作时长统计模块
- 添加OEE分析、设备状态监控等功能
- 创建技术文档和使用说明书
This commit is contained in:
DaLuJia Developer
2026-05-21 09:19:09 +08:00
commit 2abd286d42
243 changed files with 48859 additions and 0 deletions

24
src/App.vue Normal file
View File

@@ -0,0 +1,24 @@
<template>
<div id="app">
<keep-alive>
<router-view v-if="this.$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!this.$route.meta.keepAlive"/>
</div>
</template>
<style>
#app {
font-family: MiSans Bold, serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
/*font-family: Microsoft YaHei!important;*/
}
.el-dialog__wrapper {
pointer-events: none;
}
.el-dialog {
pointer-events: auto;
}
</style>

View File

@@ -0,0 +1,70 @@
@font-face {
font-family: MiSans Demibold;
src: url("MiSans-Demibold.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Bold;
src: url("MiSans-Bold.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans ExtraLight;
src: url("MiSans-ExtraLight.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Heavy;
src: url("MiSans-Heavy.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Light;
src: url("MiSans-Light.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Medium;
src: url("MiSans-Medium.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Normal;
src: url("MiSans-Normal.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Regular;
src: url("MiSans-Regular.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Semibold;
src: url("MiSans-Semibold.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: MiSans Thin;
src: url("MiSans-Thin.ttf");
font-weight: normal;
font-style: normal;
font-display: block;
}

BIN
src/assets/img/jiegang1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,155 @@
class VRButton {
static createButton(renderer, options) {
if (options) {
console.error('THREE.VRButton: The "options" parameter has been removed. Please set the reference space type via renderer.xr.setReferenceSpaceType() instead.')
}
const button = document.createElement('button')
function showEnterVR(/* device*/) {
let currentSession = null
async function onSessionStarted(session) {
session.addEventListener('end', onSessionEnded)
await renderer.xr.setSession(session)
button.textContent = 'EXIT VR'
currentSession = session
}
function onSessionEnded(/* event*/) {
currentSession.removeEventListener('end', onSessionEnded)
button.textContent = 'ENTER VR'
currentSession = null
}
//
button.style.display = ''
button.style.cursor = 'pointer'
button.style.left = 'calc(50% - 50px)'
button.style.width = '100px'
button.textContent = 'ENTER VR'
button.onmouseenter = function() {
button.style.opacity = '1.0'
}
button.onmouseleave = function() {
button.style.opacity = '0.5'
}
button.onclick = function() {
if (currentSession === null) {
// WebXR's requestReferenceSpace only works if the corresponding feature
// was requested at session creation time. For simplicity, just ask for
// the interesting ones as optional features, but be aware that the
// requestReferenceSpace call will fail if it turns out to be unavailable.
// ('local' is always available for immersive sessions and doesn't need to
// be requested separately.)
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] }
navigator.xr.requestSession('immersive-vr', sessionInit).then(onSessionStarted)
} else {
currentSession.end()
}
}
}
function disableButton() {
button.style.display = ''
button.style.cursor = 'auto'
button.style.left = 'calc(50% - 75px)'
button.style.width = '150px'
button.onmouseenter = null
button.onmouseleave = null
button.onclick = null
}
function showWebXRNotFound() {
disableButton()
button.textContent = 'VR NOT SUPPORTED'
}
function showVRNotAllowed(exception) {
disableButton()
console.warn('Exception when trying to call xr.isSessionSupported', exception)
button.textContent = 'VR NOT ALLOWED'
}
function stylizeElement(element) {
element.style.position = 'absolute'
element.style.bottom = '20px'
element.style.padding = '12px 6px'
element.style.border = '1px solid #fff'
element.style.borderRadius = '4px'
element.style.background = 'rgba(0,0,0,0.1)'
element.style.color = '#fff'
element.style.font = 'normal 13px sans-serif'
element.style.textAlign = 'center'
element.style.opacity = '0.5'
element.style.outline = 'none'
element.style.zIndex = '999'
}
if ('xr' in navigator) {
button.id = 'VRButton'
button.style.display = 'none'
stylizeElement(button)
navigator.xr.isSessionSupported('immersive-vr').then(function(supported) {
supported ? showEnterVR() : showWebXRNotFound()
if (supported && VRButton.xrSessionIsGranted) {
button.click()
}
}).catch(showVRNotAllowed)
return button
} else {
const message = document.createElement('a')
if (window.isSecureContext === false) {
message.href = document.location.href.replace(/^http:/, 'https:')
message.innerHTML = 'WEBXR NEEDS HTTPS' // TODO Improve message
} else {
message.href = 'https://immersiveweb.dev/'
message.innerHTML = 'WEBXR NOT AVAILABLE'
}
message.style.left = 'calc(50% - 90px)'
message.style.width = '180px'
message.style.textDecoration = 'none'
stylizeElement(message)
return message
}
}
static xrSessionIsGranted = false;
static registerSessionGrantedListener() {
if ('xr' in navigator) {
navigator.xr.addEventListener('sessiongranted', () => {
VRButton.xrSessionIsGranted = true
})
}
}
}
VRButton.registerSessionGrantedListener()
export { VRButton }

View File

@@ -0,0 +1,327 @@
<template>
<div style="pointer-events: none">
<el-dialog title="修改配置文件" :visible.sync="dialogFormVisible" width="1000px" class="myDia" :modal="false" :close-on-press-escape="false" v-el-drag-dialog>
<el-collapse v-model="activeNames">
<el-collapse-item title="通讯配置" name="1">
<el-row class="row-Show">
<span class="title-show">标题</span>
<el-input size="mini" style="width: 300px" v-model="configInfo['title']"></el-input>
<span class="title-show">工位和协议</span>
<el-input size="mini" style="width: 120px" v-model="configInfo['webSocketOpName']"></el-input>
<el-input size="mini" style="width: 180px" v-model="configInfo['webSocketBSport']"></el-input>
</el-row>
<el-row class="row-Show">
<span class="title-show">通讯类型</span>
<el-radio-group size="mini" v-model="configInfo['connectType']">
<el-radio :label="1">WebSocket</el-radio>
<el-radio :label="2">MQTT</el-radio>
<el-radio :label="3">其他</el-radio>
</el-radio-group>
</el-row>
<el-row class="row-Show">
<span class="title-show">Socket_IP&Port</span>
<el-input size="mini" style="width: 300px" v-model="configInfo['webSocketIp']"></el-input>
<span class="title-show">MQTT_IP&Port</span>
<el-input size="mini" style="width: 200px" v-model="configInfo['MQTTHost']"></el-input>
<el-input size="mini" style="width: 100px" v-model="configInfo['MQTTPort']"></el-input>
</el-row>
<el-row class="row-Show">
<span class="title-show">数据服务器</span>
<el-input size="mini" style="width: 300px" v-model="configInfo['dataBaseURL']"></el-input>
<span class="title-show">node服务</span>
<el-input size="mini" style="width: 300px" v-model="configInfo['NodeRootIp']"></el-input>
</el-row>
</el-collapse-item>
<el-collapse-item name="2">
<template slot="title">
机器人/设备配置
<el-button size="mini" type="warning" style="margin-left: 10px" @click.stop.prevent="dialogFormVisible = false">重置配置文件</el-button>
<el-button size="mini" type="primary" style="margin-left: 10px" @click.stop.prevent="exportConfig">导出配置文件</el-button>
{{ currentUUid }}
</template>
<el-col :span="6">
<el-table
:data="tableDataRobot"
:highlight-current-row="false"
element-loading-text="数据加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.2)"
style="width: 100%"
max-height="300"
border
size="mini"
class="my-table-show"
@cell-dblclick="showEdit"
@row-click="rowClick">
<el-table-column type="index" label=" " align="center" width="40"/>
<!-- <el-table-column align="center" label="name">-->
<!-- <template slot-scope="scope">-->
<!-- {{ scope.row.name }}-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column align="center" label="机器人模型id">
<template slot-scope="scope">
<el-input v-if="scope.row.edit" size="mini" v-model="scope.row.uuid"></el-input>
<span v-else>{{ scope.row.uuid }}</span>
</template>
</el-table-column>
<el-table-column align="center" width="50px" label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" style="color: #00893d" @click="applyRobotConfig(scope.row)">应用</el-button>
<br>
<el-button size="mini" type="text" style="color: red">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col :span="18">
<el-table
:data="RobotChildrenData"
:highlight-current-row="false"
element-loading-text="数据加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.2)"
style="width: 100%"
max-height="300"
border
size="mini">
<el-table-column type="index" label=" " align="center" width="50"/>
<!-- <el-table-column align="center" label="关节名字">-->
<!-- <template slot-scope="scope">-->
<!-- {{ scope.row.name }}-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column align="center" label="关节ID" width="150">
<template slot-scope="scope">
<el-input size="mini" v-model="scope.row.ModelId "></el-input>
</template>
</el-table-column>
<el-table-column align="center" label="type" width="110">
<template slot-scope="scope">
<el-select v-model="scope.row.type" size="mini">
<el-option
v-for="item in robotJointType"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</template>
</el-table-column>
<el-table-column align="center" label="axis" width="80">
<template slot-scope="scope">
<el-select v-model="scope.row.axis" size="mini">
<el-option
v-for="item in robotJointAxis"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</template>
</el-table-column>
<el-table-column align="center" label="flip" width="80">
<template slot-scope="scope">
<el-select v-model="scope.row.flip" size="mini">
<el-option
v-for="item in robotJointFlip"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</template>
</el-table-column>
<el-table-column align="center" label="delta" width="80">
<template slot-scope="scope">
<el-input size="mini" v-model="scope.row.delta "></el-input>
</template>
</el-table-column>
<el-table-column align="center" label="multiple" width="120">
<template slot-scope="scope">
<el-input size="mini" v-model="scope.row.multiple "></el-input>
</template>
</el-table-column>
<el-table-column align="center" width="50px" label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" style="color: #00893d" @click="applyRobotConfig(scope.row, scope.$index)">应用</el-button>
</template>
</el-table-column>
</el-table>
</el-col>
</el-collapse-item>
</el-collapse>
</el-dialog>
</div>
</template>
<script>
import Mousetrap from './js/mousetrap.js'
import axios from 'axios'
export default {
name: 'ConfigVue',
data() {
return {
currentUUid: '',
activeNames: '2',
configInfo: {},
tableDataRobot: [],
tableDataRobotChildren: {},
RobotChildrenData: [],
dialogFormVisible: false,
robotJointType: [
{
label: 'position',
value: 'position'
},
{
label: 'rotation',
value: 'rotation'
}
],
robotJointFlip: [
{
label: 1,
value: 1
},
{
label: -1,
value: -1
}
],
robotJointAxis: [
{
label: 'x',
value: 'x'
},
{
label: 'y',
value: 'y'
},
{
label: 'z',
value: 'z'
}
],
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: ''
},
formLabelWidth: '120px'
}
},
mounted() {
this.getConfig().then(res => {
this.configInfo = res
console.log(res)
this.solveRobotInfoFirst()
})
const scope = this
Mousetrap.bind('ctrl+c', function() {
// scope.solveConfig()
// return false
})
},
methods: {
solveConfig() {
console.log('ctrl + c')
this.dialogFormVisible = !this.dialogFormVisible
},
getConfig() {
return new Promise((resolve, reject) => {
axios.get('./DMT_WEB/config/config.json').then(res => {
const configData = res.data
resolve(configData)
}).catch(err => {
reject(err)
})
})
},
solveRobotInfoFirst() {
this.tableDataRobot = []
this.tableDataRobotChildren = {}
for (const argumentsKey in this.configInfo.robotList) {
this.tableDataRobot.push({
name: '',
uuid: argumentsKey,
edit: false
})
this.tableDataRobotChildren[argumentsKey] = []
for (let i = 0; i < this.configInfo.robotList[argumentsKey].length; i++) {
this.tableDataRobotChildren[argumentsKey].push(this.configInfo.robotList[argumentsKey][i])
}
}
},
showEdit(row) {
row.edit = !row.edit
},
rowClick(row) {
this.currentUUid = row.uuid
this.RobotChildrenData = this.tableDataRobotChildren[row.uuid]
},
exportConfig() {
this.configInfo['robotList'] = this.tableDataRobotChildren
const saveJsonStr = JSON.stringify(this.configInfo)
const blob = new Blob([saveJsonStr], {
type: 'application/json'
})
const objectUrl = URL.createObjectURL(blob)
const aa = document.createElement('a')
aa.href = objectUrl
aa.download = 'config.json'
aa.click()
URL.revokeObjectURL(objectUrl)
},
applyRobotConfig(row, index) {
const rootRobotJoint = window.editor.robotList[this.currentUUid][index]
if (row.ModelId === rootRobotJoint.ModelId) {
window.editor.robotList[this.currentUUid][index].axis = row.axis
window.editor.robotList[this.currentUUid][index].delta = row.delta
window.editor.robotList[this.currentUUid][index].flip = row.flip
window.editor.robotList[this.currentUUid][index].type = row.type
}
}
}
}
</script>
<style scoped>
.row-Show {
margin: 10px 0
}
.title-show {
font-weight: bolder;
font-size: 16px;
margin: 0 10px;
width: 130px;
display: inline-block;
text-align: right;
}
</style>
<style>
.myDia .el-dialog {
pointer-events: auto;
}
.myDia .el-dialog__body {
padding: 5px 15px;
}
.myDia .el-dialog--center .el-dialog__body {
padding: 15px;
}
.myDia .el-dialog__title {
font-size: 22px;
color: #000000;
}
.myDia .el-dialog__header {
padding: 15px;
}
.myDia .el-dialog__wrapper{
pointer-events:none;
}
.myDia .el-dialog{
pointer-events:auto;
}
</style>

View File

@@ -0,0 +1,46 @@
export default{
bind(el, binding) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style = 'cursor:move;'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
} else {
styL = +sty.left.replace(/\px/g, '')
styT = +sty.top.replace(/\px/g, '')
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
const l = e.clientX - disX
const t = e.clientY - disY
// 移动当前元素
dragDom.style.left = `${l + styL}px`
dragDom.style.top = `${t + styT}px`
// 将此时的位置传出去
// binding.value({x:e.pageX,y:e.pageY})
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
}

View File

@@ -0,0 +1,13 @@
import drag from './drag'
const install = function(Vue) {
Vue.directive('el-drag-dialog', drag)
}
if (window.Vue) {
window['el-drag-dialog'] = drag
Vue.use(install); // eslint-disable-line
}
drag.install = install
export default drag

1402
src/components/DMT/index.vue Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,900 @@
<template>
<div style="height: 100%">
<div id="viewPort" style="width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;"></div>
<div ref="modelProcess" style="position: absolute;top: 10px;left: 2px;color: white">正在加载模型0%</div>
<div style="display: none">
<audio controls loop id="alarm">
<source src="./libs/alarm.mp3" type="audio/mpeg">
</audio>
<audio controls id="clash">
<source src="./libs/alarm.mp3" type="audio/mpeg">
</audio>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import axios from 'axios'
import $ from 'jquery'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'
import { DmtControl } from './libs/DmtControl.js'
import { connect } from './js/websocketConnect.js'
import { connectMqtt } from './js/MqttConnect.js'
import { PlaySoundCommand } from './js/PlaySoundCommand.js'
import SpriteText from './js/three-spritetext.module.js'
// eslint-disable-next-line no-unused-vars
let container, clock, scene, camera, axesHelper, renderer, Plane
let modelCount = 0
const editor = {
cameraStateZoom: 25,
addEventListener: THREE.EventDispatcher.prototype.addEventListener,
removeEventListener: THREE.EventDispatcher.prototype.removeEventListener,
dispatchEvent: THREE.EventDispatcher.prototype.dispatchEvent,
hasEventListener: THREE.EventDispatcher.prototype.hasEventListener,
showTagList: [{
title: '工件QY16KC',
ModelId: 'A113AD62-9CE7-4ACF-8D0C-8E8A597A8768',
object: null
}],
ip: '',
WeldLight1: '',
WeldLightVisible1: false,
WeldLight2: '',
WeldLightVisible2: false,
scene: null,
objectIdUUidMap: new Map(),
opname: '',
connectType: 1,
BSport: '',
IsAllowConnect: true,
service: axios.create({
baseURL: './submit/MESCommonBase.ashx',
timeout: 1500000 // 请求超时时间
}),
selected: null,
socket: null,
mqttClient: {
connected: false
},
ProjectCode: null,
sceneConfig: null,
resourcesTree: null,
attachments: null,
resources: null,
cameraState: null,
cameraStateAll: [],
isOk: false,
objectIdSet: new Map(),
animOperations: null,
CraftList: {},
moveTrigger: null,
camera: null,
cameraPosition: 10,
currentButton: 1,
cameraControls: null,
TagActList: {}, // 存放tag对应的类型
worker: {}, // 播放工艺的线程
RotationWorker: {}, // 自传工艺的线程
ModelRotateWorker: {}, // 自传工艺的的状态数据
rotateSpeed: 5 * Math.PI / 180,
getObjectById: null, // 方法 根据ModelId获取模型
solveAttachModel: null, // 方法 处理attach问题
removeObject: null, // 方法 移除模型
setModelWireFrame: null, // 方法 设置线框
setTransparent: null, // 方法 设置透明
showMessage: null, // 方法 显示消息
drawPartTextInfo: null, // 方法 模型上显示文字
ModelLabelList: new Map(), // 模型上显示列表
playSoundCommand: new PlaySoundCommand(), // 方法 警告
attachList: new Map(), // 记录attach列表
objMaterial: new Map(),
WareHouseList: [],
WareHouse: new Map(), // 立库库存表
WareHouseBasicModel: new Map(), // 立库库存基础 零件模型需要命名为warehousepart1-100 立库定位模型需要命名为warehouse1000-1009
ProtocolIdentifiers: {
Robot_7: 'Robot_7',
Device_3: 'Device_3',
Agv_3: 'Agv_3',
Device_1: 'Device_1',
FirstPosition: 'FirstPosition',
WareHousePart: 'WareHousePart',
Robot_6: 'Robot_6',
RobotTcp: 'RobotTcp',
DrawPath: 'DrawPath',
ReLoadData: 'ReLoadData',
PlayAct: 'PlayAct',
CraftCurrOperation: 'CraftCurrOperation',
Position: 'Position',
Select: 'Select',
DragMove: 'DragMove',
ModelTransparent: 'ModelTransparent',
Visible: 'Visible',
Frames: 'Frames',
ModelRotation: 'ModelRotation',
GoHome: 'GoHome',
GetJointAxisInfo: 'GetJointAxisInfo',
GetDeviceBaseTool: 'GetDeviceBaseTool',
GetJointValue: 'GetJointValue',
C_B_ImportModel: 'C_B_ImportModel',
SendURDF: 'SendURDF',
SendDeviceJson: 'SendDeviceJson',
GetDeviceJson: 'GetDeviceJson',
SendMotionInfo: 'SendMotionInfo',
C_B_ImportLayout: 'C_B_ImportLayout',
GetPoseLocation: 'GetPoseLocation',
C_B_SetProjectCode: 'C_B_SetProjectCode',
C_B_TagIDTrigger: 'C_B_TagIDTrigger',
C_B_ReLoad_TagIDHandler: 'C_B_ReLoad_TagIDHandler',
C_B_SetCurrSignalList: 'C_B_SetCurrSignalList',
SendClearModelInfo: 'SendClearModelInfo',
C_B_SaveProjectSimulationData: 'C_B_SaveProjectSimulationData',
C_B_GetProjectSimulationData: 'C_B_GetProjectSimulationData',
C_B_KinematicsEditing: 'C_B_KinematicsEditing',
C_B_SaveModel: 'C_B_SaveModel',
C_B_ObjectAlarm: 'C_B_ObjectAlarm',
C_B_JointMoveTo: 'C_B_JointMoveTo',
JointMoveTo: 'JointMoveTo' // DT|JointMoveTo|modelCode|{"J1":"1","J2":"2","J3":"3","J4":"4","J5":"5","J6":"6","J7":"7","J8":"8","J9":"9"}
},
craftPlaySolveType: {
visible: 'visible',
rotation: 'rotation',
craft: 'craft',
signalwrite: 'signalwrite',
wait: 'wait',
FrameData: 'FrameData',
attach: 'attach',
warning: 'warning'
},
FrameOperation: {
playCraft: true,
visibleObject: true,
rotationObject: true,
writeSignal: true,
waitSignal: true,
attach: true,
warning: true
},
signalSolveType: {
visible: 'visible',
rotation: 'rotation',
craft: 'craft',
targetLocation: 'targetlocation',
attach: 'attach',
warning: 'warning'
}
}
// eslint-disable-next-line no-unused-vars
let ModelListTransParentList = []
const ModelList = []
const ModelListId = []
// eslint-disable-next-line no-unused-vars
const timer = null
// eslint-disable-next-line no-unused-vars
const lastIntersectModel = null
// eslint-disable-next-line no-unused-vars
const onUpPosition = new THREE.Vector2()
var raycaster = new THREE.Raycaster()
var mouse = new THREE.Vector2()
export default {
components: {
},
name: 'DMT',
props: {
containerHeight: {
type: String,
default: '100vh'
},
containerWidth: {
type: String,
default: '100vw'
}
},
data() {
return {
checked: false,
cameraData: [],
object: null,
s: null
}
},
mounted() {
const scope = this
editor.objectIdSet = new Map()
editor.objMaterial = new Map()
editor.attachList = new Map()
this.getConfig().then(res => {
if (Object.hasOwnProperty.call(res, 'robotList')) {
editor.robotList = res.robotList
}
if (Object.hasOwnProperty.call(res, 'WareHouseList')) {
editor.WareHouseList = res.WareHouseList
}
if (Object.hasOwnProperty.call(res, 'transParentList')) {
ModelListTransParentList = res.transParentList
}
if (Object.hasOwnProperty.call(res, 'connectType')) {
editor.connectType = res.connectType
}
editor.mqttHost = res.MQTTHost
editor.mqttPort = res.MQTTPort
editor.ip = res.webSocketIp
editor.opname = res.webSocketOpName
editor.BSport = res.webSocketBSport
editor.rootIp = res.NodeRootIp
this.getProjectConfig()
})
editor.showMessage = function(message, type, time = 2000) {
scope.$message({
message: message,
type: type,
duration: time,
center: true
})
}
editor.addEventListener('dmtModelDBClick', function(event) {
console.log('dmtModelDBClick', event)
})
},
created() {
},
methods: {
getConfig() {
return new Promise((resolve, reject) => {
axios.get('./DMT_WEB/config/config.json').then(res => {
const configData = res.data
resolve(configData)
}).catch(err => {
reject(err)
})
})
},
/**
* 获取项目配置文件,用于初始隐藏模型
*/
getProjectConfig() {
const P1 = new Promise(function(resolve, reject) {
axios({
method: 'get',
url: './DMT_WEB/models/DTDATA.json'
}).then((res) => {
editor.animOperations = {}
editor.moveTrigger = {}
res.data.forEach(n => {
Object.keys(n).forEach(function(key) {
/**
* 获取播放数据并解析数据
*/
if (key === 'OperationTree') {
n[key].forEach(CraftData => {
editor.CraftList[CraftData.CraftCode] = {}
editor.CraftList[CraftData.CraftCode]['CraftName'] = CraftData.CraftName
editor.CraftList[CraftData.CraftCode]['Varieties'] = CraftData.Varieties
editor.CraftList[CraftData.CraftCode]['CurrPlay'] = CraftData.CurrPlay
if (CraftData.CraftPlayData.length < 2) {
editor.animOperations[CraftData.CraftCode] = ''
} else {
editor.animOperations[CraftData.CraftCode] = JSON.parse(CraftData.CraftPlayData)
}
editor.moveTrigger[CraftData.CraftCode] = {
moveStatus: 0,
myVarSetInterval: null,
doAnimation: false,
currentFrame: 0,
pausedFrame: 0,
animOperations: editor.animOperations[CraftData.CraftCode],
simPaceTime: 20,
moveStatusPosition: 0
}
})
}
/**
* HandlerType: 1:显隐,2:自转,3行为
* HandlerCode: 模型或者工艺代码
* HandlerValue: 显隐0/1 ,自传:正负转速&axis,行为暂停播放0/1
* TagValue:信号值
*/
if (key === 'TagTriggerHandler') {
n[key].forEach(TagActListValue => {
if (editor.TagActList[TagActListValue.TagID]) {
editor.TagActList[TagActListValue.TagID].push({
HandlerType: TagActListValue.HandlerType,
HandlerCode: TagActListValue.HandlerCode,
HandlerValue: TagActListValue.HandlerValue,
TagValue: TagActListValue.TagValue,
JointType: TagActListValue.JointType,
Axis: TagActListValue.Axis,
Flip: TagActListValue.Flip,
Value: TagActListValue.Value,
ValueFactor: TagActListValue.ValueFactor
})
} else {
editor.TagActList[TagActListValue.TagID] = []
editor.TagActList[TagActListValue.TagID].push({
HandlerType: TagActListValue.HandlerType,
HandlerCode: TagActListValue.HandlerCode,
HandlerValue: TagActListValue.HandlerValue,
TagValue: TagActListValue.TagValue,
JointType: TagActListValue.JointType,
Axis: TagActListValue.Axis,
Flip: TagActListValue.Flip,
Value: TagActListValue.Value,
ValueFactor: TagActListValue.ValueFactor
})
}
})
}
})
})
resolve(1)
})
})
const P2 = new Promise(function(resolve, reject) {
axios({
method: 'get',
url: './DMT_WEB/models/RobotPlan_Process.json'
}).then((res) => {
editor.sceneConfig = res.data.sceneConfig // 场景配置
editor.attachments = res.data.attachments // 附加物体
editor.resources = res.data.resources // 场景模型资源
editor.resourcesTree = res.data.resourcesTree // 模型树
editor.cameraState = res.data.cameraState // 场景相机位置
editor.ProjectCode = res.data.ProjectCode // 项目代码
editor.resourcesTree.forEach(res => {
if (res.treeType === 'OriginModel') {
ModelList.push(res.name)
}
})
editor.resources.forEach(res => {
editor.objectIdUUidMap.set(res.id, res)
})
editor.attachments.forEach(res => {
editor.attachList.set(res.RootId, res.attachList)
})
resolve(1)
}).catch((error) => { console.log(error) })
})
Promise.all([P1, P2]).then(res => {
this.init()
// this.getCameraData()
})
},
/**
* 设定模型初始状态
*/
setModelSates() {
// 模型的显示隐藏
editor.resourcesTree.forEach(res => {
if (res.visible === '0') {
if (editor.getObjectById(res.id)) {
editor.getObjectById(res.id).visible = !!(parseInt(res.visible))
}
}
})
const treeRes = editor.resourcesTree
for (const treeResValue of treeRes) {
if (treeResValue.treeType !== 'folder') {
const currentModel = editor.getObjectById(treeResValue.id)
if (Object.hasOwnProperty.call(treeResValue, 'WireFrame') && treeResValue.WireFrame) {
editor.setModelWireFrame(currentModel, true)
}
if (Object.hasOwnProperty.call(treeResValue, 'transParent') && treeResValue.transParent) {
editor.setTransparent(treeResValue.id, 0.3)
}
if (currentModel) currentModel.visible = !!parseInt(treeResValue.visible)
}
}
},
/**
* 设定场景初始状态
* @param type
*/
setSceneStates(type) {
switch (type) {
case '0':
scene.background = new THREE.Color('#dcdcdc')
break
case '1':
scene.background = new THREE.Color(editor.sceneConfig.RenderColorModel.color)
break
case '2':
if (editor.sceneConfig.RenderColorModel.image === '') {
editor.scene.background = new THREE.Color(editor.sceneConfig.RenderColorModel.color)
return
}
new THREE.TextureLoader().load(editor.sceneConfig.RenderColorModel.image === '', function(texture) {
scene.background = texture
texture.dispose()
})
break
default:
scene.background = new THREE.Color('#dcdcdc')
break
}
},
/**
* 初始化场景信息
*/
init() {
const scope = this
container = document.getElementById('viewPort')
clock = new THREE.Clock()
scene = new THREE.Scene()
editor.scene = scene
const width = container.clientWidth
const height = container.clientHeight
const k = width / height
const s = 100
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 0, 2000)
camera.position.set(editor.cameraPosition, editor.cameraPosition, editor.cameraPosition)
camera.lookAt(scene.position)
camera.up.x = 0
camera.up.y = 0
camera.up.z = 1
scene.add(camera)
editor.camera = camera
axesHelper = new THREE.AxesHelper(1)
scene.add(axesHelper)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(container.clientWidth, container.clientHeight)
renderer.setClearColor(0x666666, 0)
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 0.8
renderer.shadowMap.enabled = true
renderer.hadowMapEnabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.textureEncoding = THREE.sRGBEncoding
// editor.WeldLight1 = this.drawWeldLight()
// editor.WeldLight2 = this.drawWeldLight()
const pmremGenerator = new THREE.PMREMGenerator(renderer)
pmremGenerator.compileEquirectangularShader()
const loader = new GLTFLoader().setPath('./DMT_WEB/models/')
const dracoLoader = new DRACOLoader()
new RGBELoader().setDataType(THREE.FloatType)
.load('./DMT_WEB/assets/venice_sunset_1k.hdr', function(texture) {
const envMap = pmremGenerator.fromEquirectangular(texture).texture
scene.environment = envMap
scene.background = new THREE.Color('#081f48')
texture.dispose()
pmremGenerator.dispose()
})
this.setSceneStates(editor.sceneConfig.RenderColorModel.model)
dracoLoader.setDecoderPath('./DMT_WEB/draco/')
loader.setDRACOLoader(dracoLoader)
editor.getObjectById = function(ModelId) {
let currentModel = null
const currentModelData = editor.objectIdSet.get(ModelId)
if (currentModelData) currentModel = currentModelData.object
return currentModel
}
editor.solveAttachModel = function(ModelId, attach = true) {
}
editor.removeObject = function(object) {
if (object === null) return
if (object.parent === null) return
object.parent.remove(object)
}
/**
* 设置模型线框
* @param currentObject
* @param WireframeValue
*/
editor.setModelWireFrame = function(currentObject, WireframeValue) {
currentObject.traverse(obj => {
if (obj instanceof THREE.Mesh) {
obj.material = editor.objMaterial.get(obj.id).clone()
obj.material['wireframe'] = WireframeValue
obj.material.needsUpdate = true
}
})
}
/**
* 模型透明
* @param ModelId
* @param Opacity 透明度
*/
editor.setTransparent = function(ModelId, Opacity = 0.3) {
new Promise((resolve, reject) => {
const TransModel = editor.getObjectById(ModelId)
if (!TransModel) {
console.warn('模型不存在setTransparent')
reject(false)
} else {
TransModel.traverse(obj => {
if (obj.material) {
const material_yuan = editor.objMaterial.get(obj.id).clone()
obj.material = material_yuan
if (Opacity !== 1) {
obj.material = new THREE.MeshStandardMaterial({
color: material_yuan.color,
side: 2,
transparent: true,
opacity: Opacity
})
}
}
})
resolve(true)
}
})
}
editor.drawPartTextInfo = function(ModelId, TagValue) {
new Promise((resolve, reject) => {
let LabelInfo = editor.ModelLabelList.get(ModelId)
const TransModel = editor.getObjectById(ModelId)
if (TransModel) {
if (!LabelInfo) {
LabelInfo = {
position: scope.getDeltaPosition(new THREE.Vector3(), { x: 0, y: 0, z: 0.5 }),
label: TagValue,
object: null
}
editor.ModelLabelList.set(ModelId, LabelInfo)
}
if (LabelInfo.object) editor.removeObject(LabelInfo.object)
if (TransModel.visible) {
LabelInfo.label = TagValue.replaceAll('\\n', '\n')
const spriteLabel = new SpriteText(LabelInfo.label, 0.2, '#000000')
// spriteLabel.borderColor = '#30ff00'
// spriteLabel.borderWidth = [0.3, 0.2]
spriteLabel.backgroundColor = 'rgb(255,183,0)'
spriteLabel.position.copy(LabelInfo.position)
LabelInfo.object = spriteLabel
TransModel.add(spriteLabel)
}
}
})
}
container.appendChild(renderer.domElement)
container.addEventListener('dblclick', this.ondblclick, false)
editor.cameraControls = new DmtControl(editor.camera, renderer.domElement)
editor.cameraControls.listenToKeyEvents(window) // optional
editor.cameraControls.update()
const light1 = new THREE.AmbientLight(new THREE.Color('#ffffff'), 1)
light1.name = 'ambient_light'
editor.cameraControls.object.add(light1)
const light2 = new THREE.DirectionalLight(new THREE.Color('#ffffff'), 1)
light2.position.set(0.5, 0, 0.866) // ~60º
light2.name = 'main_light'
editor.cameraControls.object.add(light2)
// document.getElementById('orientCubeWrapperDiv').style.display = 'none'
window.addEventListener('resize', this.onWindowResize, false)
const encoding = THREE.sRGBEncoding
editor.cameraControls.resetState(editor.cameraState.target, editor.cameraState.position, editor.cameraState.zoom0)
// 加载模型
for (let i = 0; i < ModelList.length; i++) {
loader.load(ModelList[i] + '.glb', function(gltf) {
const currentModel = gltf.scene.children[0]
if (currentModel.name === 'Plane') {
Plane = currentModel
}
currentModel.traverse(child => {
if (child['material']) {
child['material'].metalness = 1
if (child['material'].map) child['material'].map.encoding = encoding
if (child['material'].emissiveMap) child['material'].emissiveMap.encoding = encoding
if (child['material'].map || child['material'].emissiveMap) child['material'].needsUpdate = true
editor.objMaterial.set(child.id, child['material'])
}
})
scene.add(currentModel)
scope.setObjectByConfig(currentModel)
ModelListId.push(gltf.scene.id)
modelCount += 1
scope.$refs.modelProcess.innerText = '正在加载模型' + (modelCount / ModelList.length * 100).toFixed(2) + '%'
if (modelCount === ModelList.length) {
scope.$refs.modelProcess.style.display = 'none'
scope.lookObject(scene)
scope.setModelSates()
scope.connectService()
}
}, (xhr) => {
// eslint-disable-next-line handle-callback-err
}, (error) => {
modelCount += 1
console.log(`加载模型${ModelList[i].ModelName}出错!`)
if (modelCount === ModelList.length) {
scope.$refs.modelProcess.style.display = 'none'
scope.lookObject(scene)
scope.setModelSates()
scope.connectService()
}
})
}
this.animate()
},
/**
* 自适应窗口尺寸
*/
onWindowResize() {
renderer.setSize(container.clientWidth, container.clientHeight)
const s = 100; const k = container.clientWidth / container.clientHeight // 窗口宽高比
camera.left = -s * k
camera.right = s * k
camera.top = s
camera.bottom = -s
camera.updateProjectionMatrix()
this.render()
},
/**
* 根据有配置文件设置模型初始化属性
* @param object
*/
setObjectByConfig(object) {
const objectId = object.userData['ModelId']
const objectResource = editor.objectIdUUidMap.get(objectId)
if (objectResource) {
object.position.copy(
new THREE.Vector3(parseFloat(objectResource.location.position.x),
parseFloat(objectResource.location.position.y),
parseFloat(objectResource.location.position.z)
))
object.quaternion.copy(
new THREE.Quaternion(parseFloat(objectResource.location.quaternion.x),
parseFloat(objectResource.location.quaternion.y),
parseFloat(objectResource.location.quaternion.z),
parseFloat(objectResource.location.quaternion.w)
))
object.visible = Boolean(parseInt(objectResource.visible))
}
},
/**
* 渲染
*/
animate() {
this.render()
requestAnimationFrame(this.animate)
editor.attachList.forEach((attackList, ModelId) => {
const ModelAttachParent = editor.getObjectById(ModelId)
if (ModelAttachParent) {
if (!ModelAttachParent.lastMatrixWorld.equals(ModelAttachParent.matrixWorld)) {
for (const attackListElement of attackList) {
const attachModel = editor.getObjectById(attackListElement.ModelId)
if (attachModel) {
const newMatrixWord = ModelAttachParent.matrixWorld.clone()
const cubeTransWorld = newMatrixWord.clone().multiply(ModelAttachParent.lastMatrixWorld.clone().invert()).clone()
const cube2OldWorld = attachModel.matrixWorld.clone()
const cube2NewWorld = cubeTransWorld.clone().multiply(cube2OldWorld)
const cube2ParentWorld = attachModel.parent.matrixWorld.clone()
const cube2ParentNew = (cube2ParentWorld.invert()).multiply(cube2NewWorld)
const position = new THREE.Vector3()
const quaternion = new THREE.Quaternion()
const scale = new THREE.Vector3()
cube2ParentNew.decompose(position, quaternion, scale)
attachModel.quaternion.copy(quaternion)
attachModel.position.copy(position)
attachModel.updateMatrixWorld()
}
}
ModelAttachParent.lastMatrixWorld = ModelAttachParent.matrixWorld.clone()
}
}
})
TWEEN.update()
},
/**
* 渲染
*/
render() {
renderer.render(scene, camera)
this.sendCameraState(camera.zoom < editor.cameraStateZoom)
this.updatePosition(editor.selected)
},
connectService() {
switch (editor.connectType) {
case 1:
connect(editor)
break
case 2:
connectMqtt(editor)
break
default:
break
}
},
// drawWeldLight() {
// const geometry = new THREE.BufferGeometry()
// const vertices = []
// const textureLoader = new THREE.TextureLoader()
// const sprite1 = textureLoader.load('./css/lensflare0.png')
// const x = 0
// const y = 0.3
// const z = -0.07
// const materials = []
// vertices.push(x, y, z)
// geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
// const parameters = [
// [[1.0, 1, 1], sprite1, 50]
// ]
// const sprite = parameters[ 0 ][ 1 ]
// const size = parameters[ 0 ][ 2 ]
//
// materials[ 0 ] = new THREE.PointsMaterial({ size: size, map: sprite, blending: THREE.AdditiveBlending, depthTest: false, transparent: true })
// return new THREE.Points(geometry, materials[0])
// },
/**
* 解析模型
* @param object
*/
lookObject(object) {
object.traverse(function(child) {
if (!child) return
// 避免modelId重复
if (Object.hasOwnProperty.call(child.userData, 'ModelId')) {
if (editor.objectIdSet.has(child.userData['ModelId'])) {
child.userData['ModelId'] = child.uuid
editor.objectIdSet.set(child.uuid, {
ModelId: child.id,
Name: child.name,
position: JSON.parse(JSON.stringify(child.position)),
rotation: JSON.parse(JSON.stringify(child.rotation)),
quaternion: JSON.parse(JSON.stringify(child.quaternion)),
object: child,
matrix: child.matrix.clone(),
lastValue: null
})
} else {
editor.objectIdSet.set(child.userData['ModelId'], {
ModelId: child.id,
Name: child.name,
position: JSON.parse(JSON.stringify(child.position)),
rotation: JSON.parse(JSON.stringify(child.rotation)),
quaternion: JSON.parse(JSON.stringify(child.quaternion)),
object: child,
matrix: child.matrix.clone(),
lastValue: null
})
}
}
})
for (const robotListInfo in editor.robotList) {
for (const info of editor.robotList[robotListInfo]) {
info.object = editor.getObjectById(info.ModelId)
info.matrix = info.object ? info.object.matrix.clone() : null
info.lastValue = null
}
}
object.traverse(function(child) {
child.lastMatrixWorld = child.matrixWorld.clone()
})
window.editor = editor
},
/**
* 获取预置焦点
*/
getCameraData() {
var param = []
param[0] = ['ProjectCode', editor.ProjectCode]
var Data = this.CreateData('3', 'select * from dbo.[基础数据_焦点预置]', param)
this.ExecDatabase(Data).then(response => {
this.cameraData = response.data
})
},
/**
* 点击后 到达预置焦点
*/
setViewCamera() {
if (this.cameraData.length === 0) return
const scope = this
const viewOperation = document.getElementById('viewOperationParent')
const viewOperationChild = document.getElementById('viewOperationChild')
viewOperation.onmouseover = function() {
viewOperationChild.style.display = 'block'
}
viewOperation.onmouseout = function() {
viewOperationChild.style.display = 'none'
}
setTimeout(() => {
for (const info of this.cameraData) {
$('#cameraViewBtn' + info.焦点代码).on('click', function(e) {
const data = JSON.parse(info.焦点位置)
const target = data.target
const position = data.position
const zoom0 = data.zoom0
const id = info.焦点代码
$('#cameraViewBtn' + info.焦点代码).css({ 'background': '#00893d', 'color': '#fff' })
editor.cameraControls.resetState(target, position, zoom0)
for (const infoColor of scope.cameraData) {
if (id !== infoColor.焦点代码) {
$('#cameraViewBtn' + infoColor.焦点代码).css({ 'background': '#fff', 'color': '#606266' })
}
}
})
}
}, 1000)
},
getMousePosition(dom, x, y) {
const rect = dom.getBoundingClientRect()
return [(x - rect.left) / rect.width, (y - rect.top) / rect.height]
},
ondblclick(event) {
try {
const array = this.getMousePosition(container, event.clientX, event.clientY)
onUpPosition.fromArray(array)
let intersects = this.getIntersects(onUpPosition, editor.scene.children)
intersects = intersects.filter(res => {
return res.object.visible
})
if (intersects.length > 0) {
const rootObjId = intersects[0].object.userData['RootId']
editor.selected = editor.getObjectById(rootObjId)
editor.dispatchEvent({
type: 'dmtModelDBClick',
ModelId: editor.selected.userData['ModelId'],
ModelName: editor.selected.userData['name']
})
} else {
editor.selected = null
editor.dispatchEvent({
type: 'dmtModelDBClick',
ModelId: '',
ModelName: ''
})
}
} catch (e) {
editor.selected = null
editor.dispatchEvent({
type: 'dmtModelDBClick',
ModelId: '',
ModelName: ''
})
}
},
getIntersects(point, objects) {
mouse.set((point.x * 2) - 1, -(point.y * 2) + 1)
raycaster.setFromCamera(mouse, camera)
return raycaster.intersectObjects(objects, true)
},
sendCameraState(state) {
this.$emit('changeCameraState', state)// 自定义事件 传递值“子向父组件传值”
},
updatePosition() {
if (editor.selected) {
const targetPosition = editor.selected.getWorldPosition(new THREE.Vector3()).clone().project(camera)
const halfWidth = window.innerWidth / 2
const halfHeight = window.innerHeight / 2
const position = {
left: targetPosition.x * halfWidth + halfWidth - 200,
top: -targetPosition.y * halfHeight + halfHeight - 500
}
this.$emit('clickModel', editor.selected, position)// 自定义事件 传递值“子向父组件传值”
}
},
getDeltaPosition(positionModel, positionLabel) {
return new THREE.Vector3((positionLabel.x - positionModel.x), (positionLabel.y - positionModel.y), (positionLabel.z - positionModel.z))
}
}
}
</script>
<style>
html{
height: 99%;
}
body {
height: 100%;
}
#app {
height: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'
// import { viewCubeUpdate } from './DmtViewCube'
const _changeEvent = { type: 'change' }
class DmtControl extends OrbitControls {
constructor(object, domElement) {
super(object, domElement)
this.cubeControlDirection = {
'TOP': 'TOP',
'BOTTOM': 'BOTTOM',
'FRONT': 'FRONT',
'BACK': 'BACK',
'LEFT': 'LEFT',
'RIGHT': 'RIGHT',
'BACK-LEFT': 'BACK-LEFT',
'FRONT-LEFT': 'FRONT-LEFT',
'TOP-LEFT': 'TOP-LEFT',
'TOP-RIGHT': 'TOP-RIGHT',
'RIGHT-BOTTOM': 'RIGHT-BOTTOM',
'LEFT-BOTTOM': 'LEFT-BOTTOM',
'BACK-BOTTOM': 'BACK-BOTTOM',
'TOP-FRONT': 'TOP-FRONT',
'FRONT-BOTTOM': 'FRONT-BOTTOM',
'TOP-BACK': 'TOP-BACK',
'RIGHT-FRONT': 'RIGHT-FRONT',
'BACK-RIGHT': 'BACK-RIGHT',
'BACK-RIGHT-BOTTOM': 'BACK-RIGHT-BOTTOM',
'TOP-RIGHT-BACK': 'TOP-RIGHT-BACK',
'TOP-LEFT-BACK': 'TOP-LEFT-BACK',
'TOP-LEFT-FRONT': 'TOP-LEFT-FRONT',
'FRONT-BOTTOM-RIGHT': 'FRONT-BOTTOM-RIGHT',
'TOP-RIGHT-FRONT': 'TOP-RIGHT-FRONT',
'BACK-BOTTOM-LEFT': 'BACK-BOTTOM-LEFT',
'FRONT-BOTTOM-LEFT': 'FRONT-BOTTOM-LEFT'
}
const scope = this
const STATE = {
NONE: -1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6
}
// eslint-disable-next-line no-unused-vars
let state = STATE.NONE
this.resetState = function(target0, position0, zoom0) {
scope.target.copy(target0)
scope.object.position.copy(position0)
if (scope.object.isOrthographicCamera) {
scope.object.zoom = zoom0
} else {
scope.object.zoom = 1
}
scope.object.updateProjectionMatrix()
scope.dispatchEvent(_changeEvent)
scope.update()
state = STATE.NONE
// viewCubeUpdate()
}
this.resetStateTween = function(target1, current1, target2, current2, duration) {
const scope = this
const positionVar = {
x1: current1.x,
y1: current1.y,
z1: current1.z,
x2: target1.x,
y2: target1.y,
z2: target1.z
}
// 关闭控制器
scope.enabled = false
var tween = new TWEEN.Tween(positionVar)
tween.to({
x1: current2.x,
y1: current2.y,
z1: current2.z,
x2: target2.x,
y2: target2.y,
z2: target2.z
}, duration)
tween.onUpdate(function() {
scope.object.position.x = positionVar.x1
scope.object.position.y = positionVar.y1
scope.object.position.z = positionVar.z1
scope.target.x = positionVar.x2
scope.target.y = positionVar.y2
scope.target.z = positionVar.z2
scope.object.updateProjectionMatrix()
scope.update()
})
tween.start()
tween.onComplete(function() {
// /开启控制器
scope.enabled = true
})
tween.easing(TWEEN.Easing.Cubic.InOut)
}
this.setLookAt = function(position0, target0, zoom0) {
scope.target.copy(target0)
scope.object.position.copy(position0)
scope.object.zoom = zoom0
scope.object.updateProjectionMatrix()
scope.dispatchEvent(_changeEvent)
scope.update()
state = STATE.NONE
}
this.setLookAt1 = function(target0, rotation) {
scope.target.copy(this.target)
scope.object.rotation.copy(rotation)
scope.object.updateProjectionMatrix()
scope.dispatchEvent(_changeEvent)
scope.update()
state = STATE.NONE
}
this.moveTo = function(x, y, z) {
scope.target.set(x, y, z)
}
this.getState = function() {
return {
target: scope.target.clone(),
position: scope.object.position.clone(),
zoom: parseFloat(scope.object.zoom.toString())
}
}
}
}
export {
DmtControl
}

View File

@@ -0,0 +1,78 @@
import { WebsocketMessageReceive } from './WebSocketMessage.js'
import mqtt from 'mqtt'
function connectMqtt(editor) {
const connection = {
host: editor.mqttHost,
port: editor.mqttPort,
endpoint: '/mqtt',
clean: true, // 保留会话
connectTimeout: 4000, // 超时时间
reconnectPeriod: 4000, // 重连时间间隔
// 认证信息
clientId: 'mqttjs_3be2c321' + new Date().getTime(), // 同一个ID 无法二次连接
username: '',
password: ''
}
const subscription = {
topic: editor.opname,
qos: 0
}
const websocketMessageReceive = new WebsocketMessageReceive(editor)
const { host, port, endpoint, ...options } = connection
const wsHeaderString = location.protocol === 'https:' ? 'wss' : 'ws'
// const connectUrl = `${wsHeaderString}://${location.host}${endpoint}`
const connectUrl = `${wsHeaderString}://${host}:${port}${endpoint}`
console.log(connectUrl)
try {
editor.mqttClient = mqtt.connect(connectUrl, options)
} catch (error) {
console.log('MQTT connect error', error)
}
editor.mqttClient.on('connect', () => {
editor.showMessage('加载完成连接MQTT服务器成功', 'success', 2000)
console.log('MQTT Connection succeeded!')
editor.isOk = true
const { topic, qos } = subscription
editor.mqttClient.subscribe(topic, { qos }, (error, res) => {
if (error) {
console.log('Subscribe to topics error', error)
return
}
websocketMessageReceive.getOriginTagValue()
})
})
editor.mqttClient.on('error', error => {
console.log('MQTT Connection failed', error)
try {
editor.showMessage('MQTT服务器连接失败', 'error', 2000)
editor.socket.close()
if (editor.IsAllowConnect) {
// 显示一下
editor.showMessage('重新连接MQTT服务器', 'warning', 2000)
console.log('重新连接MQTT服务器')
// 重新连接,打开计时器
setTimeout(function() {
connectMqtt(editor)
}, 5000)
}
} catch (exception) {
const error = exception.toString()
editor.showMessage.error('MQTT服务器出现错误,关闭连接' + error, 'error', 2000)
}
})
// 接收消息
editor.mqttClient.on('message', (topic, message) => {
try {
websocketMessageReceive.websocketMessage({ data: message.toString() })
} catch (exception) {
console.log(exception.toString())
}
})
editor.mqttClient.on('close', () => {
editor.mqttClient.end()
})
}
export { connectMqtt }

View File

@@ -0,0 +1,40 @@
import * as THREE from 'three'
/**
* @param editor Editor
* @param object THREE.Object3D
* @constructor
*/
class ObjectAlarmCommand {
constructor(editor, ModelId) {
this.type = 'ObjectAlarmCommand'
this.ModelId = ModelId
this.editor = editor
this.object = editor.getObjectById(ModelId)
}
exec(isAlarm = true) {
if (!this.object) return
if (isAlarm) {
this.object.traverse(object => {
if (object instanceof THREE.Mesh && !Object.hasOwnProperty.call(object.userData, 'isAxis')) {
object.material.color = new THREE.Color('#ff0000')
object.material.side = 2
object.material.transparent = true
object.material.opacity = 0.8
}
})
// this.editor.playSoundCommand.exec('Alarm', true, this.ModelId)
} else {
this.object.traverse(object => {
if (object instanceof THREE.Mesh) {
const m = this.editor.objMaterial.get(object.id)
if (m)object.material = m.clone()
}
})
// this.editor.playSoundCommand.exec('Alarm', false, this.ModelId)
}
}
}
export { ObjectAlarmCommand }

View File

@@ -0,0 +1,58 @@
/**
* @param editor Editor
* @param object THREE.Object3D
* @constructor
*/
class PlaySoundCommand {
constructor() {
this.type = 'PlaySoundCommand'
this.PlayType = {
Alarm: 'Alarm',
Clash: 'Clash'
}
this.AlarmList = []
this.isAlarm = false
}
/**
* 系统声音
* @param type 报警Alarm 碰撞Clash 。。。。。。。
* @param state 报警/解除报警
* @param ModelId 模型ID
*/
exec(type, state = true, ModelId) {
switch (type) {
case this.PlayType.Alarm:
if (state) {
if (!this.isAlarm) this.playAlarmSound(true)
if (this.AlarmList.indexOf(ModelId) === -1) this.AlarmList.push(ModelId)
this.isAlarm = true
} else {
this.AlarmList = this.AlarmList.filter(res => { return res !== ModelId })
if (this.AlarmList.length === 0) {
this.playAlarmSound(false)
this.isAlarm = false
}
}
break
case this.PlayType.Clash:
break
default:
break
}
}
/**
*
*/
playAlarmSound(play = true) {
// const audio = document.getElementById('alarm')
// play ? audio.play() : audio.pause()
}
playClashSound(play) {
const audio = document.getElementById('clash')
play ? audio.play() : audio.pause()
}
}
export { PlaySoundCommand }

View File

@@ -0,0 +1,629 @@
import * as THREE from 'three'
import { ObjectAlarmCommand } from './ObjectAlarmCommand.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'
const WebsocketMessageReceive = function(editor) {
const scope = this
this.websocketMessage = (msg) => {
new Promise((resolve, reject) => {
let returnMes
const arrays_signal = msg.data.split('|')
const m_SourceType = arrays_signal[0] // 数据类型DT数字孪生数据
const m_DeviceType = arrays_signal[1] // 设备类型Robot机器人类型的设备Device普通设备
const m_DeviceName = arrays_signal[2] // 设备名称
const m_DevicePartName = arrays_signal[3] // 设备运动部件
const m_DevicePartDate = arrays_signal[4] // 设备运动部件数据
switch (m_SourceType) {
case 'DT':
switch (m_DeviceType) {
case editor.ProtocolIdentifiers.Robot_7: // 接受机器人运动数据进行播放 --------------------------现场用----------------
returnMes = scope.deviceMoveEvent.RobotTranslation7(m_DeviceName, m_DevicePartName, m_DevicePartDate)
break
case editor.ProtocolIdentifiers.Device_3:
console.log('设备运行')// --------------------------现场用----------------
returnMes = scope.deviceMoveEvent.DeviceThreeAxis(m_DeviceName, m_DevicePartName, m_DevicePartDate)
break
case editor.ProtocolIdentifiers.Agv_3:// --------------------------现场用----------------
console.log('AGV运行')
returnMes = scope.deviceMoveEvent.AGVThreeAxis(m_DeviceName, m_DevicePartName, m_DevicePartDate)
break
case editor.ProtocolIdentifiers.Device_1: // 接受设备运动数据进行播放,已更新
break
case editor.ProtocolIdentifiers.FirstPosition: // 接受第一个点的数据,瞬间跳到该点,已更新
break
case editor.ProtocolIdentifiers.WareHousePart: //
returnMes = scope.drawWareHousePartOne(m_DeviceName)
break
case editor.ProtocolIdentifiers.Robot_6: // 接受机器人运动数据进行播放
returnMes = scope.deviceMoveEvent.RobotTranslation(m_DeviceName, m_DevicePartName, m_DevicePartDate)
break
case editor.ProtocolIdentifiers.ReLoadData: // 加载所有要播放的数据,避免资源浪费 ***********************UUID测试好用了******************************
break
case editor.ProtocolIdentifiers.PlayAct: // 根据ID 播放数据 状态为1 0暂停 ***********************UUID测试好用了******************************
scope.playActData(m_DeviceName, parseInt(m_DevicePartName))
break
// case editor.ProtocolIdentifiers.ModelTransparent: // 根据传过来的模型名字和透明度 改变模型的透明度 ***********************UUID测试好用了******************************
// returnMes = scope.robotPlan_ModelOperation.modelTrans(m_DeviceName, m_DevicePartName)
// break
case editor.ProtocolIdentifiers.Visible: // 接受CS指令显示、隐藏模型 ***********************UUID测试好用了******************************
returnMes = scope.visibleObjectToBS(m_DeviceName, m_DevicePartName)
break
case 'JointMoveTo':
if (!JSON.parse(m_DevicePartName)) console.log('JointMoveTo数据不对' + m_DevicePartName)
this.JointMoveTo(m_DeviceName, JSON.parse(m_DevicePartName))
break
case editor.ProtocolIdentifiers.C_B_TagIDTrigger:
{
const tag = JSON.parse(m_DeviceName)
scope.solveTagValue(tag.TagID, tag.TagValue)
}
break
case editor.ProtocolIdentifiers.C_B_ObjectAlarm:
{
const Info = JSON.parse(m_DeviceName)
console.log(Info)
for (const infoElement of Info) {
new ObjectAlarmCommand(editor, infoElement.ModelId).exec(!!parseInt(infoElement.value))
}
}
break
case editor.ProtocolIdentifiers.C_B_JointMoveTo:
{
const MoveValueList = JSON.parse(m_DeviceName)
scope.ModelJointMoveTo(MoveValueList)
}
break
case 'showWeld':
this.showWeld(m_DeviceName, m_DevicePartName)
break
case 'showCheckOk':
this.showCheckOk(m_DeviceName, m_DevicePartName)
break
default:
returnMes = '不存在协议:' + msg
break
}
break
default:
returnMes = '不存在协议:' + msg
break
}
resolve(returnMes)
})
}
/**
* HandlerType: 1:显隐,2:自转,3:行为,4:私服位置5:附加拆离指令
* HandlerCode: 模型或者工艺代码
* HandlerValue: 显隐0/1 ,自传:正负转速&axis,行为暂停播放0/1
* TagValueTagValue: 信号值
* @param tagId
* @param tagValue
*/
this.solveTagValue = (tagId, tagValue) => {
const scope = this
const actionInfo = editor.TagActList[tagId]
if (!actionInfo) return
for (const actionInfoElement of actionInfo) {
switch (actionInfoElement.HandlerType) {
case editor.signalSolveType.warning: // 报警
if (tagValue === actionInfoElement.TagValue) {
new ObjectAlarmCommand(editor, actionInfoElement.HandlerCode).exec(!!parseInt(actionInfoElement.HandlerValue))
}
break
case editor.signalSolveType.visible: // 显隐
if (tagValue === actionInfoElement.TagValue) {
scope.visibleObjectToBS(actionInfoElement.HandlerCode, parseInt(actionInfoElement.HandlerValue))
}
break
case editor.signalSolveType.rotation: // 自转
if (tagValue === actionInfoElement.TagValue) {
const ValueFactor = actionInfoElement.ValueFactor
const Axis = actionInfoElement.Axis
// const JointType = actionInfoElement.JointType
const Flip = actionInfoElement.Flip
const ModelCode = actionInfoElement.HandlerCode
scope.modelRotation(ModelCode, Axis, Flip * parseFloat(ValueFactor))
}
break
case editor.signalSolveType.craft: // 行为
if (tagValue === actionInfoElement.TagValue) {
scope.playActData(actionInfoElement.HandlerCode, parseInt(actionInfoElement.HandlerValue))
}
break
case editor.signalSolveType.targetLocation: // 伺服位置
scope.solveServoSignal(actionInfoElement, tagValue)
break
case editor.signalSolveType.attach: // 附加功能
console.log('这个地方的附加功能没用写')
break
default:
break
}
}
}
/**
* 根据ACTid播放动作开启线程
* @param actID
* @param state
*/
this.playActData = (actID, state) => {
const scope = this
if (!editor.isOk) return
if (!editor.worker[actID]) editor.worker[actID] = new Worker('./DMT_WEB/webWorker/playFunction.js')
if (editor.moveTrigger[actID].moveStatus !== state) {
editor.worker[actID].postMessage({
ActID: actID,
State: state,
isOk: editor.isOk,
craftPlaySolveType: editor.craftPlaySolveType,
moveTrigger: JSON.stringify(editor.moveTrigger[actID])
})
editor.moveTrigger[actID].moveStatus = state
}
editor.worker[actID].onmessage = function(event) {
editor.moveTrigger[actID].myVarSetInterval = event.data.myVarSetInterval
editor.moveTrigger[actID].currentFrame = event.data.pausedFrame
const currentFrameData = event.data.currentFrameData
const frameType = event.data.frameType
editor.solveAttachModel('', true)
switch (frameType) {
case editor.craftPlaySolveType.attach:
if (editor.FrameOperation.attach) {
const position = currentFrameData['AttachToModelCode']
const List_AttachModel = currentFrameData['List_AttachModel']
const IsTwoWay = currentFrameData['IsTwoWay']
const IsAttach = currentFrameData['IsAttach']
if (IsAttach) {
const attachData = []
for (const editorElement of List_AttachModel) {
attachData.push({
ModelId: editorElement.ModelCode,
ModelName: editorElement.ModelName,
RootId: position,
IsTwoWay: IsTwoWay
})
}
editor.attachList.set(position, attachData)
} else {
for (const detachData of List_AttachModel) {
const detachModelCode = detachData.ModelCode
editor.scene.attach(editor.getObjectById(detachModelCode))
editor.attachList.forEach((value, key) => {
const attachList = value.filter(res => { return res.ModelId !== detachModelCode })
editor.attachList.set(key, attachList)
})
}
}
}
break
case editor.craftPlaySolveType.visible:
if (editor.FrameOperation.visibleObject) {
const position = currentFrameData['ModelCode']
const Visible = parseInt(currentFrameData['Visible'])
scope.visibleObjectToBS(position, Visible)
}
break
case editor.craftPlaySolveType.rotation:
if (editor.FrameOperation.rotationObject) {
const position = currentFrameData['ModelCode']
const Axis = currentFrameData['Axis']
const Rate = currentFrameData['Rate']
scope.modelRotation(position, Axis, Rate)
}
break
case editor.craftPlaySolveType.craft:
if (editor.FrameOperation.playCraft) {
const position = currentFrameData['CraftCode']
const craftState = parseInt(currentFrameData['CraftValue'])
scope.playActData(position, craftState)
}
break
case editor.craftPlaySolveType.FrameData:
{
const position = currentFrameData['i']
const objectToMove = editor.getObjectById(position)
if (objectToMove) {
if (currentFrameData.tx === 'NaN' || currentFrameData.ty === 'NaN' || currentFrameData.tz === 'NaN' || currentFrameData.qx === 'NaN' || currentFrameData.qy === 'NaN' || currentFrameData.qz === 'NaN' || currentFrameData.qw === 'NaN') {
console.log('craftDataError')
// editor.robotPlan_MessageList.execute(position, 'craftDataError')
} else {
const movePosition = new THREE.Vector3(parseFloat(currentFrameData.tx), parseFloat(currentFrameData.ty), parseFloat(currentFrameData.tz))
const nq = new THREE.Quaternion(parseFloat(currentFrameData.qx), parseFloat(currentFrameData.qy), parseFloat(currentFrameData.qz), parseFloat(currentFrameData.qw))
objectToMove.quaternion.copy(nq)
objectToMove.position.copy(movePosition)
}
}
}
break
case editor.craftPlaySolveType.warning: // 报警
if (editor.FrameOperation.warning) {
new ObjectAlarmCommand(scope.editor, currentFrameData['ModelCode']).exec(!!parseInt(currentFrameData['Visible']))
}
break
default:
delete editor.worker[event.data.ActID]
editor.moveTrigger[actID].moveStatus = 0
editor.moveTrigger[actID].myVarSetInterval = null
editor.moveTrigger[actID].currentFrame = 0
editor.moveTrigger[actID].pausedFrame = 0
break
}
editor.solveAttachModel('', false)
}
}
/**
* 接受CS模型自转的指令
* 接受CS模型自转的指令
* @param objectNames
* @param values
* @param axis
*/
this.modelRotation = (objectNames, axis, values) => {
const scope = this
const EP = 0.000001
let dir = 0
if (!editor.getObjectById(objectNames)) return
const currentModel = editor.getObjectById(objectNames)
if ((parseFloat(values) >= -EP) && (parseFloat(values) <= EP)) {
if (editor.RotationWorker[objectNames]) {
editor.RotationWorker[objectNames].terminate()
delete editor.RotationWorker[objectNames]
delete editor.ModelRotateWorker[objectNames]
}
return
} else {
dir = 1
if (editor.RotationWorker[objectNames]) {
editor.RotationWorker[objectNames].terminate()
delete editor.RotationWorker[objectNames]
delete editor.ModelRotateWorker[objectNames]
}
}
editor.RotationWorker[objectNames] = new Worker('webWorker/modelRotation.js')
if (!editor.ModelRotateWorker[objectNames]) {
editor.ModelRotateWorker[objectNames] = {
state: 0,
myVarSetInterval: null,
doAnimation: false,
ModelCode: objectNames
}
}
editor.RotationWorker[objectNames].postMessage({
ObjectNames: objectNames,
Values: values,
Dir: dir,
ModelRotateWorker: editor.ModelRotateWorker[objectNames],
RotateSpeed: editor.rotateSpeed,
RotationValue: currentModel.rotation[axis]
})
editor.RotationWorker[objectNames].onmessage = (event) => {
scope.modelSelfRotation(currentModel, event.data.rotation, axis)
}
}
this.modelSelfRotation = (currentModel, rotation, axis) => {
currentModel.rotation[axis] = rotation
}
this.ModelJointMoveTo = (MoveValueList) => {
// TOBS?WEB?SCADAMain?DT|C_B_JointMoveTo|[{"ModelCode":"5E6D91B8-B44D-4C44-AAEF-10492E548A1A","JointType":"rotation","Axis":"x","Flip":1,"Value":0,"ValueFactor":0.017453292519943295,"Delta":0}]
// TOBS?WEB?SCADAMain?DT|C_B_JointMoveTo|[{"ModelCode":"644506B1-A05A-4E57-BBA2-C63635586C63","JointType":"position","Axis":"y","Flip":-1,"Value":-15.0,"ValueFactor":0.001,"Delta":-655.0}]
new Promise(resolve => {
for (const MoveValue of MoveValueList) {
const ModelCode = MoveValue.ModelCode
const ValueFactor = parseFloat(MoveValue.ValueFactor)
const type = MoveValue.JointType
const Value = parseFloat(MoveValue.Value)
const Flip = parseFloat(MoveValue.Flip)
const axis = MoveValue.Axis
const Delta = parseFloat(MoveValue.Delta)
const object = editor.getObjectById(ModelCode)
const originInfo = editor.objectIdSet.get(ModelCode)
if (!originInfo) return
const matrix = originInfo['matrix']
const originValue = type === 'rotation' ? originInfo[type]['_' + axis] : originInfo[type][axis]
const lastValue = originInfo['lastValue'] === null ? originValue : originInfo['lastValue']
const moveValue = Flip * (Value - Delta) * ValueFactor
if (object) {
editor.solveAttachModel('', true)
const JointValue = originValue + moveValue
new TWEEN.Tween({
moveValue: lastValue
}).to({ moveValue: JointValue }, 100)
.onStart(function() {
originInfo['lastValue'] = JointValue
}).onUpdate(function() {
switch (type) {
case 'position':
object[type][axis] = this._object.moveValue
break
case 'rotation':
{
const moveAxis = {
x: new THREE.Vector3(1, 1, 1),
y: new THREE.Vector3(1, 1, 1),
z: new THREE.Vector3(1, 1, 1)
}
matrix.extractBasis(moveAxis.x, moveAxis.y, moveAxis.z)
const nM = new THREE.Matrix4()
nM.makeRotationAxis(moveAxis[axis], this._object.moveValue)
nM.multiply(matrix)
object.rotation.setFromRotationMatrix(nM)
object.updateWorldMatrix()
}
break
}
}).start()
editor.solveAttachModel('', false)
} else {
console.log('模型不存在')
}
}
})
}
/**
* 接伺服类的数据
* @param ModelInfo
* @param tagValue
*/
this.solveServoSignal = (ModelInfo, tagValue) => {
try {
const type = ModelInfo.JointType
const axis = ModelInfo.Axis
const flip = ModelInfo.Flip
const Value = ModelInfo.Value
const ValueFactor = ModelInfo.ValueFactor // 倍率
const ModelId = ModelInfo.HandlerCode // 模型代码
const object = editor.getObjectById(ModelId)
const originInfo = editor.objectIdSet.get(ModelId)
if (!originInfo) return
const matrix = originInfo['matrix']
const originValue = type === 'rotation' ? originInfo[type]['_' + axis] : originInfo[type][axis]
const lastValue = originInfo['lastValue'] === null ? originValue : originInfo['lastValue']
const moveValue = flip * (parseFloat(tagValue) - Value) * ValueFactor
if (object) {
editor.solveAttachModel('', true)
const JointValue = originValue + moveValue
new TWEEN.Tween({
moveValue: lastValue
}).to({ moveValue: JointValue }, 100)
.onStart(function() {
originInfo['lastValue'] = JointValue
})
.onUpdate(function() {
switch (type) {
case 'position':
object[type][axis] = this._object.moveValue
break
case 'rotation':
{
const moveAxis = {
x: new THREE.Vector3(1, 1, 1),
y: new THREE.Vector3(1, 1, 1),
z: new THREE.Vector3(1, 1, 1)
}
matrix.extractBasis(moveAxis.x, moveAxis.y, moveAxis.z)
const nM = new THREE.Matrix4()
nM.makeRotationAxis(moveAxis[axis], this._object.moveValue)
// nM.makeRotationAxis(moveAxis[axis], JointValue)
nM.multiply(matrix)
object.rotation.setFromRotationMatrix(nM)
object.updateWorldMatrix()
}
break
}
}).start()
editor.solveAttachModel('', false)
}
//
// if (object) {
// editor.solveAttachModel('', true)
// object[type][axis] = originValue + (ValueFactor * flip * parseFloat(tagValue) + Value)
// editor.solveAttachModel('', false)
// }
} catch (e) {
console.log('solveServoSignal错误')
}
}
/**
* 接受机器人类的数据
* @param ModelId
* @param JointData
* @constructor
*/
this.JointMoveTo = (ModelId, JointData = {}) => {
if (editor.robotList[ModelId]) {
const ModelList = editor.robotList[ModelId]
// window.updateRobotData(ModelId, JointData)
// window.updateLabelInfoJoint(ModelId, JointData)
for (let i = 0; i < ModelList.length; i++) {
if (ModelList[i].ModelId === '') return
this.jointMoveToPromise(JointData, ModelList[i], i, ModelId)
}
}
}
this.jointMoveToPromise = (JointData, ModelListData, i, ModelId) => {
new Promise(resolve => {
const ModelIdJoint = ModelListData.ModelId
const object = ModelListData.object
const type = ModelListData.type
const axis = ModelListData.axis
const matrix = ModelListData.matrix
const flip = ModelListData.flip
const delta = parseFloat(ModelListData.delta)
const multiple = ModelListData.multiple
const originInfo = editor.objectIdSet.get(ModelIdJoint)
if (!originInfo) return
const originValue = type === 'rotation' ? originInfo[type]['_' + axis] : originInfo[type][axis]
const lastValue = ModelListData.lastValue === null ? originValue : ModelListData.lastValue
const moveValue = flip * (parseFloat(JointData['J' + (i + 1)]) - delta) * multiple
if (object) {
const JointValue = originValue + moveValue
new TWEEN.Tween({
moveValue: lastValue
}).to({ moveValue: JointValue }, 200)
.onStart(function() {
editor.robotList[ModelId][i]['lastValue'] = JointValue
})
.onUpdate(function() {
switch (type) {
case 'position':
object[type][axis] = this._object.moveValue
break
case 'rotation':
{
const moveAxis = {
x: new THREE.Vector3(1, 1, 1),
y: new THREE.Vector3(1, 1, 1),
z: new THREE.Vector3(1, 1, 1)
}
matrix.extractBasis(moveAxis.x, moveAxis.y, moveAxis.z)
const nM = new THREE.Matrix4()
nM.makeRotationAxis(moveAxis[axis], this._object.moveValue)
// nM.makeRotationAxis(moveAxis[axis], JointValue)
nM.multiply(matrix)
object.rotation.setFromRotationMatrix(nM)
object.updateWorldMatrix()
}
break
}
}).start()
}
})
}
this.getAxisValue = (axis, value) => {
return JSON.parse('{"' + axis + '":' + value + '}')
}
this.getAxisValue = (axis, value) => {
return JSON.parse('{"' + axis + '":' + value + '}')
}
this.showWeld = (ModelId, Welding) => {
// 徐工专用
if (ModelId === '46DDBCD8-D775-49A4-BB14-30015FF87595') editor.WeldLightVisible1 = !!parseInt(Welding)
if (ModelId === '1E96DED2-6EEF-469F-BFE6-8F52FB62C2B0') editor.WeldLightVisible2 = !!parseInt(Welding)
}
this.showCheckOk = (check) => {
// 徐工专用
if (parseInt(check)) {
editor.showTagList[3].title = '工件QY16KC, 检测OK'
} else {
editor.showTagList[3].title = '工件QY16KC'
}
}
/**
* 显示隐藏物体
* @param m_DeviceName
* @param isVisible
*/
this.visibleObjectToBS = (m_DeviceName, isVisible) => {
const currentModel = editor.getObjectById(m_DeviceName)
if (currentModel) {
currentModel.visible = !!parseInt(isVisible)
currentModel.traverse(res => {
res.visible = !!parseInt(isVisible)
})
currentModel.userData['visible'] = !!parseInt(isVisible)
}
}
/**
* 初始化获取TagValue
*/
this.getOriginTagValue = () => {
let msg = ''
let socketMsg = ''
const keyWord = 'B_C_DT_WebLoaded'
msg = 'DT|' + keyWord
socketMsg = editor.BSport + msg
const topic = editor.BSport.split('?')[2]
switch (editor.connectType) {
case 1:
editor.socket.send(socketMsg)
break
case 2:
editor.mqttClient.publish(topic, msg, 0, error => {
if (error) {
console.log('Publish error', error)
}
})
break
default:
break
}
}
/**
* 更新库存状态
* @param dataInfo
*/
this.drawWareHousePartOne = (dataInfo) => {
const scope = this
try {
const whInfo = JSON.parse(dataInfo)
for (const whStoreInfo of whInfo) {
const whStoreNum = whStoreInfo.Key // 库位号 唯一值
const whStoreState = whStoreInfo.Value // 立库状态
const whNum = whStoreNum.toString().substring(0, 4) // 立库号 1000一期立库 1003刀具库 1002三坐标测量库 1001机床线旁库
const currentStoreInfo = editor.WareHouse.get(whStoreNum)
if (currentStoreInfo) {
if (parseInt(whStoreState) === 0) {
// 0的时候直接移除库存
editor.removeObject(currentStoreInfo.object)
editor.WareHouse.delete(whStoreNum)
} else {
// 不一样的状态 更新
if (currentStoreInfo.whStoreState !== whStoreState) {
// 移除零件,更新库存信息信息
editor.removeObject(currentStoreInfo.object)
scope.updateStoreModel(whNum, whStoreNum, whStoreState)
currentStoreInfo.whStoreState = whStoreState
currentStoreInfo.object = null
}
}
} else {
editor.WareHouse.set(whStoreNum, {
whNum: whNum,
whStoreNum: whStoreNum,
whStoreState: whStoreState,
object: null
})
scope.updateStoreModel(whNum, whStoreNum, whStoreState)
}
}
} catch (e) {
console.log('库存解析错误')
}
}
/**
* 更新零件
* @param whNum
* @param whStoreNum
* @param whStoreState
*/
this.updateStoreModel = (whNum, whStoreNum, whStoreState) => {
try {
const basicPartModel = editor.WareHouseBasicModel.get('warehousepart' + whStoreState)
const basicWareHouseModelData = editor.WareHouseBasicModel.get('warehouse' + whNum)
if (basicPartModel && basicWareHouseModelData) {
const basicPartModelClone = basicPartModel.clone()
const basicPartModelPosition = basicWareHouseModelData[whStoreNum]
basicPartModelClone.position.copy(basicPartModelPosition.getWorldPosition(new THREE.Vector3()))
editor.scene.add(basicPartModelClone)
editor.WareHouse.get(whStoreNum).object = basicPartModelClone
}
} catch (e) {
console.log('库存模型解析错误')
}
}
}
export {
WebsocketMessageReceive
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
import { LinearFilter, Sprite, SpriteMaterial, Texture } from 'three'
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', {
writable: false
})
return Constructor
}
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function')
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
})
Object.defineProperty(subClass, 'prototype', {
writable: false
})
if (superClass) _setPrototypeOf(subClass, superClass)
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o)
}
return _getPrototypeOf(o)
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p
return o
}
return _setPrototypeOf(o, p)
}
function _isNativeReflectConstruct() {
if (typeof Reflect === 'undefined' || !Reflect.construct) return false
if (Reflect.construct.sham) return false
if (typeof Proxy === 'function') return true
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {}))
return true
} catch (e) {
return false
}
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
}
return self
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === 'object' || typeof call === 'function')) {
return call
} else if (call !== void 0) {
throw new TypeError('Derived constructors may only return object or undefined')
}
return _assertThisInitialized(self)
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct()
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived)
var result
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor
result = Reflect.construct(Super, arguments, NewTarget)
} else {
result = Super.apply(this, arguments)
}
return _possibleConstructorReturn(this, result)
}
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest()
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread()
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr)
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr
}
function _iterableToArray(iter) {
if (typeof Symbol !== 'undefined' && iter[Symbol.iterator] != null || iter['@@iterator'] != null) return Array.from(iter)
}
function _iterableToArrayLimit(arr, i) {
var _i = arr == null ? null : typeof Symbol !== 'undefined' && arr[Symbol.iterator] || arr['@@iterator']
if (_i == null) return
var _arr = []
var _n = true
var _d = false
var _s, _e
try {
for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value)
if (i && _arr.length === i) break
}
} catch (err) {
_d = true
_e = err
} finally {
try {
if (!_n && _i['return'] != null) _i['return']()
} finally {
if (_d) throw _e
}
}
return _arr
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return
if (typeof o === 'string') return _arrayLikeToArray(o, minLen)
var n = Object.prototype.toString.call(o).slice(8, -1)
if (n === 'Object' && o.constructor) n = o.constructor.name
if (n === 'Map' || n === 'Set') return Array.from(o)
if (n === 'Arguments' || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen)
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]
return arr2
}
function _nonIterableSpread() {
throw new TypeError('Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.')
}
function _nonIterableRest() {
throw new TypeError('Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.')
}
var three = typeof window !== 'undefined' && window.THREE ? window.THREE // Prefer consumption from global THREE, if exists
: {
LinearFilter: LinearFilter,
Sprite: Sprite,
SpriteMaterial: SpriteMaterial,
Texture: Texture
}
var _default = /* #__PURE__*/(function(_three$Sprite) {
_inherits(_default, _three$Sprite)
var _super = _createSuper(_default)
function _default() {
var _this
var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''
var textHeight = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10
var color = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'rgba(255, 255, 255, 1)'
_classCallCheck(this, _default)
_this = _super.call(this, new three.SpriteMaterial())
_this._text = ''.concat(text)
_this._textHeight = textHeight
_this._color = color
_this._backgroundColor = false // no background color
_this._padding = 0
_this._borderWidth = 0
_this._borderRadius = 0
_this._borderColor = 'white'
_this._strokeWidth = 0
_this._strokeColor = 'white'
_this._fontFace = 'Arial'
_this._fontSize = 90 // defines text resolution
_this._fontWeight = 'normal'
_this._canvas = document.createElement('canvas')
_this.material.depthTest = false
_this._genCanvas()
return _this
}
_createClass(_default, [{
key: 'text',
get: function get() {
return this._text
},
set: function set(text) {
this._text = text
this._genCanvas()
}
}, {
key: 'textHeight',
get: function get() {
return this._textHeight
},
set: function set(textHeight) {
this._textHeight = textHeight
this._genCanvas()
}
}, {
key: 'color',
get: function get() {
return this._color
},
set: function set(color) {
this._color = color
this._genCanvas()
}
}, {
key: 'backgroundColor',
get: function get() {
return this._backgroundColor
},
set: function set(color) {
this._backgroundColor = color
this._genCanvas()
}
}, {
key: 'padding',
get: function get() {
return this._padding
},
set: function set(padding) {
this._padding = padding
this._genCanvas()
}
}, {
key: 'borderWidth',
get: function get() {
return this._borderWidth
},
set: function set(borderWidth) {
this._borderWidth = borderWidth
this._genCanvas()
}
}, {
key: 'borderRadius',
get: function get() {
return this._borderRadius
},
set: function set(borderRadius) {
this._borderRadius = borderRadius
this._genCanvas()
}
}, {
key: 'borderColor',
get: function get() {
return this._borderColor
},
set: function set(borderColor) {
this._borderColor = borderColor
this._genCanvas()
}
}, {
key: 'fontFace',
get: function get() {
return this._fontFace
},
set: function set(fontFace) {
this._fontFace = fontFace
this._genCanvas()
}
}, {
key: 'fontSize',
get: function get() {
return this._fontSize
},
set: function set(fontSize) {
this._fontSize = fontSize
this._genCanvas()
}
}, {
key: 'fontWeight',
get: function get() {
return this._fontWeight
},
set: function set(fontWeight) {
this._fontWeight = fontWeight
this._genCanvas()
}
}, {
key: 'strokeWidth',
get: function get() {
return this._strokeWidth
},
set: function set(strokeWidth) {
this._strokeWidth = strokeWidth
this._genCanvas()
}
}, {
key: 'strokeColor',
get: function get() {
return this._strokeColor
},
set: function set(strokeColor) {
this._strokeColor = strokeColor
this._genCanvas()
}
}, {
key: '_genCanvas',
value: function _genCanvas() {
var _this2 = this
var canvas = this._canvas
var ctx = canvas.getContext('2d')
var border = Array.isArray(this.borderWidth) ? this.borderWidth : [this.borderWidth, this.borderWidth] // x,y border
var relBorder = border.map(function(b) {
return b * _this2.fontSize * 0.1
}) // border in canvas units
var borderRadius = Array.isArray(this.borderRadius) ? this.borderRadius : [this.borderRadius, this.borderRadius, this.borderRadius, this.borderRadius] // tl tr br bl corners
var relBorderRadius = borderRadius.map(function(b) {
return b * _this2.fontSize * 0.1
}) // border radius in canvas units
var padding = Array.isArray(this.padding) ? this.padding : [this.padding, this.padding] // x,y padding
var relPadding = padding.map(function(p) {
return p * _this2.fontSize * 0.1
}) // padding in canvas units
var lines = this.text.split('\n')
var font = ''.concat(this.fontWeight, ' ').concat(this.fontSize, 'px ').concat(this.fontFace)
ctx.font = font // measure canvas with appropriate font
var innerWidth = Math.max.apply(Math, _toConsumableArray(lines.map(function(line) {
return ctx.measureText(line).width
})))
var innerHeight = this.fontSize * lines.length
canvas.width = innerWidth + relBorder[0] * 2 + relPadding[0] * 2
canvas.height = innerHeight + relBorder[1] * 2 + relPadding[1] * 2 // paint border
if (this.borderWidth) {
ctx.strokeStyle = this.borderColor
if (relBorder[0]) {
// left + right borders
var hb = relBorder[0] / 2
ctx.lineWidth = relBorder[0]
ctx.beginPath()
ctx.moveTo(hb, relBorderRadius[0])
ctx.lineTo(hb, canvas.height - relBorderRadius[3])
ctx.moveTo(canvas.width - hb, relBorderRadius[1])
ctx.lineTo(canvas.width - hb, canvas.height - relBorderRadius[2])
ctx.stroke()
}
if (relBorder[1]) {
// top + bottom borders
var _hb = relBorder[1] / 2
ctx.lineWidth = relBorder[1]
ctx.beginPath()
ctx.moveTo(Math.max(relBorder[0], relBorderRadius[0]), _hb)
ctx.lineTo(canvas.width - Math.max(relBorder[0], relBorderRadius[1]), _hb)
ctx.moveTo(Math.max(relBorder[0], relBorderRadius[3]), canvas.height - _hb)
ctx.lineTo(canvas.width - Math.max(relBorder[0], relBorderRadius[2]), canvas.height - _hb)
ctx.stroke()
}
if (this.borderRadius) {
// strike rounded corners
var cornerWidth = Math.max.apply(Math, _toConsumableArray(relBorder))
var _hb2 = cornerWidth / 2
ctx.lineWidth = cornerWidth
ctx.beginPath();
[!!relBorderRadius[0] && [relBorderRadius[0], _hb2, _hb2, relBorderRadius[0]], !!relBorderRadius[1] && [canvas.width - relBorderRadius[1], canvas.width - _hb2, _hb2, relBorderRadius[1]], !!relBorderRadius[2] && [canvas.width - relBorderRadius[2], canvas.width - _hb2, canvas.height - _hb2, canvas.height - relBorderRadius[2]], !!relBorderRadius[3] && [relBorderRadius[3], _hb2, canvas.height - _hb2, canvas.height - relBorderRadius[3]]].filter(function(d) {
return d
}).forEach(function(_ref) {
var _ref2 = _slicedToArray(_ref, 4)
var x0 = _ref2[0]
var x1 = _ref2[1]
var y0 = _ref2[2]
var y1 = _ref2[3]
ctx.moveTo(x0, y0)
ctx.quadraticCurveTo(x1, y0, x1, y1)
})
ctx.stroke()
}
} // paint background
if (this.backgroundColor) {
ctx.fillStyle = this.backgroundColor
if (!this.borderRadius) {
ctx.fillRect(relBorder[0], relBorder[1], canvas.width - relBorder[0] * 2, canvas.height - relBorder[1] * 2)
} else {
// fill with rounded corners
ctx.beginPath()
ctx.moveTo(relBorder[0], relBorderRadius[0]);
[[relBorder[0], relBorderRadius[0], canvas.width - relBorderRadius[1], relBorder[1], relBorder[1], relBorder[1]], // t
[canvas.width - relBorder[0], canvas.width - relBorder[0], canvas.width - relBorder[0], relBorder[1], relBorderRadius[1], canvas.height - relBorderRadius[2]], // r
[canvas.width - relBorder[0], canvas.width - relBorderRadius[2], relBorderRadius[3], canvas.height - relBorder[1], canvas.height - relBorder[1], canvas.height - relBorder[1]], // b
[relBorder[0], relBorder[0], relBorder[0], canvas.height - relBorder[1], canvas.height - relBorderRadius[3], relBorderRadius[0]] // t
].forEach(function(_ref3) {
var _ref4 = _slicedToArray(_ref3, 6)
var x0 = _ref4[0]
var x1 = _ref4[1]
var x2 = _ref4[2]
var y0 = _ref4[3]
var y1 = _ref4[4]
var y2 = _ref4[5]
ctx.quadraticCurveTo(x0, y0, x1, y1)
ctx.lineTo(x2, y2)
})
ctx.closePath()
ctx.fill()
}
}
ctx.translate.apply(ctx, _toConsumableArray(relBorder))
ctx.translate.apply(ctx, _toConsumableArray(relPadding)) // paint text
ctx.font = font // Set font again after canvas is resized, as context properties are reset
ctx.fillStyle = this.color
ctx.textBaseline = 'bottom'
var drawTextStroke = this.strokeWidth > 0
if (drawTextStroke) {
ctx.lineWidth = this.strokeWidth * this.fontSize / 10
ctx.strokeStyle = this.strokeColor
}
lines.forEach(function(line, index) {
var lineX = (innerWidth - ctx.measureText(line).width) / 2
var lineY = (index + 1) * _this2.fontSize
drawTextStroke && ctx.strokeText(line, lineX, lineY)
ctx.fillText(line, lineX, lineY)
}) // Inject canvas into sprite
if (this.material.map) this.material.map.dispose() // gc previous texture
var texture = this.material.map = new three.Texture(canvas)
texture.minFilter = three.LinearFilter
texture.needsUpdate = true
var yScale = this.textHeight * lines.length + border[1] * 2 + padding[1] * 2
this.scale.set(yScale * canvas.width / canvas.height, yScale, 0.001)
}
}, {
key: 'clone',
value: function clone() {
return new this.constructor(this.text, this.textHeight, this.color).copy(this)
}
}, {
key: 'copy',
value: function copy(source) {
three.Sprite.prototype.copy.call(this, source)
this.color = source.color
this.backgroundColor = source.backgroundColor
this.padding = source.padding
this.borderWidth = source.borderWidth
this.borderColor = source.borderColor
this.fontFace = source.fontFace
this.fontSize = source.fontSize
this.fontWeight = source.fontWeight
this.strokeWidth = source.strokeWidth
this.strokeColor = source.strokeColor
return this
}
}])
return _default
}(three.Sprite))
export { _default as default }

View File

@@ -0,0 +1,74 @@
import { WebsocketMessageReceive } from './WebSocketMessage.js'
function connect(editor) {
const host = 'ws://' + editor.ip + '//WebModule//websocket'
let timeout = null
const websocketMessageReceive = new WebsocketMessageReceive(editor)
// 尝试连接至服务器
try {
if ('WebSocket' in window) {
editor.socket = new WebSocket(host)
} else {
editor.showMessage('浏览器不支持WebSocket', 'error', 2000)
}
} catch (exception) {
editor.showMessage('与服务器断开连接', 'error', 2000)
return
}
// 连接成功
editor.socket.onopen = function() {
// 关闭timer
try {
clearInterval(timeout)
} catch (exception) {
console.log(exception)
}
const name = editor.opname
editor.socket.send('LIN,' + name)
editor.showMessage('加载完成,连接服务器成功!', 'success', 2000)
editor.isOk = true
websocketMessageReceive.getOriginTagValue()
// let a = 0
// setInterval(() => {
// a += 0.5
// const message = `DT|JointMoveTo|0f26fc6e-1caf-4f57-beb3-d8a65872fbdd|{"J1":"${a}","J2":"0","J3":"0","J4":"0","J5":"0","J6":"0","J7":"0","J8":"0","J9":"0","J10":"0","J11":"0","J12":"0"}`
// websocketMessageReceive.websocketMessage({ data: message })
// }, 10)
}
// 收到消息
editor.socket.onmessage = function(msg) {
try {
websocketMessageReceive.websocketMessage(msg)
} catch (exception) {
const error = exception.toString()
console.log(error)
}
}
// 连接断开
editor.socket.onclose = function(event) {
try {
editor.showMessage('onclose' + '服务器连接失败', 'error', 2000)
editor.socket.close()
if (editor.IsAllowConnect) {
// 显示一下
editor.showMessage('重新连接服务器', 'warning', 2000)
console.log('onclose' + '重新连接服务器')
// 重新连接,打开计时器
timeout = setTimeout(function() {
connect(editor)
}, 5000)
}
} catch (exception) {
const error = exception.toString()
editor.showMessage.error('服务器出现错误,关闭连接' + error, 'error', 2000)
}
}
// 出现错误
editor.socket.onerror = function(event) {
console.log('onerror_' + '服务器连接失败')
editor.showMessage.error('服务器连接失败', 'error', 2000)
}
}
export { connect }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,27 @@
// Split strategy constants
export const CENTER = 0;
export const AVERAGE = 1;
export const SAH = 2;
// Traversal constants
export const NOT_INTERSECTED = 0;
export const INTERSECTED = 1;
export const CONTAINED = 2;
// SAH cost constants
// TODO: hone these costs more. The relative difference between them should be the
// difference in measured time to perform a triangle intersection vs traversing
// bounds.
export const TRIANGLE_INTERSECT_COST = 1.25;
export const TRAVERSAL_COST = 1;
// Build constants
export const BYTES_PER_NODE = 6 * 4 + 4 + 4;
export const IS_LEAFNODE_FLAG = 0xFFFF;
// EPSILON for computing floating point error during build
// https://en.wikipedia.org/wiki/Machine_epsilon#Values_for_standard_hardware_floating_point_arithmetics
export const FLOAT32_EPSILON = Math.pow( 2, - 24 );
export const SKIP_GENERATION = Symbol( 'SKIP_GENERATION' );

View File

@@ -0,0 +1,548 @@
import { BufferAttribute, Box3, FrontSide } from 'three'
import { CENTER, BYTES_PER_NODE, IS_LEAFNODE_FLAG, SKIP_GENERATION } from './Constants.js';
import { buildPackedTree } from './build/buildTree.js';
import { OrientedBox } from '../math/OrientedBox.js';
import { arrayToBox } from '../utils/ArrayBoxUtilities.js';
import { ExtendedTrianglePool } from '../utils/ExtendedTrianglePool.js';
import { shapecast } from './cast/shapecast.js';
import { closestPointToPoint } from './cast/closestPointToPoint.js';
import { iterateOverTriangles } from './utils/iterationUtils.generated.js';
import { refit } from './cast/refit.generated.js';
import { raycast } from './cast/raycast.generated.js';
import { raycastFirst } from './cast/raycastFirst.generated.js';
import { intersectsGeometry } from './cast/intersectsGeometry.generated.js';
import { closestPointToGeometry } from './cast/closestPointToGeometry.generated.js';
import { iterateOverTriangles_indirect } from './utils/iterationUtils_indirect.generated.js';
import { refit_indirect } from './cast/refit_indirect.generated.js';
import { raycast_indirect } from './cast/raycast_indirect.generated.js';
import { raycastFirst_indirect } from './cast/raycastFirst_indirect.generated.js';
import { intersectsGeometry_indirect } from './cast/intersectsGeometry_indirect.generated.js';
import { closestPointToGeometry_indirect } from './cast/closestPointToGeometry_indirect.generated.js';
import { isSharedArrayBufferSupported } from '../utils/BufferUtils.js';
import { setTriangle } from '../utils/TriangleUtilities.js';
import { bvhcast } from './cast/bvhcast.js';
const obb = /* @__PURE__ */ new OrientedBox();
const tempBox = /* @__PURE__ */ new Box3();
export const DEFAULT_OPTIONS = {
strategy: CENTER,
maxDepth: 40,
maxLeafTris: 10,
useSharedArrayBuffer: false,
setBoundingBox: true,
onProgress: null,
indirect: false,
verbose: true,
};
export class MeshBVH {
static serialize( bvh, options = {} ) {
options = {
cloneBuffers: true,
...options,
};
const geometry = bvh.geometry;
const rootData = bvh._roots;
const indirectBuffer = bvh._indirectBuffer;
const indexAttribute = geometry.getIndex();
let result;
if ( options.cloneBuffers ) {
result = {
roots: rootData.map( root => root.slice() ),
index: indexAttribute ? indexAttribute.array.slice() : null,
indirectBuffer: indirectBuffer ? indirectBuffer.slice() : null,
};
} else {
result = {
roots: rootData,
index: indexAttribute ? indexAttribute.array : null,
indirectBuffer: indirectBuffer,
};
}
return result;
}
static deserialize( data, geometry, options = {} ) {
options = {
setIndex: true,
indirect: Boolean( data.indirectBuffer ),
...options,
};
const { index, roots, indirectBuffer } = data;
const bvh = new MeshBVH( geometry, { ...options, [ SKIP_GENERATION ]: true } );
bvh._roots = roots;
bvh._indirectBuffer = indirectBuffer || null;
if ( options.setIndex ) {
const indexAttribute = geometry.getIndex();
if ( indexAttribute === null ) {
const newIndex = new BufferAttribute( data.index, 1, false );
geometry.setIndex( newIndex );
} else if ( indexAttribute.array !== index ) {
indexAttribute.array.set( index );
indexAttribute.needsUpdate = true;
}
}
return bvh;
}
get indirect() {
return ! ! this._indirectBuffer;
}
constructor( geometry, options = {} ) {
if ( ! geometry.isBufferGeometry ) {
throw new Error( 'MeshBVH: Only BufferGeometries are supported.' );
} else if ( geometry.index && geometry.index.isInterleavedBufferAttribute ) {
throw new Error( 'MeshBVH: InterleavedBufferAttribute is not supported for the index attribute.' );
}
// default options
options = Object.assign( {
...DEFAULT_OPTIONS,
// undocumented options
// Whether to skip generating the tree. Used for deserialization.
[ SKIP_GENERATION ]: false,
}, options );
if ( options.useSharedArrayBuffer && ! isSharedArrayBufferSupported() ) {
throw new Error( 'MeshBVH: SharedArrayBuffer is not available.' );
}
// retain references to the geometry so we can use them it without having to
// take a geometry reference in every function.
this.geometry = geometry;
this._roots = null;
this._indirectBuffer = null;
if ( ! options[ SKIP_GENERATION ] ) {
buildPackedTree( this, options );
if ( ! geometry.boundingBox && options.setBoundingBox ) {
geometry.boundingBox = this.getBoundingBox( new Box3() );
}
}
const { _indirectBuffer } = this;
this.resolveTriangleIndex = options.indirect ? i => _indirectBuffer[ i ] : i => i;
}
refit( nodeIndices = null ) {
const refitFunc = this.indirect ? refit_indirect : refit;
return refitFunc( this, nodeIndices );
}
traverse( callback, rootIndex = 0 ) {
const buffer = this._roots[ rootIndex ];
const uint32Array = new Uint32Array( buffer );
const uint16Array = new Uint16Array( buffer );
_traverse( 0 );
function _traverse( node32Index, depth = 0 ) {
const node16Index = node32Index * 2;
const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), offset, count );
} else {
// TODO: use node functions here
const left = node32Index + BYTES_PER_NODE / 4;
const right = uint32Array[ node32Index + 6 ];
const splitAxis = uint32Array[ node32Index + 7 ];
const stopTraversal = callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), splitAxis );
if ( ! stopTraversal ) {
_traverse( left, depth + 1 );
_traverse( right, depth + 1 );
}
}
}
}
/* Core Cast Functions */
raycast( ray, materialOrSide = FrontSide ) {
const roots = this._roots;
const geometry = this.geometry;
const intersects = [];
const isMaterial = materialOrSide.isMaterial;
const isArrayMaterial = Array.isArray( materialOrSide );
const groups = geometry.groups;
const side = isMaterial ? materialOrSide.side : materialOrSide;
const raycastFunc = this.indirect ? raycast_indirect : raycast;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side;
const startCount = intersects.length;
raycastFunc( this, i, materialSide, ray, intersects );
if ( isArrayMaterial ) {
const materialIndex = groups[ i ].materialIndex;
for ( let j = startCount, jl = intersects.length; j < jl; j ++ ) {
intersects[ j ].face.materialIndex = materialIndex;
}
}
}
return intersects;
}
raycastFirst( ray, materialOrSide = FrontSide ) {
const roots = this._roots;
const geometry = this.geometry;
const isMaterial = materialOrSide.isMaterial;
const isArrayMaterial = Array.isArray( materialOrSide );
let closestResult = null;
const groups = geometry.groups;
const side = isMaterial ? materialOrSide.side : materialOrSide;
const raycastFirstFunc = this.indirect ? raycastFirst_indirect : raycastFirst;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side;
const result = raycastFirstFunc( this, i, materialSide, ray );
if ( result != null && ( closestResult == null || result.distance < closestResult.distance ) ) {
closestResult = result;
if ( isArrayMaterial ) {
result.face.materialIndex = groups[ i ].materialIndex;
}
}
}
return closestResult;
}
intersectsGeometry( otherGeometry, geomToMesh ) {
let result = false;
const roots = this._roots;
const intersectsGeometryFunc = this.indirect ? intersectsGeometry_indirect : intersectsGeometry;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
result = intersectsGeometryFunc( this, i, otherGeometry, geomToMesh );
if ( result ) {
break;
}
}
return result;
}
shapecast( callbacks ) {
const triangle = ExtendedTrianglePool.getPrimitive();
const iterateFunc = this.indirect ? iterateOverTriangles_indirect : iterateOverTriangles;
let {
boundsTraverseOrder,
intersectsBounds,
intersectsRange,
intersectsTriangle,
} = callbacks;
// wrap the intersectsRange function
if ( intersectsRange && intersectsTriangle ) {
const originalIntersectsRange = intersectsRange;
intersectsRange = ( offset, count, contained, depth, nodeIndex ) => {
if ( ! originalIntersectsRange( offset, count, contained, depth, nodeIndex ) ) {
return iterateFunc( offset, count, this, intersectsTriangle, contained, depth, triangle );
}
return true;
};
} else if ( ! intersectsRange ) {
if ( intersectsTriangle ) {
intersectsRange = ( offset, count, contained, depth ) => {
return iterateFunc( offset, count, this, intersectsTriangle, contained, depth, triangle );
};
} else {
intersectsRange = ( offset, count, contained ) => {
return contained;
};
}
}
// run shapecast
let result = false;
let byteOffset = 0;
const roots = this._roots;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
const root = roots[ i ];
result = shapecast( this, i, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset );
if ( result ) {
break;
}
byteOffset += root.byteLength;
}
ExtendedTrianglePool.releasePrimitive( triangle );
return result;
}
bvhcast( otherBvh, matrixToLocal, callbacks ) {
let {
intersectsRanges,
intersectsTriangles,
} = callbacks;
const triangle1 = ExtendedTrianglePool.getPrimitive();
const indexAttr1 = this.geometry.index;
const positionAttr1 = this.geometry.attributes.position;
const assignTriangle1 = this.indirect ?
i1 => {
const ti = this.resolveTriangleIndex( i1 );
setTriangle( triangle1, ti * 3, indexAttr1, positionAttr1 );
} :
i1 => {
setTriangle( triangle1, i1 * 3, indexAttr1, positionAttr1 );
};
const triangle2 = ExtendedTrianglePool.getPrimitive();
const indexAttr2 = otherBvh.geometry.index;
const positionAttr2 = otherBvh.geometry.attributes.position;
const assignTriangle2 = otherBvh.indirect ?
i2 => {
const ti2 = otherBvh.resolveTriangleIndex( i2 );
setTriangle( triangle2, ti2 * 3, indexAttr2, positionAttr2 );
} :
i2 => {
setTriangle( triangle2, i2 * 3, indexAttr2, positionAttr2 );
};
// generate triangle callback if needed
if ( intersectsTriangles ) {
const iterateOverDoubleTriangles = ( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) => {
for ( let i2 = offset2, l2 = offset2 + count2; i2 < l2; i2 ++ ) {
assignTriangle2( i2 );
triangle2.a.applyMatrix4( matrixToLocal );
triangle2.b.applyMatrix4( matrixToLocal );
triangle2.c.applyMatrix4( matrixToLocal );
triangle2.needsUpdate = true;
for ( let i1 = offset1, l1 = offset1 + count1; i1 < l1; i1 ++ ) {
assignTriangle1( i1 );
triangle1.needsUpdate = true;
if ( intersectsTriangles( triangle1, triangle2, i1, i2, depth1, index1, depth2, index2 ) ) {
return true;
}
}
}
return false;
};
if ( intersectsRanges ) {
const originalIntersectsRanges = intersectsRanges;
intersectsRanges = function ( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) {
if ( ! originalIntersectsRanges( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) ) {
return iterateOverDoubleTriangles( offset1, count1, offset2, count2, depth1, index1, depth2, index2 );
}
return true;
};
} else {
intersectsRanges = iterateOverDoubleTriangles;
}
}
return bvhcast( this, otherBvh, matrixToLocal, intersectsRanges );
}
/* Derived Cast Functions */
intersectsBox( box, boxToMesh ) {
obb.set( box.min, box.max, boxToMesh );
obb.needsUpdate = true;
return this.shapecast(
{
intersectsBounds: box => obb.intersectsBox( box ),
intersectsTriangle: tri => obb.intersectsTriangle( tri )
}
);
}
intersectsSphere( sphere ) {
return this.shapecast(
{
intersectsBounds: box => sphere.intersectsBox( box ),
intersectsTriangle: tri => tri.intersectsSphere( sphere )
}
);
}
closestPointToGeometry( otherGeometry, geometryToBvh, target1 = { }, target2 = { }, minThreshold = 0, maxThreshold = Infinity ) {
const closestPointToGeometryFunc = this.indirect ? closestPointToGeometry_indirect : closestPointToGeometry;
return closestPointToGeometryFunc(
this,
otherGeometry,
geometryToBvh,
target1,
target2,
minThreshold,
maxThreshold,
);
}
closestPointToPoint( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) {
return closestPointToPoint(
this,
point,
target,
minThreshold,
maxThreshold,
);
}
getBoundingBox( target ) {
target.makeEmpty();
const roots = this._roots;
roots.forEach( buffer => {
arrayToBox( 0, new Float32Array( buffer ), tempBox );
target.union( tempBox );
} );
return target;
}
}

View File

@@ -0,0 +1,12 @@
export class MeshBVHNode {
constructor() {
// internal nodes have boundingData, left, right, and splitAxis
// leaf nodes have offset and count (referring to primitives in the mesh geometry)
this.boundingData = new Float32Array( 6 );
}
}

View File

@@ -0,0 +1,314 @@
import { Box3, Matrix4 } from 'three'
import { BufferStack } from '../utils/BufferStack.js';
import { BOUNDING_DATA_INDEX, COUNT, IS_LEAF, LEFT_NODE, OFFSET, RIGHT_NODE } from '../utils/nodeBufferUtils.js';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
import { PrimitivePool } from '../../utils/PrimitivePool.js';
const _bufferStack1 = new BufferStack.constructor();
const _bufferStack2 = new BufferStack.constructor();
const _boxPool = new PrimitivePool( () => new Box3() );
const _leftBox1 = new Box3();
const _rightBox1 = new Box3();
const _leftBox2 = new Box3();
const _rightBox2 = new Box3();
let _active = false;
export function bvhcast( bvh, otherBvh, matrixToLocal, intersectsRanges ) {
if ( _active ) {
throw new Error( 'MeshBVH: Recursive calls to bvhcast not supported.' );
}
_active = true;
const roots = bvh._roots;
const otherRoots = otherBvh._roots;
let result;
let offset1 = 0;
let offset2 = 0;
const invMat = new Matrix4().copy( matrixToLocal ).invert();
// iterate over the first set of roots
for ( let i = 0, il = roots.length; i < il; i ++ ) {
_bufferStack1.setBuffer( roots[ i ] );
offset2 = 0;
// prep the initial root box
const localBox = _boxPool.getPrimitive();
arrayToBox( BOUNDING_DATA_INDEX( 0 ), _bufferStack1.float32Array, localBox );
localBox.applyMatrix4( invMat );
// iterate over the second set of roots
for ( let j = 0, jl = otherRoots.length; j < jl; j ++ ) {
_bufferStack2.setBuffer( otherRoots[ i ] );
result = _traverse(
0, 0, matrixToLocal, invMat, intersectsRanges,
offset1, offset2, 0, 0,
localBox,
);
_bufferStack2.clearBuffer();
offset2 += otherRoots[ j ].length;
if ( result ) {
break;
}
}
// release stack info
_boxPool.releasePrimitive( localBox );
_bufferStack1.clearBuffer();
offset1 += roots[ i ].length;
if ( result ) {
break;
}
}
_active = false;
return result;
}
function _traverse(
node1Index32,
node2Index32,
matrix2to1,
matrix1to2,
intersectsRangesFunc,
// offsets for ids
node1IndexByteOffset = 0,
node2IndexByteOffset = 0,
// tree depth
depth1 = 0,
depth2 = 0,
currBox = null,
reversed = false,
) {
// get the buffer stacks associated with the current indices
let bufferStack1, bufferStack2;
if ( reversed ) {
bufferStack1 = _bufferStack2;
bufferStack2 = _bufferStack1;
} else {
bufferStack1 = _bufferStack1;
bufferStack2 = _bufferStack2;
}
// get the local instances of the typed buffers
const
float32Array1 = bufferStack1.float32Array,
uint32Array1 = bufferStack1.uint32Array,
uint16Array1 = bufferStack1.uint16Array,
float32Array2 = bufferStack2.float32Array,
uint32Array2 = bufferStack2.uint32Array,
uint16Array2 = bufferStack2.uint16Array;
const node1Index16 = node1Index32 * 2;
const node2Index16 = node2Index32 * 2;
const isLeaf1 = IS_LEAF( node1Index16, uint16Array1 );
const isLeaf2 = IS_LEAF( node2Index16, uint16Array2 );
let result = false;
if ( isLeaf2 && isLeaf1 ) {
// if both bounds are leaf nodes then fire the callback if the boxes intersect
if ( reversed ) {
result = intersectsRangesFunc(
OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ),
OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ),
depth2, node2IndexByteOffset + node2Index32,
depth1, node1IndexByteOffset + node1Index32,
);
} else {
result = intersectsRangesFunc(
OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ),
OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ),
depth1, node1IndexByteOffset + node1Index32,
depth2, node2IndexByteOffset + node2Index32,
);
}
} else if ( isLeaf2 ) {
// SWAP
// If we've traversed to the leaf node on the other bvh then we need to swap over
// to traverse down the first one
// get the new box to use
const newBox = _boxPool.getPrimitive();
arrayToBox( BOUNDING_DATA_INDEX( node2Index32 ), float32Array2, newBox );
newBox.applyMatrix4( matrix2to1 );
// get the child bounds to check before traversal
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
node2Index32, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
node2Index32, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
} else {
// if neither are leaves then we should swap if one of the children does not
// intersect with the current bounds
// get the child bounds to check
const cl2 = LEFT_NODE( node2Index32 );
const cr2 = RIGHT_NODE( node2Index32, uint32Array2 );
arrayToBox( BOUNDING_DATA_INDEX( cl2 ), float32Array2, _leftBox2 );
arrayToBox( BOUNDING_DATA_INDEX( cr2 ), float32Array2, _rightBox2 );
const leftIntersects = currBox.intersectsBox( _leftBox2 );
const rightIntersects = currBox.intersectsBox( _rightBox2 );
if ( leftIntersects && rightIntersects ) {
// continue to traverse both children if they both intersect
result = _traverse(
node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1,
currBox, reversed,
) || _traverse(
node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else if ( leftIntersects ) {
if ( isLeaf1 ) {
// if the current box is a leaf then just continue
result = _traverse(
node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else {
// SWAP
// if only one box intersects then we have to swap to the other bvh to continue
const newBox = _boxPool.getPrimitive();
newBox.copy( _leftBox2 ).applyMatrix4( matrix2to1 );
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
cl2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
cl2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
}
} else if ( rightIntersects ) {
if ( isLeaf1 ) {
// if the current box is a leaf then just continue
result = _traverse(
node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexByteOffset, node2IndexByteOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else {
// SWAP
// if only one box intersects then we have to swap to the other bvh to continue
const newBox = _boxPool.getPrimitive();
newBox.copy( _rightBox2 ).applyMatrix4( matrix2to1 );
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
cr2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
cr2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexByteOffset, node1IndexByteOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
}
}
}
return result;
}

View File

@@ -0,0 +1,256 @@
import { Matrix4, Vector3 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { getTriCount } from '../build/geometryUtils.js';
import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js';
/*********************************************************************/
/* This file is generated from "closestPointToGeometry.template.js". */
/*********************************************************************/
const tempMatrix = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
const temp1 = /* @__PURE__ */ new Vector3();
const temp2 = /* @__PURE__ */ new Vector3();
const temp3 = /* @__PURE__ */ new Vector3();
const temp4 = /* @__PURE__ */ new Vector3();
function closestPointToGeometry(
bvh,
otherGeometry,
geometryToBvh,
target1 = { },
target2 = { },
minThreshold = 0,
maxThreshold = Infinity,
) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
obb.needsUpdate = true;
const geometry = bvh.geometry;
const pos = geometry.attributes.position;
const index = geometry.index;
const otherPos = otherGeometry.attributes.position;
const otherIndex = otherGeometry.index;
const triangle = ExtendedTrianglePool.getPrimitive();
const triangle2 = ExtendedTrianglePool.getPrimitive();
let tempTarget1 = temp1;
let tempTargetDest1 = temp2;
let tempTarget2 = null;
let tempTargetDest2 = null;
if ( target2 ) {
tempTarget2 = temp3;
tempTargetDest2 = temp4;
}
let closestDistance = Infinity;
let closestDistanceTriIndex = null;
let closestDistanceOtherTriIndex = null;
tempMatrix.copy( geometryToBvh ).invert();
obb2.matrix.copy( tempMatrix );
bvh.shapecast(
{
boundsTraverseOrder: box => {
return obb.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
if ( score < closestDistance && score < maxThreshold ) {
// if we know the triangles of this bounds will be intersected next then
// save the bounds to use during triangle checks.
if ( isLeaf ) {
obb2.min.copy( box.min );
obb2.max.copy( box.max );
obb2.needsUpdate = true;
}
return true;
}
return false;
},
intersectsRange: ( offset, count ) => {
if ( otherGeometry.boundsTree ) {
// if the other geometry has a bvh then use the accelerated path where we use shapecast to find
// the closest bounds in the other geometry to check.
const otherBvh = otherGeometry.boundsTree;
return otherBvh.shapecast( {
boundsTraverseOrder: box => {
return obb2.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
return score < closestDistance && score < maxThreshold;
},
intersectsRange: ( otherOffset, otherCount ) => {
for ( let i2 = otherOffset, l2 = otherOffset + otherCount; i2 < l2; i2 ++ ) {
setTriangle( triangle2, 3 * i2, otherIndex, otherPos );
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
setTriangle( triangle, 3 * i, index, pos );
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
},
} );
} else {
// If no bounds tree then we'll just check every triangle.
const triCount = getTriCount( otherGeometry );
for ( let i2 = 0, l2 = triCount; i2 < l2; i2 ++ ) {
setTriangle( triangle2, 3 * i2, otherIndex, otherPos );
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
setTriangle( triangle, 3 * i, index, pos );
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
}
},
}
);
ExtendedTrianglePool.releasePrimitive( triangle );
ExtendedTrianglePool.releasePrimitive( triangle2 );
if ( closestDistance === Infinity ) {
return null;
}
if ( ! target1.point ) {
target1.point = tempTargetDest1.clone();
} else {
target1.point.copy( tempTargetDest1 );
}
target1.distance = closestDistance,
target1.faceIndex = closestDistanceTriIndex;
if ( target2 ) {
if ( ! target2.point ) target2.point = tempTargetDest2.clone();
else target2.point.copy( tempTargetDest2 );
target2.point.applyMatrix4( tempMatrix );
tempTargetDest1.applyMatrix4( tempMatrix );
target2.distance = tempTargetDest1.sub( target2.point ).length();
target2.faceIndex = closestDistanceOtherTriIndex;
}
return target1;
}
export { closestPointToGeometry };

View File

@@ -0,0 +1,271 @@
import { Vector3, Matrix4 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { getTriCount } from '../build/geometryUtils.js';
import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js';
const tempMatrix = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
const temp1 = /* @__PURE__ */ new Vector3();
const temp2 = /* @__PURE__ */ new Vector3();
const temp3 = /* @__PURE__ */ new Vector3();
const temp4 = /* @__PURE__ */ new Vector3();
export function closestPointToGeometry/* @echo INDIRECT_STRING */(
bvh,
otherGeometry,
geometryToBvh,
target1 = { },
target2 = { },
minThreshold = 0,
maxThreshold = Infinity,
) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
obb.needsUpdate = true;
const geometry = bvh.geometry;
const pos = geometry.attributes.position;
const index = geometry.index;
const otherPos = otherGeometry.attributes.position;
const otherIndex = otherGeometry.index;
const triangle = ExtendedTrianglePool.getPrimitive();
const triangle2 = ExtendedTrianglePool.getPrimitive();
let tempTarget1 = temp1;
let tempTargetDest1 = temp2;
let tempTarget2 = null;
let tempTargetDest2 = null;
if ( target2 ) {
tempTarget2 = temp3;
tempTargetDest2 = temp4;
}
let closestDistance = Infinity;
let closestDistanceTriIndex = null;
let closestDistanceOtherTriIndex = null;
tempMatrix.copy( geometryToBvh ).invert();
obb2.matrix.copy( tempMatrix );
bvh.shapecast(
{
boundsTraverseOrder: box => {
return obb.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
if ( score < closestDistance && score < maxThreshold ) {
// if we know the triangles of this bounds will be intersected next then
// save the bounds to use during triangle checks.
if ( isLeaf ) {
obb2.min.copy( box.min );
obb2.max.copy( box.max );
obb2.needsUpdate = true;
}
return true;
}
return false;
},
intersectsRange: ( offset, count ) => {
if ( otherGeometry.boundsTree ) {
// if the other geometry has a bvh then use the accelerated path where we use shapecast to find
// the closest bounds in the other geometry to check.
const otherBvh = otherGeometry.boundsTree;
return otherBvh.shapecast( {
boundsTraverseOrder: box => {
return obb2.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
return score < closestDistance && score < maxThreshold;
},
intersectsRange: ( otherOffset, otherCount ) => {
for ( let i2 = otherOffset, l2 = otherOffset + otherCount; i2 < l2; i2 ++ ) {
/* @if INDIRECT */
const ti2 = otherBvh.resolveTriangleIndex( i2 );
setTriangle( triangle2, 3 * ti2, otherIndex, otherPos );
/* @else */
setTriangle( triangle2, 3 * i2, otherIndex, otherPos );
/* @endif */
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
/* @if INDIRECT */
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, index, pos );
/* @else */
setTriangle( triangle, 3 * i, index, pos );
/* @endif */
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
},
} );
} else {
// If no bounds tree then we'll just check every triangle.
const triCount = getTriCount( otherGeometry );
for ( let i2 = 0, l2 = triCount; i2 < l2; i2 ++ ) {
setTriangle( triangle2, 3 * i2, otherIndex, otherPos );
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
/* @if INDIRECT */
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, index, pos );
/* @else */
setTriangle( triangle, 3 * i, index, pos );
/* @endif */
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
}
},
}
);
ExtendedTrianglePool.releasePrimitive( triangle );
ExtendedTrianglePool.releasePrimitive( triangle2 );
if ( closestDistance === Infinity ) {
return null;
}
if ( ! target1.point ) {
target1.point = tempTargetDest1.clone();
} else {
target1.point.copy( tempTargetDest1 );
}
target1.distance = closestDistance,
target1.faceIndex = closestDistanceTriIndex;
if ( target2 ) {
if ( ! target2.point ) target2.point = tempTargetDest2.clone();
else target2.point.copy( tempTargetDest2 );
target2.point.applyMatrix4( tempMatrix );
tempTargetDest1.applyMatrix4( tempMatrix );
target2.distance = tempTargetDest1.sub( target2.point ).length();
target2.faceIndex = closestDistanceOtherTriIndex;
}
return target1;
}

View File

@@ -0,0 +1,256 @@
import { Matrix4, Vector3 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { getTriCount } from '../build/geometryUtils.js';
import { ExtendedTrianglePool } from '../../utils/ExtendedTrianglePool.js';
/*********************************************************************/
/* This file is generated from "closestPointToGeometry.template.js". */
/*********************************************************************/
const tempMatrix = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
const temp1 = /* @__PURE__ */ new Vector3();
const temp2 = /* @__PURE__ */ new Vector3();
const temp3 = /* @__PURE__ */ new Vector3();
const temp4 = /* @__PURE__ */ new Vector3();
function closestPointToGeometry_indirect(
bvh,
otherGeometry,
geometryToBvh,
target1 = { },
target2 = { },
minThreshold = 0,
maxThreshold = Infinity,
) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
obb.needsUpdate = true;
const geometry = bvh.geometry;
const pos = geometry.attributes.position;
const index = geometry.index;
const otherPos = otherGeometry.attributes.position;
const otherIndex = otherGeometry.index;
const triangle = ExtendedTrianglePool.getPrimitive();
const triangle2 = ExtendedTrianglePool.getPrimitive();
let tempTarget1 = temp1;
let tempTargetDest1 = temp2;
let tempTarget2 = null;
let tempTargetDest2 = null;
if ( target2 ) {
tempTarget2 = temp3;
tempTargetDest2 = temp4;
}
let closestDistance = Infinity;
let closestDistanceTriIndex = null;
let closestDistanceOtherTriIndex = null;
tempMatrix.copy( geometryToBvh ).invert();
obb2.matrix.copy( tempMatrix );
bvh.shapecast(
{
boundsTraverseOrder: box => {
return obb.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
if ( score < closestDistance && score < maxThreshold ) {
// if we know the triangles of this bounds will be intersected next then
// save the bounds to use during triangle checks.
if ( isLeaf ) {
obb2.min.copy( box.min );
obb2.max.copy( box.max );
obb2.needsUpdate = true;
}
return true;
}
return false;
},
intersectsRange: ( offset, count ) => {
if ( otherGeometry.boundsTree ) {
// if the other geometry has a bvh then use the accelerated path where we use shapecast to find
// the closest bounds in the other geometry to check.
const otherBvh = otherGeometry.boundsTree;
return otherBvh.shapecast( {
boundsTraverseOrder: box => {
return obb2.distanceToBox( box );
},
intersectsBounds: ( box, isLeaf, score ) => {
return score < closestDistance && score < maxThreshold;
},
intersectsRange: ( otherOffset, otherCount ) => {
for ( let i2 = otherOffset, l2 = otherOffset + otherCount; i2 < l2; i2 ++ ) {
const ti2 = otherBvh.resolveTriangleIndex( i2 );
setTriangle( triangle2, 3 * ti2, otherIndex, otherPos );
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, index, pos );
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
},
} );
} else {
// If no bounds tree then we'll just check every triangle.
const triCount = getTriCount( otherGeometry );
for ( let i2 = 0, l2 = triCount; i2 < l2; i2 ++ ) {
setTriangle( triangle2, 3 * i2, otherIndex, otherPos );
triangle2.a.applyMatrix4( geometryToBvh );
triangle2.b.applyMatrix4( geometryToBvh );
triangle2.c.applyMatrix4( geometryToBvh );
triangle2.needsUpdate = true;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, index, pos );
triangle.needsUpdate = true;
const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 );
if ( dist < closestDistance ) {
tempTargetDest1.copy( tempTarget1 );
if ( tempTargetDest2 ) {
tempTargetDest2.copy( tempTarget2 );
}
closestDistance = dist;
closestDistanceTriIndex = i;
closestDistanceOtherTriIndex = i2;
}
// stop traversal if we find a point that's under the given threshold
if ( dist < minThreshold ) {
return true;
}
}
}
}
},
}
);
ExtendedTrianglePool.releasePrimitive( triangle );
ExtendedTrianglePool.releasePrimitive( triangle2 );
if ( closestDistance === Infinity ) {
return null;
}
if ( ! target1.point ) {
target1.point = tempTargetDest1.clone();
} else {
target1.point.copy( tempTargetDest1 );
}
target1.distance = closestDistance,
target1.faceIndex = closestDistanceTriIndex;
if ( target2 ) {
if ( ! target2.point ) target2.point = tempTargetDest2.clone();
else target2.point.copy( tempTargetDest2 );
target2.point.applyMatrix4( tempMatrix );
tempTargetDest1.applyMatrix4( tempMatrix );
target2.distance = tempTargetDest1.sub( target2.point ).length();
target2.faceIndex = closestDistanceOtherTriIndex;
}
return target1;
}
export { closestPointToGeometry_indirect };

View File

@@ -0,0 +1,78 @@
import { Vector3 } from 'three'
const temp = /* @__PURE__ */ new Vector3();
const temp1 = /* @__PURE__ */ new Vector3();
export function closestPointToPoint(
bvh,
point,
target = { },
minThreshold = 0,
maxThreshold = Infinity,
) {
// early out if under minThreshold
// skip checking if over maxThreshold
// set minThreshold = maxThreshold to quickly check if a point is within a threshold
// returns Infinity if no value found
const minThresholdSq = minThreshold * minThreshold;
const maxThresholdSq = maxThreshold * maxThreshold;
let closestDistanceSq = Infinity;
let closestDistanceTriIndex = null;
bvh.shapecast(
{
boundsTraverseOrder: box => {
temp.copy( point ).clamp( box.min, box.max );
return temp.distanceToSquared( point );
},
intersectsBounds: ( box, isLeaf, score ) => {
return score < closestDistanceSq && score < maxThresholdSq;
},
intersectsTriangle: ( tri, triIndex ) => {
tri.closestPointToPoint( point, temp );
const distSq = point.distanceToSquared( temp );
if ( distSq < closestDistanceSq ) {
temp1.copy( temp );
closestDistanceSq = distSq;
closestDistanceTriIndex = triIndex;
}
if ( distSq < minThresholdSq ) {
return true;
} else {
return false;
}
},
}
);
if ( closestDistanceSq === Infinity ) return null;
const closestDistance = Math.sqrt( closestDistanceSq );
if ( ! target.point ) target.point = temp1.clone();
else target.point.copy( temp1 );
target.distance = closestDistance,
target.faceIndex = closestDistanceTriIndex;
return target;
}

View File

@@ -0,0 +1,169 @@
import { Box3, Matrix4 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { ExtendedTriangle } from '../../math/ExtendedTriangle.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
import { IS_LEAF, OFFSET, COUNT, BOUNDING_DATA_INDEX } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
/*****************************************************************/
/* This file is generated from "intersectsGeometry.template.js". */
/*****************************************************************/
/* eslint-disable indent */
const boundingBox = /* @__PURE__ */ new Box3();
const triangle = /* @__PURE__ */ new ExtendedTriangle();
const triangle2 = /* @__PURE__ */ new ExtendedTriangle();
const invertedMat = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
function intersectsGeometry( bvh, root, otherGeometry, geometryToBvh ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _intersectsGeometry( 0, bvh, otherGeometry, geometryToBvh );
BufferStack.clearBuffer();
return result;
}
function _intersectsGeometry( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
if ( cachedObb === null ) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
cachedObb = obb;
}
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const thisGeometry = bvh.geometry;
const thisIndex = thisGeometry.index;
const thisPos = thisGeometry.attributes.position;
const index = otherGeometry.index;
const pos = otherGeometry.attributes.position;
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
// get the inverse of the geometry matrix so we can transform our triangles into the
// geometry space we're trying to test. We assume there are fewer triangles being checked
// here.
invertedMat.copy( geometryToBvh ).invert();
if ( otherGeometry.boundsTree ) {
// if there's a bounds tree
arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2 );
obb2.matrix.copy( invertedMat );
obb2.needsUpdate = true;
// TODO: use a triangle iteration function here
const res = otherGeometry.boundsTree.shapecast( {
intersectsBounds: box => obb2.intersectsBox( box ),
intersectsTriangle: tri => {
tri.a.applyMatrix4( geometryToBvh );
tri.b.applyMatrix4( geometryToBvh );
tri.c.applyMatrix4( geometryToBvh );
tri.needsUpdate = true;
for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle2, i, thisIndex, thisPos );
triangle2.needsUpdate = true;
if ( tri.intersectsTriangle( triangle2 ) ) {
return true;
}
}
return false;
}
} );
return res;
} else {
// if we're just dealing with raw geometry
for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle, i, thisIndex, thisPos );
triangle.a.applyMatrix4( invertedMat );
triangle.b.applyMatrix4( invertedMat );
triangle.c.applyMatrix4( invertedMat );
triangle.needsUpdate = true;
for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) {
setTriangle( triangle2, i2, index, pos );
triangle2.needsUpdate = true;
if ( triangle.intersectsTriangle( triangle2 ) ) {
return true;
}
}
}
}
} else {
const left = nodeIndex32 + 8;
const right = uint32Array[ nodeIndex32 + 6 ];
arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox );
const leftIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( left, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( leftIntersection ) return true;
arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox );
const rightIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( right, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( rightIntersection ) return true;
return false;
}
}
export { intersectsGeometry };

View File

@@ -0,0 +1,196 @@
/* eslint-disable indent */
import { Box3, Matrix4 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { ExtendedTriangle } from '../../math/ExtendedTriangle.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
import { COUNT, OFFSET, IS_LEAF, BOUNDING_DATA_INDEX } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
const boundingBox = /* @__PURE__ */ new Box3();
const triangle = /* @__PURE__ */ new ExtendedTriangle();
const triangle2 = /* @__PURE__ */ new ExtendedTriangle();
const invertedMat = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
export function intersectsGeometry/* @echo INDIRECT_STRING */( bvh, root, otherGeometry, geometryToBvh ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _intersectsGeometry( 0, bvh, otherGeometry, geometryToBvh );
BufferStack.clearBuffer();
return result;
}
function _intersectsGeometry( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
if ( cachedObb === null ) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
cachedObb = obb;
}
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const thisGeometry = bvh.geometry;
const thisIndex = thisGeometry.index;
const thisPos = thisGeometry.attributes.position;
const index = otherGeometry.index;
const pos = otherGeometry.attributes.position;
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
// get the inverse of the geometry matrix so we can transform our triangles into the
// geometry space we're trying to test. We assume there are fewer triangles being checked
// here.
invertedMat.copy( geometryToBvh ).invert();
if ( otherGeometry.boundsTree ) {
// if there's a bounds tree
arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2 );
obb2.matrix.copy( invertedMat );
obb2.needsUpdate = true;
// TODO: use a triangle iteration function here
const res = otherGeometry.boundsTree.shapecast( {
intersectsBounds: box => obb2.intersectsBox( box ),
intersectsTriangle: tri => {
tri.a.applyMatrix4( geometryToBvh );
tri.b.applyMatrix4( geometryToBvh );
tri.c.applyMatrix4( geometryToBvh );
tri.needsUpdate = true;
/* @if INDIRECT */
for ( let i = offset, l = count + offset; i < l; i ++ ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle2, 3 * bvh.resolveTriangleIndex( i ), thisIndex, thisPos );
triangle2.needsUpdate = true;
if ( tri.intersectsTriangle( triangle2 ) ) {
return true;
}
}
/* @else */
for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle2, i, thisIndex, thisPos );
triangle2.needsUpdate = true;
if ( tri.intersectsTriangle( triangle2 ) ) {
return true;
}
}
/* @endif */
return false;
}
} );
return res;
} else {
// if we're just dealing with raw geometry
/* @if INDIRECT */
for ( let i = offset, l = count + offset; i < l; i ++ ) {
// this triangle needs to be transformed into the current BVH coordinate frame
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, thisIndex, thisPos );
/* @else */
for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle, i, thisIndex, thisPos );
/* @endif */
triangle.a.applyMatrix4( invertedMat );
triangle.b.applyMatrix4( invertedMat );
triangle.c.applyMatrix4( invertedMat );
triangle.needsUpdate = true;
for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) {
setTriangle( triangle2, i2, index, pos );
triangle2.needsUpdate = true;
if ( triangle.intersectsTriangle( triangle2 ) ) {
return true;
}
}
/* @if INDIRECT */
}
/* @else */
}
/* @endif */
}
} else {
const left = nodeIndex32 + 8;
const right = uint32Array[ nodeIndex32 + 6 ];
arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox );
const leftIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( left, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( leftIntersection ) return true;
arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox );
const rightIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( right, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( rightIntersection ) return true;
return false;
}
}

View File

@@ -0,0 +1,167 @@
import { Box3, Matrix4 } from 'three'
import { OrientedBox } from '../../math/OrientedBox.js';
import { ExtendedTriangle } from '../../math/ExtendedTriangle.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
import { IS_LEAF, OFFSET, COUNT, BOUNDING_DATA_INDEX } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
/*****************************************************************/
/* This file is generated from "intersectsGeometry.template.js". */
/*****************************************************************/
/* eslint-disable indent */
const boundingBox = /* @__PURE__ */ new Box3();
const triangle = /* @__PURE__ */ new ExtendedTriangle();
const triangle2 = /* @__PURE__ */ new ExtendedTriangle();
const invertedMat = /* @__PURE__ */ new Matrix4();
const obb = /* @__PURE__ */ new OrientedBox();
const obb2 = /* @__PURE__ */ new OrientedBox();
function intersectsGeometry_indirect( bvh, root, otherGeometry, geometryToBvh ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _intersectsGeometry( 0, bvh, otherGeometry, geometryToBvh );
BufferStack.clearBuffer();
return result;
}
function _intersectsGeometry( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
if ( cachedObb === null ) {
if ( ! otherGeometry.boundingBox ) {
otherGeometry.computeBoundingBox();
}
obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
cachedObb = obb;
}
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const thisGeometry = bvh.geometry;
const thisIndex = thisGeometry.index;
const thisPos = thisGeometry.attributes.position;
const index = otherGeometry.index;
const pos = otherGeometry.attributes.position;
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
// get the inverse of the geometry matrix so we can transform our triangles into the
// geometry space we're trying to test. We assume there are fewer triangles being checked
// here.
invertedMat.copy( geometryToBvh ).invert();
if ( otherGeometry.boundsTree ) {
// if there's a bounds tree
arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2 );
obb2.matrix.copy( invertedMat );
obb2.needsUpdate = true;
// TODO: use a triangle iteration function here
const res = otherGeometry.boundsTree.shapecast( {
intersectsBounds: box => obb2.intersectsBox( box ),
intersectsTriangle: tri => {
tri.a.applyMatrix4( geometryToBvh );
tri.b.applyMatrix4( geometryToBvh );
tri.c.applyMatrix4( geometryToBvh );
tri.needsUpdate = true;
for ( let i = offset, l = count + offset; i < l; i ++ ) {
// this triangle needs to be transformed into the current BVH coordinate frame
setTriangle( triangle2, 3 * bvh.resolveTriangleIndex( i ), thisIndex, thisPos );
triangle2.needsUpdate = true;
if ( tri.intersectsTriangle( triangle2 ) ) {
return true;
}
}
return false;
}
} );
return res;
} else {
// if we're just dealing with raw geometry
for ( let i = offset, l = count + offset; i < l; i ++ ) {
// this triangle needs to be transformed into the current BVH coordinate frame
const ti = bvh.resolveTriangleIndex( i );
setTriangle( triangle, 3 * ti, thisIndex, thisPos );
triangle.a.applyMatrix4( invertedMat );
triangle.b.applyMatrix4( invertedMat );
triangle.c.applyMatrix4( invertedMat );
triangle.needsUpdate = true;
for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) {
setTriangle( triangle2, i2, index, pos );
triangle2.needsUpdate = true;
if ( triangle.intersectsTriangle( triangle2 ) ) {
return true;
}
}
}
}
} else {
const left = nodeIndex32 + 8;
const right = uint32Array[ nodeIndex32 + 6 ];
arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox );
const leftIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( left, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( leftIntersection ) return true;
arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox );
const rightIntersection =
cachedObb.intersectsBox( boundingBox ) &&
_intersectsGeometry( right, bvh, otherGeometry, geometryToBvh, cachedObb );
if ( rightIntersection ) return true;
return false;
}
}
export { intersectsGeometry_indirect };

View File

@@ -0,0 +1,53 @@
import { intersectRay } from '../utils/intersectUtils.js';
import { IS_LEAF, OFFSET, COUNT, LEFT_NODE, RIGHT_NODE } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import { intersectTris } from '../utils/iterationUtils.generated.js';
import '../utils/iterationUtils_indirect.generated.js';
/******************************************************/
/* This file is generated from "raycast.template.js". */
/******************************************************/
function raycast( bvh, root, side, ray, intersects ) {
BufferStack.setBuffer( bvh._roots[ root ] );
_raycast( 0, bvh, side, ray, intersects );
BufferStack.clearBuffer();
}
function _raycast( nodeIndex32, bvh, side, ray, intersects ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
const nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
intersectTris( bvh, side, ray, offset, count, intersects );
} else {
const leftIndex = LEFT_NODE( nodeIndex32 );
if ( intersectRay( leftIndex, float32Array, ray ) ) {
_raycast( leftIndex, bvh, side, ray, intersects );
}
const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array );
if ( intersectRay( rightIndex, float32Array, ray ) ) {
_raycast( rightIndex, bvh, side, ray, intersects );
}
}
}
export { raycast };

View File

@@ -0,0 +1,53 @@
import { intersectRay } from '../utils/intersectUtils.js';
import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import { intersectTris } from '../utils/iterationUtils.generated.js';
import { intersectTris_indirect } from '../utils/iterationUtils_indirect.generated.js';
export function raycast/* @echo INDIRECT_STRING */( bvh, root, side, ray, intersects ) {
BufferStack.setBuffer( bvh._roots[ root ] );
_raycast( 0, bvh, side, ray, intersects );
BufferStack.clearBuffer();
}
function _raycast( nodeIndex32, bvh, side, ray, intersects ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
const nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
/* @if INDIRECT */
intersectTris_indirect( bvh, side, ray, offset, count, intersects );
/* @else */
intersectTris( bvh, side, ray, offset, count, intersects );
/* @endif */
} else {
const leftIndex = LEFT_NODE( nodeIndex32 );
if ( intersectRay( leftIndex, float32Array, ray ) ) {
_raycast( leftIndex, bvh, side, ray, intersects );
}
const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array );
if ( intersectRay( rightIndex, float32Array, ray ) ) {
_raycast( rightIndex, bvh, side, ray, intersects );
}
}
}

View File

@@ -0,0 +1,102 @@
import { IS_LEAF, OFFSET, COUNT, SPLIT_AXIS, LEFT_NODE, RIGHT_NODE } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import { intersectRay } from '../utils/intersectUtils.js';
import { intersectClosestTri } from '../utils/iterationUtils.generated.js';
import '../utils/iterationUtils_indirect.generated.js';
/***********************************************************/
/* This file is generated from "raycastFirst.template.js". */
/***********************************************************/
const _xyzFields = [ 'x', 'y', 'z' ];
function raycastFirst( bvh, root, side, ray ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _raycastFirst( 0, bvh, side, ray );
BufferStack.clearBuffer();
return result;
}
function _raycastFirst( nodeIndex32, bvh, side, ray ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
return intersectClosestTri( bvh, side, ray, offset, count );
} else {
// consider the position of the split plane with respect to the oncoming ray; whichever direction
// the ray is coming from, look for an intersection among that side of the tree first
const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
const xyzAxis = _xyzFields[ splitAxis ];
const rayDir = ray.direction[ xyzAxis ];
const leftToRight = rayDir >= 0;
// c1 is the child to check first
let c1, c2;
if ( leftToRight ) {
c1 = LEFT_NODE( nodeIndex32 );
c2 = RIGHT_NODE( nodeIndex32, uint32Array );
} else {
c1 = RIGHT_NODE( nodeIndex32, uint32Array );
c2 = LEFT_NODE( nodeIndex32 );
}
const c1Intersection = intersectRay( c1, float32Array, ray );
const c1Result = c1Intersection ? _raycastFirst( c1, bvh, side, ray ) : null;
// if we got an intersection in the first node and it's closer than the second node's bounding
// box, we don't need to consider the second node because it couldn't possibly be a better result
if ( c1Result ) {
// check if the point is within the second bounds
// "point" is in the local frame of the bvh
const point = c1Result.point[ xyzAxis ];
const isOutside = leftToRight ?
point <= float32Array[ c2 + splitAxis ] : // min bounding data
point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data
if ( isOutside ) {
return c1Result;
}
}
// either there was no intersection in the first node, or there could still be a closer
// intersection in the second, so check the second node and then take the better of the two
const c2Intersection = intersectRay( c2, float32Array, ray );
const c2Result = c2Intersection ? _raycastFirst( c2, bvh, side, ray ) : null;
if ( c1Result && c2Result ) {
return c1Result.distance <= c2Result.distance ? c1Result : c2Result;
} else {
return c1Result || c2Result || null;
}
}
}
export { raycastFirst };

View File

@@ -0,0 +1,102 @@
import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF, SPLIT_AXIS } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import { intersectRay } from '../utils/intersectUtils.js';
import { intersectClosestTri } from '../utils/iterationUtils.generated.js';
import { intersectClosestTri_indirect } from '../utils/iterationUtils_indirect.generated.js';
const _xyzFields = [ 'x', 'y', 'z' ];
export function raycastFirst/* @echo INDIRECT_STRING */( bvh, root, side, ray ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _raycastFirst( 0, bvh, side, ray );
BufferStack.clearBuffer();
return result;
}
function _raycastFirst( nodeIndex32, bvh, side, ray ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
/* @if INDIRECT */
return intersectClosestTri_indirect( bvh, side, ray, offset, count );
/* @else */
return intersectClosestTri( bvh, side, ray, offset, count );
/* @endif */
} else {
// consider the position of the split plane with respect to the oncoming ray; whichever direction
// the ray is coming from, look for an intersection among that side of the tree first
const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
const xyzAxis = _xyzFields[ splitAxis ];
const rayDir = ray.direction[ xyzAxis ];
const leftToRight = rayDir >= 0;
// c1 is the child to check first
let c1, c2;
if ( leftToRight ) {
c1 = LEFT_NODE( nodeIndex32 );
c2 = RIGHT_NODE( nodeIndex32, uint32Array );
} else {
c1 = RIGHT_NODE( nodeIndex32, uint32Array );
c2 = LEFT_NODE( nodeIndex32 );
}
const c1Intersection = intersectRay( c1, float32Array, ray );
const c1Result = c1Intersection ? _raycastFirst( c1, bvh, side, ray ) : null;
// if we got an intersection in the first node and it's closer than the second node's bounding
// box, we don't need to consider the second node because it couldn't possibly be a better result
if ( c1Result ) {
// check if the point is within the second bounds
// "point" is in the local frame of the bvh
const point = c1Result.point[ xyzAxis ];
const isOutside = leftToRight ?
point <= float32Array[ c2 + splitAxis ] : // min bounding data
point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data
if ( isOutside ) {
return c1Result;
}
}
// either there was no intersection in the first node, or there could still be a closer
// intersection in the second, so check the second node and then take the better of the two
const c2Intersection = intersectRay( c2, float32Array, ray );
const c2Result = c2Intersection ? _raycastFirst( c2, bvh, side, ray ) : null;
if ( c1Result && c2Result ) {
return c1Result.distance <= c2Result.distance ? c1Result : c2Result;
} else {
return c1Result || c2Result || null;
}
}
}

View File

@@ -0,0 +1,101 @@
import { IS_LEAF, OFFSET, COUNT, SPLIT_AXIS, LEFT_NODE, RIGHT_NODE } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import { intersectRay } from '../utils/intersectUtils.js';
import '../utils/iterationUtils.generated.js';
import { intersectClosestTri_indirect } from '../utils/iterationUtils_indirect.generated.js';
/***********************************************************/
/* This file is generated from "raycastFirst.template.js". */
/***********************************************************/
const _xyzFields = [ 'x', 'y', 'z' ];
function raycastFirst_indirect( bvh, root, side, ray ) {
BufferStack.setBuffer( bvh._roots[ root ] );
const result = _raycastFirst( 0, bvh, side, ray );
BufferStack.clearBuffer();
return result;
}
function _raycastFirst( nodeIndex32, bvh, side, ray ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
return intersectClosestTri_indirect( bvh, side, ray, offset, count );
} else {
// consider the position of the split plane with respect to the oncoming ray; whichever direction
// the ray is coming from, look for an intersection among that side of the tree first
const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
const xyzAxis = _xyzFields[ splitAxis ];
const rayDir = ray.direction[ xyzAxis ];
const leftToRight = rayDir >= 0;
// c1 is the child to check first
let c1, c2;
if ( leftToRight ) {
c1 = LEFT_NODE( nodeIndex32 );
c2 = RIGHT_NODE( nodeIndex32, uint32Array );
} else {
c1 = RIGHT_NODE( nodeIndex32, uint32Array );
c2 = LEFT_NODE( nodeIndex32 );
}
const c1Intersection = intersectRay( c1, float32Array, ray );
const c1Result = c1Intersection ? _raycastFirst( c1, bvh, side, ray ) : null;
// if we got an intersection in the first node and it's closer than the second node's bounding
// box, we don't need to consider the second node because it couldn't possibly be a better result
if ( c1Result ) {
// check if the point is within the second bounds
// "point" is in the local frame of the bvh
const point = c1Result.point[ xyzAxis ];
const isOutside = leftToRight ?
point <= float32Array[ c2 + splitAxis ] : // min bounding data
point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data
if ( isOutside ) {
return c1Result;
}
}
// either there was no intersection in the first node, or there could still be a closer
// intersection in the second, so check the second node and then take the better of the two
const c2Intersection = intersectRay( c2, float32Array, ray );
const c2Result = c2Intersection ? _raycastFirst( c2, bvh, side, ray ) : null;
if ( c1Result && c2Result ) {
return c1Result.distance <= c2Result.distance ? c1Result : c2Result;
} else {
return c1Result || c2Result || null;
}
}
}
export { raycastFirst_indirect };

View File

@@ -0,0 +1,52 @@
import { intersectRay } from '../utils/intersectUtils.js';
import { IS_LEAF, OFFSET, COUNT, LEFT_NODE, RIGHT_NODE } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
import '../utils/iterationUtils.generated.js';
import { intersectTris_indirect } from '../utils/iterationUtils_indirect.generated.js';
/******************************************************/
/* This file is generated from "raycast.template.js". */
/******************************************************/
function raycast_indirect( bvh, root, side, ray, intersects ) {
BufferStack.setBuffer( bvh._roots[ root ] );
_raycast( 0, bvh, side, ray, intersects );
BufferStack.clearBuffer();
}
function _raycast( nodeIndex32, bvh, side, ray, intersects ) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
const nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
intersectTris_indirect( bvh, side, ray, offset, count, intersects );
} else {
const leftIndex = LEFT_NODE( nodeIndex32 );
if ( intersectRay( leftIndex, float32Array, ray ) ) {
_raycast( leftIndex, bvh, side, ray, intersects );
}
const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array );
if ( intersectRay( rightIndex, float32Array, ray ) ) {
_raycast( rightIndex, bvh, side, ray, intersects );
}
}
}
export { raycast_indirect };

View File

@@ -0,0 +1,172 @@
import { IS_LEAFNODE_FLAG } from '../Constants.js';
/****************************************************/
/* This file is generated from "refit.template.js". */
/****************************************************/
function refit( bvh, nodeIndices = null ) {
if ( nodeIndices && Array.isArray( nodeIndices ) ) {
nodeIndices = new Set( nodeIndices );
}
const geometry = bvh.geometry;
const indexArr = geometry.index ? geometry.index.array : null;
const posAttr = geometry.attributes.position;
let buffer, uint32Array, uint16Array, float32Array;
let byteOffset = 0;
const roots = bvh._roots;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
buffer = roots[ i ];
uint32Array = new Uint32Array( buffer );
uint16Array = new Uint16Array( buffer );
float32Array = new Float32Array( buffer );
_traverse( 0, byteOffset );
byteOffset += buffer.byteLength;
}
function _traverse( node32Index, byteOffset, force = false ) {
const node16Index = node32Index * 2;
const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
let minx = Infinity;
let miny = Infinity;
let minz = Infinity;
let maxx = - Infinity;
let maxy = - Infinity;
let maxz = - Infinity;
for ( let i = 3 * offset, l = 3 * ( offset + count ); i < l; i ++ ) {
let index = indexArr[ i ];
const x = posAttr.getX( index );
const y = posAttr.getY( index );
const z = posAttr.getZ( index );
if ( x < minx ) minx = x;
if ( x > maxx ) maxx = x;
if ( y < miny ) miny = y;
if ( y > maxy ) maxy = y;
if ( z < minz ) minz = z;
if ( z > maxz ) maxz = z;
}
if (
float32Array[ node32Index + 0 ] !== minx ||
float32Array[ node32Index + 1 ] !== miny ||
float32Array[ node32Index + 2 ] !== minz ||
float32Array[ node32Index + 3 ] !== maxx ||
float32Array[ node32Index + 4 ] !== maxy ||
float32Array[ node32Index + 5 ] !== maxz
) {
float32Array[ node32Index + 0 ] = minx;
float32Array[ node32Index + 1 ] = miny;
float32Array[ node32Index + 2 ] = minz;
float32Array[ node32Index + 3 ] = maxx;
float32Array[ node32Index + 4 ] = maxy;
float32Array[ node32Index + 5 ] = maxz;
return true;
} else {
return false;
}
} else {
const left = node32Index + 8;
const right = uint32Array[ node32Index + 6 ];
// the identifying node indices provided by the shapecast function include offsets of all
// root buffers to guarantee they're unique between roots so offset left and right indices here.
const offsetLeft = left + byteOffset;
const offsetRight = right + byteOffset;
let forceChildren = force;
let includesLeft = false;
let includesRight = false;
if ( nodeIndices ) {
// if we see that neither the left or right child are included in the set that need to be updated
// then we assume that all children need to be updated.
if ( ! forceChildren ) {
includesLeft = nodeIndices.has( offsetLeft );
includesRight = nodeIndices.has( offsetRight );
forceChildren = ! includesLeft && ! includesRight;
}
} else {
includesLeft = true;
includesRight = true;
}
const traverseLeft = forceChildren || includesLeft;
const traverseRight = forceChildren || includesRight;
let leftChange = false;
if ( traverseLeft ) {
leftChange = _traverse( left, byteOffset, forceChildren );
}
let rightChange = false;
if ( traverseRight ) {
rightChange = _traverse( right, byteOffset, forceChildren );
}
const didChange = leftChange || rightChange;
if ( didChange ) {
for ( let i = 0; i < 3; i ++ ) {
const lefti = left + i;
const righti = right + i;
const minLeftValue = float32Array[ lefti ];
const maxLeftValue = float32Array[ lefti + 3 ];
const minRightValue = float32Array[ righti ];
const maxRightValue = float32Array[ righti + 3 ];
float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue;
float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue;
}
}
return didChange;
}
}
}
export { refit };

View File

@@ -0,0 +1,196 @@
import { IS_LEAFNODE_FLAG } from '../Constants.js';
export function refit/* @echo INDIRECT_STRING */( bvh, nodeIndices = null ) {
if ( nodeIndices && Array.isArray( nodeIndices ) ) {
nodeIndices = new Set( nodeIndices );
}
const geometry = bvh.geometry;
const indexArr = geometry.index ? geometry.index.array : null;
const posAttr = geometry.attributes.position;
let buffer, uint32Array, uint16Array, float32Array;
let byteOffset = 0;
const roots = bvh._roots;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
buffer = roots[ i ];
uint32Array = new Uint32Array( buffer );
uint16Array = new Uint16Array( buffer );
float32Array = new Float32Array( buffer );
_traverse( 0, byteOffset );
byteOffset += buffer.byteLength;
}
function _traverse( node32Index, byteOffset, force = false ) {
const node16Index = node32Index * 2;
const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
let minx = Infinity;
let miny = Infinity;
let minz = Infinity;
let maxx = - Infinity;
let maxy = - Infinity;
let maxz = - Infinity;
/* @if INDIRECT */
for ( let i = offset, l = offset + count; i < l; i ++ ) {
const t = 3 * bvh.resolveTriangleIndex( i );
for ( let j = 0; j < 3; j ++ ) {
let index = t + j;
index = indexArr ? indexArr[ index ] : index;
const x = posAttr.getX( index );
const y = posAttr.getY( index );
const z = posAttr.getZ( index );
if ( x < minx ) minx = x;
if ( x > maxx ) maxx = x;
if ( y < miny ) miny = y;
if ( y > maxy ) maxy = y;
if ( z < minz ) minz = z;
if ( z > maxz ) maxz = z;
}
}
/* @else */
for ( let i = 3 * offset, l = 3 * ( offset + count ); i < l; i ++ ) {
let index = indexArr[ i ];
const x = posAttr.getX( index );
const y = posAttr.getY( index );
const z = posAttr.getZ( index );
if ( x < minx ) minx = x;
if ( x > maxx ) maxx = x;
if ( y < miny ) miny = y;
if ( y > maxy ) maxy = y;
if ( z < minz ) minz = z;
if ( z > maxz ) maxz = z;
}
/* @endif */
if (
float32Array[ node32Index + 0 ] !== minx ||
float32Array[ node32Index + 1 ] !== miny ||
float32Array[ node32Index + 2 ] !== minz ||
float32Array[ node32Index + 3 ] !== maxx ||
float32Array[ node32Index + 4 ] !== maxy ||
float32Array[ node32Index + 5 ] !== maxz
) {
float32Array[ node32Index + 0 ] = minx;
float32Array[ node32Index + 1 ] = miny;
float32Array[ node32Index + 2 ] = minz;
float32Array[ node32Index + 3 ] = maxx;
float32Array[ node32Index + 4 ] = maxy;
float32Array[ node32Index + 5 ] = maxz;
return true;
} else {
return false;
}
} else {
const left = node32Index + 8;
const right = uint32Array[ node32Index + 6 ];
// the identifying node indices provided by the shapecast function include offsets of all
// root buffers to guarantee they're unique between roots so offset left and right indices here.
const offsetLeft = left + byteOffset;
const offsetRight = right + byteOffset;
let forceChildren = force;
let includesLeft = false;
let includesRight = false;
if ( nodeIndices ) {
// if we see that neither the left or right child are included in the set that need to be updated
// then we assume that all children need to be updated.
if ( ! forceChildren ) {
includesLeft = nodeIndices.has( offsetLeft );
includesRight = nodeIndices.has( offsetRight );
forceChildren = ! includesLeft && ! includesRight;
}
} else {
includesLeft = true;
includesRight = true;
}
const traverseLeft = forceChildren || includesLeft;
const traverseRight = forceChildren || includesRight;
let leftChange = false;
if ( traverseLeft ) {
leftChange = _traverse( left, byteOffset, forceChildren );
}
let rightChange = false;
if ( traverseRight ) {
rightChange = _traverse( right, byteOffset, forceChildren );
}
const didChange = leftChange || rightChange;
if ( didChange ) {
for ( let i = 0; i < 3; i ++ ) {
const lefti = left + i;
const righti = right + i;
const minLeftValue = float32Array[ lefti ];
const maxLeftValue = float32Array[ lefti + 3 ];
const minRightValue = float32Array[ righti ];
const maxRightValue = float32Array[ righti + 3 ];
float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue;
float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue;
}
}
return didChange;
}
}
}

View File

@@ -0,0 +1,179 @@
import { IS_LEAFNODE_FLAG } from '../Constants.js';
/****************************************************/
/* This file is generated from "refit.template.js". */
/****************************************************/
function refit_indirect( bvh, nodeIndices = null ) {
if ( nodeIndices && Array.isArray( nodeIndices ) ) {
nodeIndices = new Set( nodeIndices );
}
const geometry = bvh.geometry;
const indexArr = geometry.index ? geometry.index.array : null;
const posAttr = geometry.attributes.position;
let buffer, uint32Array, uint16Array, float32Array;
let byteOffset = 0;
const roots = bvh._roots;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
buffer = roots[ i ];
uint32Array = new Uint32Array( buffer );
uint16Array = new Uint16Array( buffer );
float32Array = new Float32Array( buffer );
_traverse( 0, byteOffset );
byteOffset += buffer.byteLength;
}
function _traverse( node32Index, byteOffset, force = false ) {
const node16Index = node32Index * 2;
const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
let minx = Infinity;
let miny = Infinity;
let minz = Infinity;
let maxx = - Infinity;
let maxy = - Infinity;
let maxz = - Infinity;
for ( let i = offset, l = offset + count; i < l; i ++ ) {
const t = 3 * bvh.resolveTriangleIndex( i );
for ( let j = 0; j < 3; j ++ ) {
let index = t + j;
index = indexArr ? indexArr[ index ] : index;
const x = posAttr.getX( index );
const y = posAttr.getY( index );
const z = posAttr.getZ( index );
if ( x < minx ) minx = x;
if ( x > maxx ) maxx = x;
if ( y < miny ) miny = y;
if ( y > maxy ) maxy = y;
if ( z < minz ) minz = z;
if ( z > maxz ) maxz = z;
}
}
if (
float32Array[ node32Index + 0 ] !== minx ||
float32Array[ node32Index + 1 ] !== miny ||
float32Array[ node32Index + 2 ] !== minz ||
float32Array[ node32Index + 3 ] !== maxx ||
float32Array[ node32Index + 4 ] !== maxy ||
float32Array[ node32Index + 5 ] !== maxz
) {
float32Array[ node32Index + 0 ] = minx;
float32Array[ node32Index + 1 ] = miny;
float32Array[ node32Index + 2 ] = minz;
float32Array[ node32Index + 3 ] = maxx;
float32Array[ node32Index + 4 ] = maxy;
float32Array[ node32Index + 5 ] = maxz;
return true;
} else {
return false;
}
} else {
const left = node32Index + 8;
const right = uint32Array[ node32Index + 6 ];
// the identifying node indices provided by the shapecast function include offsets of all
// root buffers to guarantee they're unique between roots so offset left and right indices here.
const offsetLeft = left + byteOffset;
const offsetRight = right + byteOffset;
let forceChildren = force;
let includesLeft = false;
let includesRight = false;
if ( nodeIndices ) {
// if we see that neither the left or right child are included in the set that need to be updated
// then we assume that all children need to be updated.
if ( ! forceChildren ) {
includesLeft = nodeIndices.has( offsetLeft );
includesRight = nodeIndices.has( offsetRight );
forceChildren = ! includesLeft && ! includesRight;
}
} else {
includesLeft = true;
includesRight = true;
}
const traverseLeft = forceChildren || includesLeft;
const traverseRight = forceChildren || includesRight;
let leftChange = false;
if ( traverseLeft ) {
leftChange = _traverse( left, byteOffset, forceChildren );
}
let rightChange = false;
if ( traverseRight ) {
rightChange = _traverse( right, byteOffset, forceChildren );
}
const didChange = leftChange || rightChange;
if ( didChange ) {
for ( let i = 0; i < 3; i ++ ) {
const lefti = left + i;
const righti = right + i;
const minLeftValue = float32Array[ lefti ];
const maxLeftValue = float32Array[ lefti + 3 ];
const minRightValue = float32Array[ righti ];
const maxRightValue = float32Array[ righti + 3 ];
float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue;
float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue;
}
}
return didChange;
}
}
}
export { refit_indirect };

View File

@@ -0,0 +1,214 @@
import { Box3 } from 'three'
import { CONTAINED } from '../Constants.js';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
import { PrimitivePool } from '../../utils/PrimitivePool.js';
import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF, BOUNDING_DATA_INDEX } from '../utils/nodeBufferUtils.js';
import { BufferStack } from '../utils/BufferStack.js';
let _box1, _box2;
const boxStack = [];
const boxPool = /* @__PURE__ */ new PrimitivePool( () => new Box3() );
export function shapecast( bvh, root, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset ) {
// setup
_box1 = boxPool.getPrimitive();
_box2 = boxPool.getPrimitive();
boxStack.push( _box1, _box2 );
BufferStack.setBuffer( bvh._roots[ root ] );
const result = shapecastTraverse( 0, bvh.geometry, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset );
// cleanup
BufferStack.clearBuffer();
boxPool.releasePrimitive( _box1 );
boxPool.releasePrimitive( _box2 );
boxStack.pop();
boxStack.pop();
const length = boxStack.length;
if ( length > 0 ) {
_box2 = boxStack[ length - 1 ];
_box1 = boxStack[ length - 2 ];
}
return result;
}
function shapecastTraverse(
nodeIndex32,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc = null,
nodeIndexByteOffset = 0, // offset for unique node identifier
depth = 0
) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, _box1 );
return intersectsRangeFunc( offset, count, false, depth, nodeIndexByteOffset + nodeIndex32, _box1 );
} else {
const left = LEFT_NODE( nodeIndex32 );
const right = RIGHT_NODE( nodeIndex32, uint32Array );
let c1 = left;
let c2 = right;
let score1, score2;
let box1, box2;
if ( nodeScoreFunc ) {
box1 = _box1;
box2 = _box2;
// bounding data is not offset
arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 );
arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 );
score1 = nodeScoreFunc( box1 );
score2 = nodeScoreFunc( box2 );
if ( score2 < score1 ) {
c1 = right;
c2 = left;
const temp = score1;
score1 = score2;
score2 = temp;
box1 = box2;
// box2 is always set before use below
}
}
// Check box 1 intersection
if ( ! box1 ) {
box1 = _box1;
arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 );
}
const isC1Leaf = IS_LEAF( c1 * 2, uint16Array );
const c1Intersection = intersectsBoundsFunc( box1, isC1Leaf, score1, depth + 1, nodeIndexByteOffset + c1 );
let c1StopTraversal;
if ( c1Intersection === CONTAINED ) {
const offset = getLeftOffset( c1 );
const end = getRightEndOffset( c1 );
const count = end - offset;
c1StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c1, box1 );
} else {
c1StopTraversal =
c1Intersection &&
shapecastTraverse(
c1,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc,
nodeIndexByteOffset,
depth + 1
);
}
if ( c1StopTraversal ) return true;
// Check box 2 intersection
// cached box2 will have been overwritten by previous traversal
box2 = _box2;
arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 );
const isC2Leaf = IS_LEAF( c2 * 2, uint16Array );
const c2Intersection = intersectsBoundsFunc( box2, isC2Leaf, score2, depth + 1, nodeIndexByteOffset + c2 );
let c2StopTraversal;
if ( c2Intersection === CONTAINED ) {
const offset = getLeftOffset( c2 );
const end = getRightEndOffset( c2 );
const count = end - offset;
c2StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c2, box2 );
} else {
c2StopTraversal =
c2Intersection &&
shapecastTraverse(
c2,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc,
nodeIndexByteOffset,
depth + 1
);
}
if ( c2StopTraversal ) return true;
return false;
// Define these inside the function so it has access to the local variables needed
// when converting to the buffer equivalents
function getLeftOffset( nodeIndex32 ) {
const { uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
// traverse until we find a leaf
while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) {
nodeIndex32 = LEFT_NODE( nodeIndex32 );
nodeIndex16 = nodeIndex32 * 2;
}
return OFFSET( nodeIndex32, uint32Array );
}
function getRightEndOffset( nodeIndex32 ) {
const { uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
// traverse until we find a leaf
while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) {
// adjust offset to point to the right node
nodeIndex32 = RIGHT_NODE( nodeIndex32, uint32Array );
nodeIndex16 = nodeIndex32 * 2;
}
// return the end offset of the triangle range
return OFFSET( nodeIndex32, uint32Array ) + COUNT( nodeIndex16, uint16Array );
}
}
}

View File

@@ -0,0 +1,45 @@
class _BufferStack {
constructor() {
this.float32Array = null;
this.uint16Array = null;
this.uint32Array = null;
const stack = [];
let prevBuffer = null;
this.setBuffer = buffer => {
if ( prevBuffer ) {
stack.push( prevBuffer );
}
prevBuffer = buffer;
this.float32Array = new Float32Array( buffer );
this.uint16Array = new Uint16Array( buffer );
this.uint32Array = new Uint32Array( buffer );
};
this.clearBuffer = () => {
prevBuffer = null;
this.float32Array = null;
this.uint16Array = null;
this.uint32Array = null;
if ( stack.length !== 0 ) {
this.setBuffer( stack.pop() );
}
};
}
}
export const BufferStack = new _BufferStack();

View File

@@ -0,0 +1,80 @@
/**
* This function performs intersection tests similar to Ray.intersectBox in three.js,
* with the difference that the box values are read from an array to improve performance.
*/
export function intersectRay( nodeIndex32, array, ray ) {
let tmin, tmax, tymin, tymax, tzmin, tzmax;
const invdirx = 1 / ray.direction.x,
invdiry = 1 / ray.direction.y,
invdirz = 1 / ray.direction.z;
const ox = ray.origin.x;
const oy = ray.origin.y;
const oz = ray.origin.z;
let minx = array[ nodeIndex32 ];
let maxx = array[ nodeIndex32 + 3 ];
let miny = array[ nodeIndex32 + 1 ];
let maxy = array[ nodeIndex32 + 3 + 1 ];
let minz = array[ nodeIndex32 + 2 ];
let maxz = array[ nodeIndex32 + 3 + 2 ];
if ( invdirx >= 0 ) {
tmin = ( minx - ox ) * invdirx;
tmax = ( maxx - ox ) * invdirx;
} else {
tmin = ( maxx - ox ) * invdirx;
tmax = ( minx - ox ) * invdirx;
}
if ( invdiry >= 0 ) {
tymin = ( miny - oy ) * invdiry;
tymax = ( maxy - oy ) * invdiry;
} else {
tymin = ( maxy - oy ) * invdiry;
tymax = ( miny - oy ) * invdiry;
}
if ( ( tmin > tymax ) || ( tymin > tmax ) ) return false;
if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;
if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;
if ( invdirz >= 0 ) {
tzmin = ( minz - oz ) * invdirz;
tzmax = ( maxz - oz ) * invdirz;
} else {
tzmin = ( maxz - oz ) * invdirz;
tzmax = ( minz - oz ) * invdirz;
}
if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return false;
// if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin; // Uncomment this line if add the distance check
if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;
//return point closest to the ray (positive side)
if ( tmax < 0 ) return false;
return true;
}

View File

@@ -0,0 +1,81 @@
import { intersectTri } from '../../utils/ThreeRayIntersectUtilities.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
/*************************************************************/
/* This file is generated from "iterationUtils.template.js". */
/*************************************************************/
/* eslint-disable indent */
function intersectTris( bvh, side, ray, offset, count, intersections ) {
const { geometry, _indirectBuffer } = bvh;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
intersectTri( geometry, side, ray, i, intersections );
}
}
function intersectClosestTri( bvh, side, ray, offset, count ) {
const { geometry, _indirectBuffer } = bvh;
let dist = Infinity;
let res = null;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
let intersection;
intersection = intersectTri( geometry, side, ray, i );
if ( intersection && intersection.distance < dist ) {
res = intersection;
dist = intersection.distance;
}
}
return res;
}
function iterateOverTriangles(
offset,
count,
bvh,
intersectsTriangleFunc,
contained,
depth,
triangle
) {
const { geometry } = bvh;
const { index } = geometry;
const pos = geometry.attributes.position;
for ( let i = offset, l = count + offset; i < l; i ++ ) {
let tri;
tri = i;
setTriangle( triangle, tri * 3, index, pos );
triangle.needsUpdate = true;
if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) {
return true;
}
}
return false;
}
export { intersectClosestTri, intersectTris, iterateOverTriangles };

View File

@@ -0,0 +1,94 @@
/* eslint-disable indent */
import { intersectTri } from '../../utils/ThreeRayIntersectUtilities.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
export function intersectTris/* @echo INDIRECT_STRING */( bvh, side, ray, offset, count, intersections ) {
const { geometry, _indirectBuffer } = bvh;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
/* @if INDIRECT */
let vi = _indirectBuffer ? _indirectBuffer[ i ] : i;
intersectTri( geometry, side, ray, vi, intersections );
/* @else */
intersectTri( geometry, side, ray, i, intersections );
/* @endif */
}
}
export function intersectClosestTri/* @echo INDIRECT_STRING */( bvh, side, ray, offset, count ) {
const { geometry, _indirectBuffer } = bvh;
let dist = Infinity;
let res = null;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
let intersection;
/* @if INDIRECT */
intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i );
/* @else */
intersection = intersectTri( geometry, side, ray, i );
/* @endif */
if ( intersection && intersection.distance < dist ) {
res = intersection;
dist = intersection.distance;
}
}
return res;
}
export function iterateOverTriangles/* @echo INDIRECT_STRING */(
offset,
count,
bvh,
intersectsTriangleFunc,
contained,
depth,
triangle
) {
const { geometry } = bvh;
const { index } = geometry;
const pos = geometry.attributes.position;
for ( let i = offset, l = count + offset; i < l; i ++ ) {
let tri;
/* @if INDIRECT */
tri = bvh.resolveTriangleIndex( i );
/* @else */
tri = i;
/* @endif */
setTriangle( triangle, tri * 3, index, pos );
triangle.needsUpdate = true;
if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,79 @@
import { intersectTri } from '../../utils/ThreeRayIntersectUtilities.js';
import { setTriangle } from '../../utils/TriangleUtilities.js';
/*************************************************************/
/* This file is generated from "iterationUtils.template.js". */
/*************************************************************/
/* eslint-disable indent */
function intersectTris_indirect( bvh, side, ray, offset, count, intersections ) {
const { geometry, _indirectBuffer } = bvh;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
let vi = _indirectBuffer ? _indirectBuffer[ i ] : i;
intersectTri( geometry, side, ray, vi, intersections );
}
}
function intersectClosestTri_indirect( bvh, side, ray, offset, count ) {
const { geometry, _indirectBuffer } = bvh;
let dist = Infinity;
let res = null;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
let intersection;
intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i );
if ( intersection && intersection.distance < dist ) {
res = intersection;
dist = intersection.distance;
}
}
return res;
}
function iterateOverTriangles_indirect(
offset,
count,
bvh,
intersectsTriangleFunc,
contained,
depth,
triangle
) {
const { geometry } = bvh;
const { index } = geometry;
const pos = geometry.attributes.position;
for ( let i = offset, l = count + offset; i < l; i ++ ) {
let tri;
tri = bvh.resolveTriangleIndex( i );
setTriangle( triangle, tri * 3, index, pos );
triangle.needsUpdate = true;
if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) {
return true;
}
}
return false;
}
export { intersectClosestTri_indirect, intersectTris_indirect, iterateOverTriangles_indirect };

View File

@@ -0,0 +1,41 @@
export function IS_LEAF( n16, uint16Array ) {
return uint16Array[ n16 + 15 ] === 0xFFFF;
}
export function OFFSET( n32, uint32Array ) {
return uint32Array[ n32 + 6 ];
}
export function COUNT( n16, uint16Array ) {
return uint16Array[ n16 + 14 ];
}
export function LEFT_NODE( n32 ) {
return n32 + 8;
}
export function RIGHT_NODE( n32, uint32Array ) {
return uint32Array[ n32 + 6 ];
}
export function SPLIT_AXIS( n32, uint32Array ) {
return uint32Array[ n32 + 7 ];
}
export function BOUNDING_DATA_INDEX( n32 ) {
return n32;
}

View File

@@ -0,0 +1,292 @@
import { Box3, Vector3 } from 'three'
import { TRAVERSAL_COST, TRIANGLE_INTERSECT_COST } from '../core/Constants.js';
import { arrayToBox } from '../utils/ArrayBoxUtilities.js';
import { isSharedArrayBufferSupported } from '../utils/BufferUtils.js';
const _box1 = /* @__PURE__ */ new Box3();
const _box2 = /* @__PURE__ */ new Box3();
const _vec = /* @__PURE__ */ new Vector3();
// https://stackoverflow.com/questions/1248302/how-to-get-the-size-of-a-javascript-object
function getPrimitiveSize( el ) {
switch ( typeof el ) {
case 'number':
return 8;
case 'string':
return el.length * 2;
case 'boolean':
return 4;
default:
return 0;
}
}
function isTypedArray( arr ) {
const regex = /(Uint|Int|Float)(8|16|32)Array/;
return regex.test( arr.constructor.name );
}
function getRootExtremes( bvh, group ) {
const result = {
nodeCount: 0,
leafNodeCount: 0,
depth: {
min: Infinity, max: - Infinity
},
tris: {
min: Infinity, max: - Infinity
},
splits: [ 0, 0, 0 ],
surfaceAreaScore: 0,
};
bvh.traverse( ( depth, isLeaf, boundingData, offsetOrSplit, count ) => {
const l0 = boundingData[ 0 + 3 ] - boundingData[ 0 ];
const l1 = boundingData[ 1 + 3 ] - boundingData[ 1 ];
const l2 = boundingData[ 2 + 3 ] - boundingData[ 2 ];
const surfaceArea = 2 * ( l0 * l1 + l1 * l2 + l2 * l0 );
result.nodeCount ++;
if ( isLeaf ) {
result.leafNodeCount ++;
result.depth.min = Math.min( depth, result.depth.min );
result.depth.max = Math.max( depth, result.depth.max );
result.tris.min = Math.min( count, result.tris.min );
result.tris.max = Math.max( count, result.tris.max );
result.surfaceAreaScore += surfaceArea * TRIANGLE_INTERSECT_COST * count;
} else {
result.splits[ offsetOrSplit ] ++;
result.surfaceAreaScore += surfaceArea * TRAVERSAL_COST;
}
}, group );
// If there are no leaf nodes because the tree hasn't finished generating yet.
if ( result.tris.min === Infinity ) {
result.tris.min = 0;
result.tris.max = 0;
}
if ( result.depth.min === Infinity ) {
result.depth.min = 0;
result.depth.max = 0;
}
return result;
}
function getBVHExtremes( bvh ) {
return bvh._roots.map( ( root, i ) => getRootExtremes( bvh, i ) );
}
function estimateMemoryInBytes( obj ) {
const traversed = new Set();
const stack = [ obj ];
let bytes = 0;
while ( stack.length ) {
const curr = stack.pop();
if ( traversed.has( curr ) ) {
continue;
}
traversed.add( curr );
for ( let key in curr ) {
if ( ! curr.hasOwnProperty( key ) ) {
continue;
}
bytes += getPrimitiveSize( key );
const value = curr[ key ];
if ( value && ( typeof value === 'object' || typeof value === 'function' ) ) {
if ( isTypedArray( value ) ) {
bytes += value.byteLength;
} else if ( isSharedArrayBufferSupported() && value instanceof SharedArrayBuffer ) {
bytes += value.byteLength;
} else if ( value instanceof ArrayBuffer ) {
bytes += value.byteLength;
} else {
stack.push( value );
}
} else {
bytes += getPrimitiveSize( value );
}
}
}
return bytes;
}
function validateBounds( bvh ) {
const geometry = bvh.geometry;
const depthStack = [];
const index = geometry.index;
const position = geometry.getAttribute( 'position' );
let passes = true;
bvh.traverse( ( depth, isLeaf, boundingData, offset, count ) => {
const info = {
depth,
isLeaf,
boundingData,
offset,
count,
};
depthStack[ depth ] = info;
arrayToBox( 0, boundingData, _box1 );
const parent = depthStack[ depth - 1 ];
if ( isLeaf ) {
// check triangles
for ( let i = offset, l = offset + count; i < l; i ++ ) {
const triIndex = bvh.resolveTriangleIndex( i );
let i0 = 3 * triIndex;
let i1 = 3 * triIndex + 1;
let i2 = 3 * triIndex + 2;
if ( index ) {
i0 = index.getX( i0 );
i1 = index.getX( i1 );
i2 = index.getX( i2 );
}
let isContained;
_vec.fromBufferAttribute( position, i0 );
isContained = _box1.containsPoint( _vec );
_vec.fromBufferAttribute( position, i1 );
isContained = isContained && _box1.containsPoint( _vec );
_vec.fromBufferAttribute( position, i2 );
isContained = isContained && _box1.containsPoint( _vec );
console.assert( isContained, 'Leaf bounds does not fully contain triangle.' );
passes = passes && isContained;
}
}
if ( parent ) {
// check if my bounds fit in my parents
arrayToBox( 0, boundingData, _box2 );
const isContained = _box2.containsBox( _box1 );
console.assert( isContained, 'Parent bounds does not fully contain child.' );
passes = passes && isContained;
}
} );
return passes;
}
// Returns a simple, human readable object that represents the BVH.
function getJSONStructure( bvh ) {
const depthStack = [];
bvh.traverse( ( depth, isLeaf, boundingData, offset, count ) => {
const info = {
bounds: arrayToBox( 0, boundingData, new Box3() ),
};
if ( isLeaf ) {
info.count = count;
info.offset = offset;
} else {
info.left = null;
info.right = null;
}
depthStack[ depth ] = info;
// traversal hits the left then right node
const parent = depthStack[ depth - 1 ];
if ( parent ) {
if ( parent.left === null ) {
parent.left = info;
} else {
parent.right = info;
}
}
} );
return depthStack[ 0 ];
}
export { estimateMemoryInBytes, getBVHExtremes, validateBounds, getJSONStructure };

View File

@@ -0,0 +1,4 @@
export * from './glsl/common_functions.glsl.js';
export * from './glsl/bvh_distance_functions.glsl.js';
export * from './glsl/bvh_ray_functions.glsl.js';
export * from './glsl/bvh_struct_definitions.glsl.js';

View File

@@ -0,0 +1,191 @@
import {
DataTexture,
FloatType,
UnsignedIntType,
RGBAFormat,
RGIntegerFormat,
NearestFilter,
BufferAttribute,
} from 'three'
import {
FloatVertexAttributeTexture,
UIntVertexAttributeTexture,
} from './VertexAttributeTexture.js';
import { BYTES_PER_NODE } from '../core/Constants.js';
import {
BOUNDING_DATA_INDEX,
COUNT,
IS_LEAF,
RIGHT_NODE,
OFFSET,
SPLIT_AXIS,
} from '../core/utils/nodeBufferUtils.js';
import { getIndexArray, getVertexCount } from '../core/build/geometryUtils.js';
export class MeshBVHUniformStruct {
constructor() {
this.index = new UIntVertexAttributeTexture();
this.position = new FloatVertexAttributeTexture();
this.bvhBounds = new DataTexture();
this.bvhContents = new DataTexture();
this._cachedIndexAttr = null;
this.index.overrideItemSize = 3;
}
updateFrom( bvh ) {
const { geometry } = bvh;
bvhToTextures( bvh, this.bvhBounds, this.bvhContents );
this.position.updateFrom( geometry.attributes.position );
// dereference a new index attribute if we're using indirect storage
if ( bvh.indirect ) {
const indirectBuffer = bvh._indirectBuffer;
if (
this._cachedIndexAttr === null ||
this._cachedIndexAttr.count !== indirectBuffer.length
) {
if ( geometry.index ) {
this._cachedIndexAttr = geometry.index.clone();
} else {
const array = getIndexArray( getVertexCount( geometry ) );
this._cachedIndexAttr = new BufferAttribute( array, 1, false );
}
}
dereferenceIndex( geometry, indirectBuffer, this._cachedIndexAttr );
this.index.updateFrom( this._cachedIndexAttr );
} else {
this.index.updateFrom( geometry.index );
}
}
dispose() {
const { index, position, bvhBounds, bvhContents } = this;
if ( index ) index.dispose();
if ( position ) position.dispose();
if ( bvhBounds ) bvhBounds.dispose();
if ( bvhContents ) bvhContents.dispose();
}
}
function dereferenceIndex( geometry, indirectBuffer, target ) {
const unpacked = target.array;
const indexArray = geometry.index ? geometry.index.array : null;
for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
const i3 = 3 * i;
const v3 = 3 * indirectBuffer[ i ];
for ( let c = 0; c < 3; c ++ ) {
unpacked[ i3 + c ] = indexArray ? indexArray[ v3 + c ] : v3 + c;
}
}
}
function bvhToTextures( bvh, boundsTexture, contentsTexture ) {
const roots = bvh._roots;
if ( roots.length !== 1 ) {
throw new Error( 'MeshBVHUniformStruct: Multi-root BVHs not supported.' );
}
const root = roots[ 0 ];
const uint16Array = new Uint16Array( root );
const uint32Array = new Uint32Array( root );
const float32Array = new Float32Array( root );
// Both bounds need two elements per node so compute the height so it's twice as long as
// the width so we can expand the row by two and still have a square texture
const nodeCount = root.byteLength / BYTES_PER_NODE;
const boundsDimension = 2 * Math.ceil( Math.sqrt( nodeCount / 2 ) );
const boundsArray = new Float32Array( 4 * boundsDimension * boundsDimension );
const contentsDimension = Math.ceil( Math.sqrt( nodeCount ) );
const contentsArray = new Uint32Array( 2 * contentsDimension * contentsDimension );
for ( let i = 0; i < nodeCount; i ++ ) {
const nodeIndex32 = i * BYTES_PER_NODE / 4;
const nodeIndex16 = nodeIndex32 * 2;
const boundsIndex = BOUNDING_DATA_INDEX( nodeIndex32 );
for ( let b = 0; b < 3; b ++ ) {
boundsArray[ 8 * i + 0 + b ] = float32Array[ boundsIndex + 0 + b ];
boundsArray[ 8 * i + 4 + b ] = float32Array[ boundsIndex + 3 + b ];
}
if ( IS_LEAF( nodeIndex16, uint16Array ) ) {
const count = COUNT( nodeIndex16, uint16Array );
const offset = OFFSET( nodeIndex32, uint32Array );
const mergedLeafCount = 0xffff0000 | count;
contentsArray[ i * 2 + 0 ] = mergedLeafCount;
contentsArray[ i * 2 + 1 ] = offset;
} else {
const rightIndex = 4 * RIGHT_NODE( nodeIndex32, uint32Array ) / BYTES_PER_NODE;
const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
contentsArray[ i * 2 + 0 ] = splitAxis;
contentsArray[ i * 2 + 1 ] = rightIndex;
}
}
boundsTexture.image.data = boundsArray;
boundsTexture.image.width = boundsDimension;
boundsTexture.image.height = boundsDimension;
boundsTexture.format = RGBAFormat;
boundsTexture.type = FloatType;
boundsTexture.internalFormat = 'RGBA32F';
boundsTexture.minFilter = NearestFilter;
boundsTexture.magFilter = NearestFilter;
boundsTexture.generateMipmaps = false;
boundsTexture.needsUpdate = true;
boundsTexture.dispose();
contentsTexture.image.data = contentsArray;
contentsTexture.image.width = contentsDimension;
contentsTexture.image.height = contentsDimension;
contentsTexture.format = RGIntegerFormat;
contentsTexture.type = UnsignedIntType;
contentsTexture.internalFormat = 'RG32UI';
contentsTexture.minFilter = NearestFilter;
contentsTexture.magFilter = NearestFilter;
contentsTexture.generateMipmaps = false;
contentsTexture.needsUpdate = true;
contentsTexture.dispose();
}

View File

@@ -0,0 +1,309 @@
import {
DataTexture,
FloatType,
IntType,
UnsignedIntType,
ByteType,
UnsignedByteType,
ShortType,
UnsignedShortType,
RedFormat,
RGFormat,
RGBAFormat,
RedIntegerFormat,
RGIntegerFormat,
RGBAIntegerFormat,
NearestFilter,
} from 'three'
function countToStringFormat( count ) {
switch ( count ) {
case 1: return 'R';
case 2: return 'RG';
case 3: return 'RGBA';
case 4: return 'RGBA';
}
throw new Error();
}
function countToFormat( count ) {
switch ( count ) {
case 1: return RedFormat;
case 2: return RGFormat;
case 3: return RGBAFormat;
case 4: return RGBAFormat;
}
}
function countToIntFormat( count ) {
switch ( count ) {
case 1: return RedIntegerFormat;
case 2: return RGIntegerFormat;
case 3: return RGBAIntegerFormat;
case 4: return RGBAIntegerFormat;
}
}
export class VertexAttributeTexture extends DataTexture {
constructor() {
super();
this.minFilter = NearestFilter;
this.magFilter = NearestFilter;
this.generateMipmaps = false;
this.overrideItemSize = null;
this._forcedType = null;
}
updateFrom( attr ) {
const overrideItemSize = this.overrideItemSize;
const originalItemSize = attr.itemSize;
const originalCount = attr.count;
if ( overrideItemSize !== null ) {
if ( ( originalItemSize * originalCount ) % overrideItemSize !== 0.0 ) {
throw new Error( 'VertexAttributeTexture: overrideItemSize must divide evenly into buffer length.' );
}
attr.itemSize = overrideItemSize;
attr.count = originalCount * originalItemSize / overrideItemSize;
}
const itemSize = attr.itemSize;
const count = attr.count;
const normalized = attr.normalized;
const originalBufferCons = attr.array.constructor;
const byteCount = originalBufferCons.BYTES_PER_ELEMENT;
let targetType = this._forcedType;
let finalStride = itemSize;
// derive the type of texture this should be in the shader
if ( targetType === null ) {
switch ( originalBufferCons ) {
case Float32Array:
targetType = FloatType;
break;
case Uint8Array:
case Uint16Array:
case Uint32Array:
targetType = UnsignedIntType;
break;
case Int8Array:
case Int16Array:
case Int32Array:
targetType = IntType;
break;
}
}
// get the target format to store the texture as
let type, format, normalizeValue, targetBufferCons;
let internalFormat = countToStringFormat( itemSize );
switch ( targetType ) {
case FloatType:
normalizeValue = 1.0;
format = countToFormat( itemSize );
if ( normalized && byteCount === 1 ) {
targetBufferCons = originalBufferCons;
internalFormat += '8';
if ( originalBufferCons === Uint8Array ) {
type = UnsignedByteType;
} else {
type = ByteType;
internalFormat += '_SNORM';
}
} else {
targetBufferCons = Float32Array;
internalFormat += '32F';
type = FloatType;
}
break;
case IntType:
internalFormat += byteCount * 8 + 'I';
normalizeValue = normalized ? Math.pow( 2, originalBufferCons.BYTES_PER_ELEMENT * 8 - 1 ) : 1.0;
format = countToIntFormat( itemSize );
if ( byteCount === 1 ) {
targetBufferCons = Int8Array;
type = ByteType;
} else if ( byteCount === 2 ) {
targetBufferCons = Int16Array;
type = ShortType;
} else {
targetBufferCons = Int32Array;
type = IntType;
}
break;
case UnsignedIntType:
internalFormat += byteCount * 8 + 'UI';
normalizeValue = normalized ? Math.pow( 2, originalBufferCons.BYTES_PER_ELEMENT * 8 - 1 ) : 1.0;
format = countToIntFormat( itemSize );
if ( byteCount === 1 ) {
targetBufferCons = Uint8Array;
type = UnsignedByteType;
} else if ( byteCount === 2 ) {
targetBufferCons = Uint16Array;
type = UnsignedShortType;
} else {
targetBufferCons = Uint32Array;
type = UnsignedIntType;
}
break;
}
// there will be a mismatch between format length and final length because
// RGBFormat and RGBIntegerFormat was removed
if ( finalStride === 3 && ( format === RGBAFormat || format === RGBAIntegerFormat ) ) {
finalStride = 4;
}
// copy the data over to the new texture array
const dimension = Math.ceil( Math.sqrt( count ) ) || 1;
const length = finalStride * dimension * dimension;
const dataArray = new targetBufferCons( length );
// temporarily set the normalized state to false since we have custom normalization logic
const originalNormalized = attr.normalized;
attr.normalized = false;
for ( let i = 0; i < count; i ++ ) {
const ii = finalStride * i;
dataArray[ ii ] = attr.getX( i ) / normalizeValue;
if ( itemSize >= 2 ) {
dataArray[ ii + 1 ] = attr.getY( i ) / normalizeValue;
}
if ( itemSize >= 3 ) {
dataArray[ ii + 2 ] = attr.getZ( i ) / normalizeValue;
if ( finalStride === 4 ) {
dataArray[ ii + 3 ] = 1.0;
}
}
if ( itemSize >= 4 ) {
dataArray[ ii + 3 ] = attr.getW( i ) / normalizeValue;
}
}
attr.normalized = originalNormalized;
this.internalFormat = internalFormat;
this.format = format;
this.type = type;
this.image.width = dimension;
this.image.height = dimension;
this.image.data = dataArray;
this.needsUpdate = true;
this.dispose();
attr.itemSize = originalItemSize;
attr.count = originalCount;
}
}
export class UIntVertexAttributeTexture extends VertexAttributeTexture {
constructor() {
super();
this._forcedType = UnsignedIntType;
}
}
export class IntVertexAttributeTexture extends VertexAttributeTexture {
constructor() {
super();
this._forcedType = IntType;
}
}
export class FloatVertexAttributeTexture extends VertexAttributeTexture {
constructor() {
super();
this._forcedType = FloatType;
}
}

View File

@@ -0,0 +1,195 @@
// Distance to Point
export const bvh_distance_functions = /* glsl */`
float dot2( vec3 v ) {
return dot( v, v );
}
// https://www.shadertoy.com/view/ttfGWl
vec3 closestPointToTriangle( vec3 p, vec3 v0, vec3 v1, vec3 v2, out vec3 barycoord ) {
vec3 v10 = v1 - v0;
vec3 v21 = v2 - v1;
vec3 v02 = v0 - v2;
vec3 p0 = p - v0;
vec3 p1 = p - v1;
vec3 p2 = p - v2;
vec3 nor = cross( v10, v02 );
// method 2, in barycentric space
vec3 q = cross( nor, p0 );
float d = 1.0 / dot2( nor );
float u = d * dot( q, v02 );
float v = d * dot( q, v10 );
float w = 1.0 - u - v;
if( u < 0.0 ) {
w = clamp( dot( p2, v02 ) / dot2( v02 ), 0.0, 1.0 );
u = 0.0;
v = 1.0 - w;
} else if( v < 0.0 ) {
u = clamp( dot( p0, v10 ) / dot2( v10 ), 0.0, 1.0 );
v = 0.0;
w = 1.0 - u;
} else if( w < 0.0 ) {
v = clamp( dot( p1, v21 ) / dot2( v21 ), 0.0, 1.0 );
w = 0.0;
u = 1.0-v;
}
barycoord = vec3( u, v, w );
return u * v1 + v * v2 + w * v0;
}
float distanceToTriangles(
// geometry info and triangle range
sampler2D positionAttr, usampler2D indexAttr, uint offset, uint count,
// point and cut off range
vec3 point, float closestDistanceSquared,
// outputs
inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord, inout float side, inout vec3 outPoint
) {
bool found = false;
vec3 localBarycoord;
for ( uint i = offset, l = offset + count; i < l; i ++ ) {
uvec3 indices = uTexelFetch1D( indexAttr, i ).xyz;
vec3 a = texelFetch1D( positionAttr, indices.x ).rgb;
vec3 b = texelFetch1D( positionAttr, indices.y ).rgb;
vec3 c = texelFetch1D( positionAttr, indices.z ).rgb;
// get the closest point and barycoord
vec3 closestPoint = closestPointToTriangle( point, a, b, c, localBarycoord );
vec3 delta = point - closestPoint;
float sqDist = dot2( delta );
if ( sqDist < closestDistanceSquared ) {
// set the output results
closestDistanceSquared = sqDist;
faceIndices = uvec4( indices.xyz, i );
faceNormal = normalize( cross( a - b, b - c ) );
barycoord = localBarycoord;
outPoint = closestPoint;
side = sign( dot( faceNormal, delta ) );
}
}
return closestDistanceSquared;
}
float distanceSqToBounds( vec3 point, vec3 boundsMin, vec3 boundsMax ) {
vec3 clampedPoint = clamp( point, boundsMin, boundsMax );
vec3 delta = point - clampedPoint;
return dot( delta, delta );
}
float distanceSqToBVHNodeBoundsPoint( vec3 point, sampler2D bvhBounds, uint currNodeIndex ) {
uint cni2 = currNodeIndex * 2u;
vec3 boundsMin = texelFetch1D( bvhBounds, cni2 ).xyz;
vec3 boundsMax = texelFetch1D( bvhBounds, cni2 + 1u ).xyz;
return distanceSqToBounds( point, boundsMin, boundsMax );
}
// use a macro to hide the fact that we need to expand the struct into separate fields
#define\
bvhClosestPointToPoint(\
bvh,\
point, faceIndices, faceNormal, barycoord, side, outPoint\
)\
_bvhClosestPointToPoint(\
bvh.position, bvh.index, bvh.bvhBounds, bvh.bvhContents,\
point, faceIndices, faceNormal, barycoord, side, outPoint\
)
float _bvhClosestPointToPoint(
// bvh info
sampler2D bvh_position, usampler2D bvh_index, sampler2D bvh_bvhBounds, usampler2D bvh_bvhContents,
// point to check
vec3 point,
// output variables
inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord,
inout float side, inout vec3 outPoint
) {
// stack needs to be twice as long as the deepest tree we expect because
// we push both the left and right child onto the stack every traversal
int ptr = 0;
uint stack[ BVH_STACK_DEPTH ];
stack[ 0 ] = 0u;
float closestDistanceSquared = pow( 100000.0, 2.0 );
bool found = false;
while ( ptr > - 1 && ptr < BVH_STACK_DEPTH ) {
uint currNodeIndex = stack[ ptr ];
ptr --;
// check if we intersect the current bounds
float boundsHitDistance = distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, currNodeIndex );
if ( boundsHitDistance > closestDistanceSquared ) {
continue;
}
uvec2 boundsInfo = uTexelFetch1D( bvh_bvhContents, currNodeIndex ).xy;
bool isLeaf = bool( boundsInfo.x & 0xffff0000u );
if ( isLeaf ) {
uint count = boundsInfo.x & 0x0000ffffu;
uint offset = boundsInfo.y;
closestDistanceSquared = distanceToTriangles(
bvh_position, bvh_index, offset, count, point, closestDistanceSquared,
// outputs
faceIndices, faceNormal, barycoord, side, outPoint
);
} else {
uint leftIndex = currNodeIndex + 1u;
uint splitAxis = boundsInfo.x & 0x0000ffffu;
uint rightIndex = boundsInfo.y;
bool leftToRight = distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, leftIndex ) < distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, rightIndex );//rayDirection[ splitAxis ] >= 0.0;
uint c1 = leftToRight ? leftIndex : rightIndex;
uint c2 = leftToRight ? rightIndex : leftIndex;
// set c2 in the stack so we traverse it later. We need to keep track of a pointer in
// the stack while we traverse. The second pointer added is the one that will be
// traversed first
ptr ++;
stack[ ptr ] = c2;
ptr ++;
stack[ ptr ] = c1;
}
}
return sqrt( closestDistanceSquared );
}
`;

View File

@@ -0,0 +1,213 @@
export const bvh_ray_functions = /* glsl */`
#ifndef TRI_INTERSECT_EPSILON
#define TRI_INTERSECT_EPSILON 1e-5
#endif
// Raycasting
bool intersectsBounds( vec3 rayOrigin, vec3 rayDirection, vec3 boundsMin, vec3 boundsMax, out float dist ) {
// https://www.reddit.com/r/opengl/comments/8ntzz5/fast_glsl_ray_box_intersection/
// https://tavianator.com/2011/ray_box.html
vec3 invDir = 1.0 / rayDirection;
// find intersection distances for each plane
vec3 tMinPlane = invDir * ( boundsMin - rayOrigin );
vec3 tMaxPlane = invDir * ( boundsMax - rayOrigin );
// get the min and max distances from each intersection
vec3 tMinHit = min( tMaxPlane, tMinPlane );
vec3 tMaxHit = max( tMaxPlane, tMinPlane );
// get the furthest hit distance
vec2 t = max( tMinHit.xx, tMinHit.yz );
float t0 = max( t.x, t.y );
// get the minimum hit distance
t = min( tMaxHit.xx, tMaxHit.yz );
float t1 = min( t.x, t.y );
// set distance to 0.0 if the ray starts inside the box
dist = max( t0, 0.0 );
return t1 >= dist;
}
bool intersectsTriangle(
vec3 rayOrigin, vec3 rayDirection, vec3 a, vec3 b, vec3 c,
out vec3 barycoord, out vec3 norm, out float dist, out float side
) {
// https://stackoverflow.com/questions/42740765/intersection-between-line-and-triangle-in-3d
vec3 edge1 = b - a;
vec3 edge2 = c - a;
norm = cross( edge1, edge2 );
float det = - dot( rayDirection, norm );
float invdet = 1.0 / det;
vec3 AO = rayOrigin - a;
vec3 DAO = cross( AO, rayDirection );
vec4 uvt;
uvt.x = dot( edge2, DAO ) * invdet;
uvt.y = - dot( edge1, DAO ) * invdet;
uvt.z = dot( AO, norm ) * invdet;
uvt.w = 1.0 - uvt.x - uvt.y;
// set the hit information
barycoord = uvt.wxy; // arranged in A, B, C order
dist = uvt.z;
side = sign( det );
norm = side * normalize( norm );
// add an epsilon to avoid misses between triangles
uvt += vec4( TRI_INTERSECT_EPSILON );
return all( greaterThanEqual( uvt, vec4( 0.0 ) ) );
}
bool intersectTriangles(
// geometry info and triangle range
sampler2D positionAttr, usampler2D indexAttr, uint offset, uint count,
// ray
vec3 rayOrigin, vec3 rayDirection,
// outputs
inout float minDistance, inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord,
inout float side, inout float dist
) {
bool found = false;
vec3 localBarycoord, localNormal;
float localDist, localSide;
for ( uint i = offset, l = offset + count; i < l; i ++ ) {
uvec3 indices = uTexelFetch1D( indexAttr, i ).xyz;
vec3 a = texelFetch1D( positionAttr, indices.x ).rgb;
vec3 b = texelFetch1D( positionAttr, indices.y ).rgb;
vec3 c = texelFetch1D( positionAttr, indices.z ).rgb;
if (
intersectsTriangle( rayOrigin, rayDirection, a, b, c, localBarycoord, localNormal, localDist, localSide )
&& localDist < minDistance
) {
found = true;
minDistance = localDist;
faceIndices = uvec4( indices.xyz, i );
faceNormal = localNormal;
side = localSide;
barycoord = localBarycoord;
dist = localDist;
}
}
return found;
}
bool intersectsBVHNodeBounds( vec3 rayOrigin, vec3 rayDirection, sampler2D bvhBounds, uint currNodeIndex, out float dist ) {
uint cni2 = currNodeIndex * 2u;
vec3 boundsMin = texelFetch1D( bvhBounds, cni2 ).xyz;
vec3 boundsMax = texelFetch1D( bvhBounds, cni2 + 1u ).xyz;
return intersectsBounds( rayOrigin, rayDirection, boundsMin, boundsMax, dist );
}
// use a macro to hide the fact that we need to expand the struct into separate fields
#define\
bvhIntersectFirstHit(\
bvh,\
rayOrigin, rayDirection, faceIndices, faceNormal, barycoord, side, dist\
)\
_bvhIntersectFirstHit(\
bvh.position, bvh.index, bvh.bvhBounds, bvh.bvhContents,\
rayOrigin, rayDirection, faceIndices, faceNormal, barycoord, side, dist\
)
bool _bvhIntersectFirstHit(
// bvh info
sampler2D bvh_position, usampler2D bvh_index, sampler2D bvh_bvhBounds, usampler2D bvh_bvhContents,
// ray
vec3 rayOrigin, vec3 rayDirection,
// output variables split into separate variables due to output precision
inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord,
inout float side, inout float dist
) {
// stack needs to be twice as long as the deepest tree we expect because
// we push both the left and right child onto the stack every traversal
int ptr = 0;
uint stack[ BVH_STACK_DEPTH ];
stack[ 0 ] = 0u;
float triangleDistance = INFINITY;
bool found = false;
while ( ptr > - 1 && ptr < BVH_STACK_DEPTH ) {
uint currNodeIndex = stack[ ptr ];
ptr --;
// check if we intersect the current bounds
float boundsHitDistance;
if (
! intersectsBVHNodeBounds( rayOrigin, rayDirection, bvh_bvhBounds, currNodeIndex, boundsHitDistance )
|| boundsHitDistance > triangleDistance
) {
continue;
}
uvec2 boundsInfo = uTexelFetch1D( bvh_bvhContents, currNodeIndex ).xy;
bool isLeaf = bool( boundsInfo.x & 0xffff0000u );
if ( isLeaf ) {
uint count = boundsInfo.x & 0x0000ffffu;
uint offset = boundsInfo.y;
found = intersectTriangles(
bvh_position, bvh_index, offset, count,
rayOrigin, rayDirection, triangleDistance,
faceIndices, faceNormal, barycoord, side, dist
) || found;
} else {
uint leftIndex = currNodeIndex + 1u;
uint splitAxis = boundsInfo.x & 0x0000ffffu;
uint rightIndex = boundsInfo.y;
bool leftToRight = rayDirection[ splitAxis ] >= 0.0;
uint c1 = leftToRight ? leftIndex : rightIndex;
uint c2 = leftToRight ? rightIndex : leftIndex;
// set c2 in the stack so we traverse it later. We need to keep track of a pointer in
// the stack while we traverse. The second pointer added is the one that will be
// traversed first
ptr ++;
stack[ ptr ] = c2;
ptr ++;
stack[ ptr ] = c1;
}
}
return found;
}
`;

View File

@@ -0,0 +1,14 @@
// Note that a struct cannot be used for the hit record including faceIndices, faceNormal, barycoord,
// side, and dist because on some mobile GPUS (such as Adreno) numbers are afforded less precision specifically
// when in a struct leading to inaccurate hit results. See KhronosGroup/WebGL#3351 for more details.
export const bvh_struct_definitions = /* glsl */`
struct BVH {
usampler2D index;
sampler2D position;
sampler2D bvhBounds;
usampler2D bvhContents;
};
`;

View File

@@ -0,0 +1,83 @@
export const common_functions = /* glsl */`
// A stack of uint32 indices can can store the indices for
// a perfectly balanced tree with a depth up to 31. Lower stack
// depth gets higher performance.
//
// However not all trees are balanced. Best value to set this to
// is the trees max depth.
#ifndef BVH_STACK_DEPTH
#define BVH_STACK_DEPTH 60
#endif
#ifndef INFINITY
#define INFINITY 1e20
#endif
// Utilities
uvec4 uTexelFetch1D( usampler2D tex, uint index ) {
uint width = uint( textureSize( tex, 0 ).x );
uvec2 uv;
uv.x = index % width;
uv.y = index / width;
return texelFetch( tex, ivec2( uv ), 0 );
}
ivec4 iTexelFetch1D( isampler2D tex, uint index ) {
uint width = uint( textureSize( tex, 0 ).x );
uvec2 uv;
uv.x = index % width;
uv.y = index / width;
return texelFetch( tex, ivec2( uv ), 0 );
}
vec4 texelFetch1D( sampler2D tex, uint index ) {
uint width = uint( textureSize( tex, 0 ).x );
uvec2 uv;
uv.x = index % width;
uv.y = index / width;
return texelFetch( tex, ivec2( uv ), 0 );
}
vec4 textureSampleBarycoord( sampler2D tex, vec3 barycoord, uvec3 faceIndices ) {
return
barycoord.x * texelFetch1D( tex, faceIndices.x ) +
barycoord.y * texelFetch1D( tex, faceIndices.y ) +
barycoord.z * texelFetch1D( tex, faceIndices.z );
}
void ndcToCameraRay(
vec2 coord, mat4 cameraWorld, mat4 invProjectionMatrix,
out vec3 rayOrigin, out vec3 rayDirection
) {
// get camera look direction and near plane for camera clipping
vec4 lookDirection = cameraWorld * vec4( 0.0, 0.0, - 1.0, 0.0 );
vec4 nearVector = invProjectionMatrix * vec4( 0.0, 0.0, - 1.0, 1.0 );
float near = abs( nearVector.z / nearVector.w );
// get the camera direction and position from camera matrices
vec4 origin = cameraWorld * vec4( 0.0, 0.0, 0.0, 1.0 );
vec4 direction = invProjectionMatrix * vec4( coord, 0.5, 1.0 );
direction /= direction.w;
direction = cameraWorld * direction - origin;
// slide the origin along the ray until it sits at the near clip plane position
origin.xyz += direction.xyz * near / dot( direction, lookDirection );
rayOrigin = origin.xyz;
rayDirection = direction.xyz;
}
`;

View File

@@ -0,0 +1,22 @@
export { MeshBVH } from './core/MeshBVH.js'
export { MeshBVHHelper } from './objects/MeshBVHHelper.js'
export { CENTER, AVERAGE, SAH, NOT_INTERSECTED, INTERSECTED, CONTAINED } from './core/Constants.js'
export { getBVHExtremes, estimateMemoryInBytes, getJSONStructure, validateBounds } from './debug/Debug.js'
export { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from './utils/ExtensionUtilities.js'
export { getTriangleHitPointInfo } from './utils/TriangleUtilities.js'
export * from './math/ExtendedTriangle.js'
export * from './math/OrientedBox.js'
export * from './gpu/MeshBVHUniformStruct.js'
export * from './gpu/VertexAttributeTexture.js'
export * from './utils/StaticGeometryGenerator.js'
export * as BVHShaderGLSL from './gpu/BVHShaderGLSL.js'
// backwards compatibility
import * as BVHShaderGLSL from './gpu/BVHShaderGLSL.js'
export const shaderStructs = BVHShaderGLSL.bvh_struct_definitions
export const shaderDistanceFunction = BVHShaderGLSL.bvh_distance_functions
export const shaderIntersectFunction = `
${BVHShaderGLSL.common_functions}
${BVHShaderGLSL.bvh_ray_functions}
`
// version: 0.7.4

View File

@@ -0,0 +1,514 @@
import { Triangle, Vector3, Line3, Sphere, Plane } from 'three'
import { SeparatingAxisBounds } from './SeparatingAxisBounds.js';
import { closestPointsSegmentToSegment, sphereIntersectTriangle } from './MathUtilities.js';
const ZERO_EPSILON = 1e-15;
function isNearZero( value ) {
return Math.abs( value ) < ZERO_EPSILON;
}
export class ExtendedTriangle extends Triangle {
constructor( ...args ) {
super( ...args );
this.isExtendedTriangle = true;
this.satAxes = new Array( 4 ).fill().map( () => new Vector3() );
this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() );
this.points = [ this.a, this.b, this.c ];
this.sphere = new Sphere();
this.plane = new Plane();
this.needsUpdate = true;
}
intersectsSphere( sphere ) {
return sphereIntersectTriangle( sphere, this );
}
update() {
const a = this.a;
const b = this.b;
const c = this.c;
const points = this.points;
const satAxes = this.satAxes;
const satBounds = this.satBounds;
const axis0 = satAxes[ 0 ];
const sab0 = satBounds[ 0 ];
this.getNormal( axis0 );
sab0.setFromPoints( axis0, points );
const axis1 = satAxes[ 1 ];
const sab1 = satBounds[ 1 ];
axis1.subVectors( a, b );
sab1.setFromPoints( axis1, points );
const axis2 = satAxes[ 2 ];
const sab2 = satBounds[ 2 ];
axis2.subVectors( b, c );
sab2.setFromPoints( axis2, points );
const axis3 = satAxes[ 3 ];
const sab3 = satBounds[ 3 ];
axis3.subVectors( c, a );
sab3.setFromPoints( axis3, points );
this.sphere.setFromPoints( this.points );
this.plane.setFromNormalAndCoplanarPoint( axis0, a );
this.needsUpdate = false;
}
}
ExtendedTriangle.prototype.closestPointToSegment = ( function () {
const point1 = new Vector3();
const point2 = new Vector3();
const edge = new Line3();
return function distanceToSegment( segment, target1 = null, target2 = null ) {
const { start, end } = segment;
const points = this.points;
let distSq;
let closestDistanceSq = Infinity;
// check the triangle edges
for ( let i = 0; i < 3; i ++ ) {
const nexti = ( i + 1 ) % 3;
edge.start.copy( points[ i ] );
edge.end.copy( points[ nexti ] );
closestPointsSegmentToSegment( edge, segment, point1, point2 );
distSq = point1.distanceToSquared( point2 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
}
}
// check end points
this.closestPointToPoint( start, point1 );
distSq = start.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( start );
}
this.closestPointToPoint( end, point1 );
distSq = end.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( end );
}
return Math.sqrt( closestDistanceSq );
};
} )();
ExtendedTriangle.prototype.intersectsTriangle = ( function () {
const saTri2 = new ExtendedTriangle();
const arr1 = new Array( 3 );
const arr2 = new Array( 3 );
const cachedSatBounds = new SeparatingAxisBounds();
const cachedSatBounds2 = new SeparatingAxisBounds();
const cachedAxis = new Vector3();
const dir = new Vector3();
const dir1 = new Vector3();
const dir2 = new Vector3();
const tempDir = new Vector3();
const edge = new Line3();
const edge1 = new Line3();
const edge2 = new Line3();
const tempPoint = new Vector3();
function triIntersectPlane( tri, plane, targetEdge ) {
// find the edge that intersects the other triangle plane
const points = tri.points;
let count = 0;
let startPointIntersection = - 1;
for ( let i = 0; i < 3; i ++ ) {
const { start, end } = edge;
start.copy( points[ i ] );
end.copy( points[ ( i + 1 ) % 3 ] );
edge.delta( dir );
const startIntersects = isNearZero( plane.distanceToPoint( start ) );
if ( isNearZero( plane.normal.dot( dir ) ) && startIntersects ) {
// if the edge lies on the plane then take the line
targetEdge.copy( edge );
count = 2;
break;
}
// check if the start point is near the plane because "intersectLine" is not robust to that case
const doesIntersect = plane.intersectLine( edge, tempPoint );
if ( ! doesIntersect && startIntersects ) {
tempPoint.copy( start );
}
// ignore the end point
if ( ( doesIntersect || startIntersects ) && ! isNearZero( tempPoint.distanceTo( end ) ) ) {
if ( count <= 1 ) {
// assign to the start or end point and save which index was snapped to
// the start point if necessary
const point = count === 1 ? targetEdge.start : targetEdge.end;
point.copy( tempPoint );
if ( startIntersects ) {
startPointIntersection = count;
}
} else if ( count >= 2 ) {
// if we're here that means that there must have been one point that had
// snapped to the start point so replace it here
const point = startPointIntersection === 1 ? targetEdge.start : targetEdge.end;
point.copy( tempPoint );
count = 2;
break;
}
count ++;
if ( count === 2 && startPointIntersection === - 1 ) {
break;
}
}
}
return count;
}
// TODO: If the triangles are coplanar and intersecting the target is nonsensical. It should at least
// be a line contained by both triangles if not a different special case somehow represented in the return result.
return function intersectsTriangle( other, target = null, suppressLog = false ) {
if ( this.needsUpdate ) {
this.update();
}
if ( ! other.isExtendedTriangle ) {
saTri2.copy( other );
saTri2.update();
other = saTri2;
} else if ( other.needsUpdate ) {
other.update();
}
const plane1 = this.plane;
const plane2 = other.plane;
if ( Math.abs( plane1.normal.dot( plane2.normal ) ) > 1.0 - 1e-10 ) {
// perform separating axis intersection test only for coplanar triangles
const satBounds1 = this.satBounds;
const satAxes1 = this.satAxes;
arr2[ 0 ] = other.a;
arr2[ 1 ] = other.b;
arr2[ 2 ] = other.c;
for ( let i = 0; i < 4; i ++ ) {
const sb = satBounds1[ i ];
const sa = satAxes1[ i ];
cachedSatBounds.setFromPoints( sa, arr2 );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
const satBounds2 = other.satBounds;
const satAxes2 = other.satAxes;
arr1[ 0 ] = this.a;
arr1[ 1 ] = this.b;
arr1[ 2 ] = this.c;
for ( let i = 0; i < 4; i ++ ) {
const sb = satBounds2[ i ];
const sa = satAxes2[ i ];
cachedSatBounds.setFromPoints( sa, arr1 );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
// check crossed axes
for ( let i = 0; i < 4; i ++ ) {
const sa1 = satAxes1[ i ];
for ( let i2 = 0; i2 < 4; i2 ++ ) {
const sa2 = satAxes2[ i2 ];
cachedAxis.crossVectors( sa1, sa2 );
cachedSatBounds.setFromPoints( cachedAxis, arr1 );
cachedSatBounds2.setFromPoints( cachedAxis, arr2 );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
}
if ( target ) {
// TODO find two points that intersect on the edges and make that the result
if ( ! suppressLog ) {
console.warn( 'ExtendedTriangle.intersectsTriangle: Triangles are coplanar which does not support an output edge. Setting edge to 0, 0, 0.' );
}
target.start.set( 0, 0, 0 );
target.end.set( 0, 0, 0 );
}
return true;
} else {
// find the edge that intersects the other triangle plane
const count1 = triIntersectPlane( this, plane2, edge1 );
if ( count1 === 1 && other.containsPoint( edge1.end ) ) {
if ( target ) {
target.start.copy( edge1.end );
target.end.copy( edge1.end );
}
return true;
} else if ( count1 !== 2 ) {
return false;
}
// find the other triangles edge that intersects this plane
const count2 = triIntersectPlane( other, plane1, edge2 );
if ( count2 === 1 && this.containsPoint( edge2.end ) ) {
if ( target ) {
target.start.copy( edge2.end );
target.end.copy( edge2.end );
}
return true;
} else if ( count2 !== 2 ) {
return false;
}
// find swap the second edge so both lines are running the same direction
edge1.delta( dir1 );
edge2.delta( dir2 );
if ( dir1.dot( dir2 ) < 0 ) {
let tmp = edge2.start;
edge2.start = edge2.end;
edge2.end = tmp;
}
// check if the edges are overlapping
const s1 = edge1.start.dot( dir1 );
const e1 = edge1.end.dot( dir1 );
const s2 = edge2.start.dot( dir1 );
const e2 = edge2.end.dot( dir1 );
const separated1 = e1 < s2;
const separated2 = s1 < e2;
if ( s1 !== e2 && s2 !== e1 && separated1 === separated2 ) {
return false;
}
// assign the target output
if ( target ) {
tempDir.subVectors( edge1.start, edge2.start );
if ( tempDir.dot( dir1 ) > 0 ) {
target.start.copy( edge1.start );
} else {
target.start.copy( edge2.start );
}
tempDir.subVectors( edge1.end, edge2.end );
if ( tempDir.dot( dir1 ) < 0 ) {
target.end.copy( edge1.end );
} else {
target.end.copy( edge2.end );
}
}
return true;
}
};
} )();
ExtendedTriangle.prototype.distanceToPoint = ( function () {
const target = new Vector3();
return function distanceToPoint( point ) {
this.closestPointToPoint( point, target );
return point.distanceTo( target );
};
} )();
ExtendedTriangle.prototype.distanceToTriangle = ( function () {
const point = new Vector3();
const point2 = new Vector3();
const cornerFields = [ 'a', 'b', 'c' ];
const line1 = new Line3();
const line2 = new Line3();
return function distanceToTriangle( other, target1 = null, target2 = null ) {
const lineTarget = target1 || target2 ? line1 : null;
if ( this.intersectsTriangle( other, lineTarget ) ) {
if ( target1 || target2 ) {
if ( target1 ) lineTarget.getCenter( target1 );
if ( target2 ) lineTarget.getCenter( target2 );
}
return 0;
}
let closestDistanceSq = Infinity;
// check all point distances
for ( let i = 0; i < 3; i ++ ) {
let dist;
const field = cornerFields[ i ];
const otherVec = other[ field ];
this.closestPointToPoint( otherVec, point );
dist = otherVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( otherVec );
}
const thisVec = this[ field ];
other.closestPointToPoint( thisVec, point );
dist = thisVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( thisVec );
if ( target2 ) target2.copy( point );
}
}
for ( let i = 0; i < 3; i ++ ) {
const f11 = cornerFields[ i ];
const f12 = cornerFields[ ( i + 1 ) % 3 ];
line1.set( this[ f11 ], this[ f12 ] );
for ( let i2 = 0; i2 < 3; i2 ++ ) {
const f21 = cornerFields[ i2 ];
const f22 = cornerFields[ ( i2 + 1 ) % 3 ];
line2.set( other[ f21 ], other[ f22 ] );
closestPointsSegmentToSegment( line1, line2, point, point2 );
const dist = point.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( point2 );
}
}
}
return Math.sqrt( closestDistanceSq );
};
} )();

View File

@@ -0,0 +1,203 @@
import { Vector3, Vector2, Plane, Line3 } from 'three'
export const closestPointLineToLine = ( function () {
// https://github.com/juj/MathGeoLib/blob/master/src/Geometry/Line.cpp#L56
const dir1 = new Vector3();
const dir2 = new Vector3();
const v02 = new Vector3();
return function closestPointLineToLine( l1, l2, result ) {
const v0 = l1.start;
const v10 = dir1;
const v2 = l2.start;
const v32 = dir2;
v02.subVectors( v0, v2 );
dir1.subVectors( l1.end, l1.start );
dir2.subVectors( l2.end, l2.start );
// float d0232 = v02.Dot(v32);
const d0232 = v02.dot( v32 );
// float d3210 = v32.Dot(v10);
const d3210 = v32.dot( v10 );
// float d3232 = v32.Dot(v32);
const d3232 = v32.dot( v32 );
// float d0210 = v02.Dot(v10);
const d0210 = v02.dot( v10 );
// float d1010 = v10.Dot(v10);
const d1010 = v10.dot( v10 );
// float denom = d1010*d3232 - d3210*d3210;
const denom = d1010 * d3232 - d3210 * d3210;
let d, d2;
if ( denom !== 0 ) {
d = ( d0232 * d3210 - d0210 * d3232 ) / denom;
} else {
d = 0;
}
d2 = ( d0232 + d * d3210 ) / d3232;
result.x = d;
result.y = d2;
};
} )();
export const closestPointsSegmentToSegment = ( function () {
// https://github.com/juj/MathGeoLib/blob/master/src/Geometry/LineSegment.cpp#L187
const paramResult = new Vector2();
const temp1 = new Vector3();
const temp2 = new Vector3();
return function closestPointsSegmentToSegment( l1, l2, target1, target2 ) {
closestPointLineToLine( l1, l2, paramResult );
let d = paramResult.x;
let d2 = paramResult.y;
if ( d >= 0 && d <= 1 && d2 >= 0 && d2 <= 1 ) {
l1.at( d, target1 );
l2.at( d2, target2 );
return;
} else if ( d >= 0 && d <= 1 ) {
// Only d2 is out of bounds.
if ( d2 < 0 ) {
l2.at( 0, target2 );
} else {
l2.at( 1, target2 );
}
l1.closestPointToPoint( target2, true, target1 );
return;
} else if ( d2 >= 0 && d2 <= 1 ) {
// Only d is out of bounds.
if ( d < 0 ) {
l1.at( 0, target1 );
} else {
l1.at( 1, target1 );
}
l2.closestPointToPoint( target1, true, target2 );
return;
} else {
// Both u and u2 are out of bounds.
let p;
if ( d < 0 ) {
p = l1.start;
} else {
p = l1.end;
}
let p2;
if ( d2 < 0 ) {
p2 = l2.start;
} else {
p2 = l2.end;
}
const closestPoint = temp1;
const closestPoint2 = temp2;
l1.closestPointToPoint( p2, true, temp1 );
l2.closestPointToPoint( p, true, temp2 );
if ( closestPoint.distanceToSquared( p2 ) <= closestPoint2.distanceToSquared( p ) ) {
target1.copy( closestPoint );
target2.copy( p2 );
return;
} else {
target1.copy( p );
target2.copy( closestPoint2 );
return;
}
}
};
} )();
export const sphereIntersectTriangle = ( function () {
// https://stackoverflow.com/questions/34043955/detect-collision-between-sphere-and-triangle-in-three-js
const closestPointTemp = new Vector3();
const projectedPointTemp = new Vector3();
const planeTemp = new Plane();
const lineTemp = new Line3();
return function sphereIntersectTriangle( sphere, triangle ) {
const { radius, center } = sphere;
const { a, b, c } = triangle;
// phase 1
lineTemp.start = a;
lineTemp.end = b;
const closestPoint1 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint1.distanceTo( center ) <= radius ) return true;
lineTemp.start = a;
lineTemp.end = c;
const closestPoint2 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint2.distanceTo( center ) <= radius ) return true;
lineTemp.start = b;
lineTemp.end = c;
const closestPoint3 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
if ( closestPoint3.distanceTo( center ) <= radius ) return true;
// phase 2
const plane = triangle.getPlane( planeTemp );
const dp = Math.abs( plane.distanceToPoint( center ) );
if ( dp <= radius ) {
const pp = plane.projectPoint( center, projectedPointTemp );
const cp = triangle.containsPoint( pp );
if ( cp ) return true;
}
return false;
};
} )();

View File

@@ -0,0 +1,421 @@
import { Vector3, Matrix4, Line3 } from 'three'
import { SeparatingAxisBounds } from './SeparatingAxisBounds.js';
import { ExtendedTriangle } from './ExtendedTriangle.js';
import { closestPointsSegmentToSegment } from './MathUtilities.js';
export class OrientedBox {
constructor( min, max, matrix ) {
this.isOrientedBox = true;
this.min = new Vector3();
this.max = new Vector3();
this.matrix = new Matrix4();
this.invMatrix = new Matrix4();
this.points = new Array( 8 ).fill().map( () => new Vector3() );
this.satAxes = new Array( 3 ).fill().map( () => new Vector3() );
this.satBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() );
this.alignedSatBounds = new Array( 3 ).fill().map( () => new SeparatingAxisBounds() );
this.needsUpdate = false;
if ( min ) this.min.copy( min );
if ( max ) this.max.copy( max );
if ( matrix ) this.matrix.copy( matrix );
}
set( min, max, matrix ) {
this.min.copy( min );
this.max.copy( max );
this.matrix.copy( matrix );
this.needsUpdate = true;
}
copy( other ) {
this.min.copy( other.min );
this.max.copy( other.max );
this.matrix.copy( other.matrix );
this.needsUpdate = true;
}
}
OrientedBox.prototype.update = ( function () {
return function update() {
const matrix = this.matrix;
const min = this.min;
const max = this.max;
const points = this.points;
for ( let x = 0; x <= 1; x ++ ) {
for ( let y = 0; y <= 1; y ++ ) {
for ( let z = 0; z <= 1; z ++ ) {
const i = ( ( 1 << 0 ) * x ) | ( ( 1 << 1 ) * y ) | ( ( 1 << 2 ) * z );
const v = points[ i ];
v.x = x ? max.x : min.x;
v.y = y ? max.y : min.y;
v.z = z ? max.z : min.z;
v.applyMatrix4( matrix );
}
}
}
const satBounds = this.satBounds;
const satAxes = this.satAxes;
const minVec = points[ 0 ];
for ( let i = 0; i < 3; i ++ ) {
const axis = satAxes[ i ];
const sb = satBounds[ i ];
const index = 1 << i;
const pi = points[ index ];
axis.subVectors( minVec, pi );
sb.setFromPoints( axis, points );
}
const alignedSatBounds = this.alignedSatBounds;
alignedSatBounds[ 0 ].setFromPointsField( points, 'x' );
alignedSatBounds[ 1 ].setFromPointsField( points, 'y' );
alignedSatBounds[ 2 ].setFromPointsField( points, 'z' );
this.invMatrix.copy( this.matrix ).invert();
this.needsUpdate = false;
};
} )();
OrientedBox.prototype.intersectsBox = ( function () {
const aabbBounds = new SeparatingAxisBounds();
return function intersectsBox( box ) {
// TODO: should this be doing SAT against the AABB?
if ( this.needsUpdate ) {
this.update();
}
const min = box.min;
const max = box.max;
const satBounds = this.satBounds;
const satAxes = this.satAxes;
const alignedSatBounds = this.alignedSatBounds;
aabbBounds.min = min.x;
aabbBounds.max = max.x;
if ( alignedSatBounds[ 0 ].isSeparated( aabbBounds ) ) return false;
aabbBounds.min = min.y;
aabbBounds.max = max.y;
if ( alignedSatBounds[ 1 ].isSeparated( aabbBounds ) ) return false;
aabbBounds.min = min.z;
aabbBounds.max = max.z;
if ( alignedSatBounds[ 2 ].isSeparated( aabbBounds ) ) return false;
for ( let i = 0; i < 3; i ++ ) {
const axis = satAxes[ i ];
const sb = satBounds[ i ];
aabbBounds.setFromBox( axis, box );
if ( sb.isSeparated( aabbBounds ) ) return false;
}
return true;
};
} )();
OrientedBox.prototype.intersectsTriangle = ( function () {
const saTri = new ExtendedTriangle();
const pointsArr = new Array( 3 );
const cachedSatBounds = new SeparatingAxisBounds();
const cachedSatBounds2 = new SeparatingAxisBounds();
const cachedAxis = new Vector3();
return function intersectsTriangle( triangle ) {
if ( this.needsUpdate ) {
this.update();
}
if ( ! triangle.isExtendedTriangle ) {
saTri.copy( triangle );
saTri.update();
triangle = saTri;
} else if ( triangle.needsUpdate ) {
triangle.update();
}
const satBounds = this.satBounds;
const satAxes = this.satAxes;
pointsArr[ 0 ] = triangle.a;
pointsArr[ 1 ] = triangle.b;
pointsArr[ 2 ] = triangle.c;
for ( let i = 0; i < 3; i ++ ) {
const sb = satBounds[ i ];
const sa = satAxes[ i ];
cachedSatBounds.setFromPoints( sa, pointsArr );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
const triSatBounds = triangle.satBounds;
const triSatAxes = triangle.satAxes;
const points = this.points;
for ( let i = 0; i < 3; i ++ ) {
const sb = triSatBounds[ i ];
const sa = triSatAxes[ i ];
cachedSatBounds.setFromPoints( sa, points );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
}
// check crossed axes
for ( let i = 0; i < 3; i ++ ) {
const sa1 = satAxes[ i ];
for ( let i2 = 0; i2 < 4; i2 ++ ) {
const sa2 = triSatAxes[ i2 ];
cachedAxis.crossVectors( sa1, sa2 );
cachedSatBounds.setFromPoints( cachedAxis, pointsArr );
cachedSatBounds2.setFromPoints( cachedAxis, points );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
}
return true;
};
} )();
OrientedBox.prototype.closestPointToPoint = ( function () {
return function closestPointToPoint( point, target1 ) {
if ( this.needsUpdate ) {
this.update();
}
target1
.copy( point )
.applyMatrix4( this.invMatrix )
.clamp( this.min, this.max )
.applyMatrix4( this.matrix );
return target1;
};
} )();
OrientedBox.prototype.distanceToPoint = ( function () {
const target = new Vector3();
return function distanceToPoint( point ) {
this.closestPointToPoint( point, target );
return point.distanceTo( target );
};
} )();
OrientedBox.prototype.distanceToBox = ( function () {
const xyzFields = [ 'x', 'y', 'z' ];
const segments1 = new Array( 12 ).fill().map( () => new Line3() );
const segments2 = new Array( 12 ).fill().map( () => new Line3() );
const point1 = new Vector3();
const point2 = new Vector3();
// early out if we find a value below threshold
return function distanceToBox( box, threshold = 0, target1 = null, target2 = null ) {
if ( this.needsUpdate ) {
this.update();
}
if ( this.intersectsBox( box ) ) {
if ( target1 || target2 ) {
box.getCenter( point2 );
this.closestPointToPoint( point2, point1 );
box.closestPointToPoint( point1, point2 );
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
}
return 0;
}
const threshold2 = threshold * threshold;
const min = box.min;
const max = box.max;
const points = this.points;
// iterate over every edge and compare distances
let closestDistanceSq = Infinity;
// check over all these points
for ( let i = 0; i < 8; i ++ ) {
const p = points[ i ];
point2.copy( p ).clamp( min, max );
const dist = p.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( p );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
// generate and check all line segment distances
let count = 0;
for ( let i = 0; i < 3; i ++ ) {
for ( let i1 = 0; i1 <= 1; i1 ++ ) {
for ( let i2 = 0; i2 <= 1; i2 ++ ) {
const nextIndex = ( i + 1 ) % 3;
const nextIndex2 = ( i + 2 ) % 3;
// get obb line segments
const index = i1 << nextIndex | i2 << nextIndex2;
const index2 = 1 << i | i1 << nextIndex | i2 << nextIndex2;
const p1 = points[ index ];
const p2 = points[ index2 ];
const line1 = segments1[ count ];
line1.set( p1, p2 );
// get aabb line segments
const f1 = xyzFields[ i ];
const f2 = xyzFields[ nextIndex ];
const f3 = xyzFields[ nextIndex2 ];
const line2 = segments2[ count ];
const start = line2.start;
const end = line2.end;
start[ f1 ] = min[ f1 ];
start[ f2 ] = i1 ? min[ f2 ] : max[ f2 ];
start[ f3 ] = i2 ? min[ f3 ] : max[ f2 ];
end[ f1 ] = max[ f1 ];
end[ f2 ] = i1 ? min[ f2 ] : max[ f2 ];
end[ f3 ] = i2 ? min[ f3 ] : max[ f2 ];
count ++;
}
}
}
// check all the other boxes point
for ( let x = 0; x <= 1; x ++ ) {
for ( let y = 0; y <= 1; y ++ ) {
for ( let z = 0; z <= 1; z ++ ) {
point2.x = x ? max.x : min.x;
point2.y = y ? max.y : min.y;
point2.z = z ? max.z : min.z;
this.closestPointToPoint( point2, point1 );
const dist = point2.distanceToSquared( point1 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
}
}
for ( let i = 0; i < 12; i ++ ) {
const l1 = segments1[ i ];
for ( let i2 = 0; i2 < 12; i2 ++ ) {
const l2 = segments2[ i2 ];
closestPointsSegmentToSegment( l1, l2, point1, point2 );
const dist = point1.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
if ( dist < threshold2 ) return Math.sqrt( dist );
}
}
}
return Math.sqrt( closestDistanceSq );
};
} )();

View File

@@ -0,0 +1,95 @@
import { Vector3 } from 'three'
export class SeparatingAxisBounds {
constructor() {
this.min = Infinity
this.max = -Infinity
}
setFromPointsField(points, field) {
let min = Infinity
let max = -Infinity
for (let i = 0, l = points.length; i < l; i++) {
const p = points[ i ]
const val = p[ field ]
min = val < min ? val : min
max = val > max ? val : max
}
this.min = min
this.max = max
}
setFromPoints(axis, points) {
let min = Infinity
let max = -Infinity
for (let i = 0, l = points.length; i < l; i++) {
const p = points[ i ]
const val = axis.dot(p)
min = val < min ? val : min
max = val > max ? val : max
}
this.min = min
this.max = max
}
isSeparated(other) {
return this.min > other.max || other.min > this.max
}
}
SeparatingAxisBounds.prototype.setFromBox = (function() {
const p = new Vector3()
return function setFromBox(axis, box) {
const boxMin = box.min
const boxMax = box.max
let min = Infinity
let max = -Infinity
for (let x = 0; x <= 1; x++) {
for (let y = 0; y <= 1; y++) {
for (let z = 0; z <= 1; z++) {
p.x = boxMin.x * x + boxMax.x * (1 - x)
p.y = boxMin.y * y + boxMax.y * (1 - y)
p.z = boxMin.z * z + boxMax.z * (1 - z)
const val = axis.dot(p)
min = Math.min(val, min)
max = Math.max(val, max)
}
}
}
this.min = min
this.max = max
}
})()
export const areIntersecting = (function() {
const cacheSatBounds = new SeparatingAxisBounds()
return function areIntersecting(shape1, shape2) {
const points1 = shape1.points
const satAxes1 = shape1.satAxes
const satBounds1 = shape1.satBounds
const points2 = shape2.points
const satAxes2 = shape2.satAxes
const satBounds2 = shape2.satBounds
// check axes of the first shape
for (let i = 0; i < 3; i++) {
const sb = satBounds1[ i ]
const sa = satAxes1[ i ]
cacheSatBounds.setFromPoints(sa, points2)
if (sb.isSeparated(cacheSatBounds)) return false
}
// check axes of the second shape
for (let i = 0; i < 3; i++) {
const sb = satBounds2[ i ]
const sa = satAxes2[ i ]
cacheSatBounds.setFromPoints(sa, points1)
if (sb.isSeparated(cacheSatBounds)) return false
}
}
})()

View File

@@ -0,0 +1,306 @@
import { LineBasicMaterial, BufferAttribute, Box3, Group, MeshBasicMaterial, Object3D, BufferGeometry } from 'three'
import { arrayToBox } from '../utils/ArrayBoxUtilities.js'
import { MeshBVH } from '../core/MeshBVH.js'
const boundingBox = /* @__PURE__ */ new Box3()
class MeshBVHRootHelper extends Object3D {
get isMesh() {
return !this.displayEdges
}
get isLineSegments() {
return this.displayEdges
}
get isLine() {
return this.displayEdges
}
constructor(bvh, material, depth = 10, group = 0) {
super()
this.material = material
this.geometry = new BufferGeometry()
this.name = 'MeshBVHRootHelper'
this.depth = depth
this.displayParents = false
this.bvh = bvh
this.displayEdges = true
this._group = group
}
raycast() {}
update() {
const geometry = this.geometry
const boundsTree = this.bvh
const group = this._group
geometry.dispose()
this.visible = false
if (boundsTree) {
// count the number of bounds required
const targetDepth = this.depth - 1
const displayParents = this.displayParents
let boundsCount = 0
boundsTree.traverse((depth, isLeaf) => {
if (depth >= targetDepth || isLeaf) {
boundsCount++
return true
} else if (displayParents) {
boundsCount++
}
}, group)
// fill in the position buffer with the bounds corners
let posIndex = 0
const positionArray = new Float32Array(8 * 3 * boundsCount)
boundsTree.traverse((depth, isLeaf, boundingData) => {
const terminate = depth >= targetDepth || isLeaf
if (terminate || displayParents) {
arrayToBox(0, boundingData, boundingBox)
const { min, max } = boundingBox
for (let x = -1; x <= 1; x += 2) {
const xVal = x < 0 ? min.x : max.x
for (let y = -1; y <= 1; y += 2) {
const yVal = y < 0 ? min.y : max.y
for (let z = -1; z <= 1; z += 2) {
const zVal = z < 0 ? min.z : max.z
positionArray[ posIndex + 0 ] = xVal
positionArray[ posIndex + 1 ] = yVal
positionArray[ posIndex + 2 ] = zVal
posIndex += 3
}
}
}
return terminate
}
}, group)
let indexArray
let indices
if (this.displayEdges) {
// fill in the index buffer to point to the corner points
indices = new Uint8Array([
// x axis
0, 4,
1, 5,
2, 6,
3, 7,
// y axis
0, 2,
1, 3,
4, 6,
5, 7,
// z axis
0, 1,
2, 3,
4, 5,
6, 7
])
} else {
indices = new Uint8Array([
// X-, X+
0, 1, 2,
2, 1, 3,
4, 6, 5,
6, 7, 5,
// Y-, Y+
1, 4, 5,
0, 4, 1,
2, 3, 6,
3, 7, 6,
// Z-, Z+
0, 2, 4,
2, 6, 4,
1, 5, 3,
3, 5, 7
])
}
if (positionArray.length > 65535) {
indexArray = new Uint32Array(indices.length * boundsCount)
} else {
indexArray = new Uint16Array(indices.length * boundsCount)
}
const indexLength = indices.length
for (let i = 0; i < boundsCount; i++) {
const posOffset = i * 8
const indexOffset = i * indexLength
for (let j = 0; j < indexLength; j++) {
indexArray[ indexOffset + j ] = posOffset + indices[ j ]
}
}
// update the geometry
geometry.setIndex(
new BufferAttribute(indexArray, 1, false),
)
geometry.setAttribute(
'position',
new BufferAttribute(positionArray, 3, false),
)
this.visible = true
}
}
}
class MeshBVHHelper extends Group {
get color() {
return this.edgeMaterial.color
}
get opacity() {
return this.edgeMaterial.opacity
}
set opacity(v) {
this.edgeMaterial.opacity = v
this.meshMaterial.opacity = v
}
constructor(mesh = null, bvh = null, depth = 10) {
// handle bvh, depth signature
if (mesh instanceof MeshBVH) {
depth = bvh || 10
bvh = mesh
mesh = null
}
// handle mesh, depth signature
if (typeof bvh === 'number') {
depth = bvh
bvh = null
}
super()
this.name = 'MeshBVHHelper'
this.depth = depth
this.mesh = mesh
this.bvh = bvh
this.displayParents = false
this.displayEdges = true
this._roots = []
const edgeMaterial = new LineBasicMaterial({
color: 0x00FF88,
transparent: true,
opacity: 0.3,
depthWrite: false
})
const meshMaterial = new MeshBasicMaterial({
color: 0x00FF88,
transparent: true,
opacity: 0.3,
depthWrite: false
})
meshMaterial.color = edgeMaterial.color
this.edgeMaterial = edgeMaterial
this.meshMaterial = meshMaterial
this.update()
}
update() {
const bvh = this.bvh || this.mesh.geometry.boundsTree
const totalRoots = bvh ? bvh._roots.length : 0
while (this._roots.length > totalRoots) {
const root = this._roots.pop()
root.geometry.dispose()
this.remove(root)
}
for (let i = 0; i < totalRoots; i++) {
const { depth, edgeMaterial, meshMaterial, displayParents, displayEdges } = this
if (i >= this._roots.length) {
const root = new MeshBVHRootHelper(bvh, edgeMaterial, depth, i)
this.add(root)
this._roots.push(root)
}
const root = this._roots[ i ]
root.bvh = bvh
root.depth = depth
root.displayParents = displayParents
root.displayEdges = displayEdges
root.material = displayEdges ? edgeMaterial : meshMaterial
root.update()
}
}
updateMatrixWorld(...args) {
const mesh = this.mesh
const parent = this.parent
if (mesh !== null) {
mesh.updateWorldMatrix(true, false)
if (parent) {
this.matrix
.copy(parent.matrixWorld)
.invert()
.multiply(mesh.matrixWorld)
} else {
this.matrix
.copy(mesh.matrixWorld)
}
this.matrix.decompose(
this.position,
this.quaternion,
this.scale,
)
}
super.updateMatrixWorld(...args)
}
copy(source) {
this.depth = source.depth
this.mesh = source.mesh
this.bvh = source.bvh
this.opacity = source.opacity
this.color.copy(source.color)
}
clone() {
return new MeshBVHHelper(this.mesh, this.bvh, this.depth)
}
dispose() {
this.edgeMaterial.dispose()
this.meshMaterial.dispose()
const children = this.children
for (let i = 0, l = children.length; i < l; i++) {
children[ i ].geometry.dispose()
}
}
}
export class MeshBVHVisualizer extends MeshBVHHelper {
constructor(...args) {
super(...args)
console.warn('MeshBVHVisualizer: MeshBVHVisualizer has been deprecated. Use MeshBVHHelper, instead.')
}
}
export { MeshBVHHelper }

View File

@@ -0,0 +1,108 @@
export function arrayToBox( nodeIndex32, array, target ) {
target.min.x = array[ nodeIndex32 ];
target.min.y = array[ nodeIndex32 + 1 ];
target.min.z = array[ nodeIndex32 + 2 ];
target.max.x = array[ nodeIndex32 + 3 ];
target.max.y = array[ nodeIndex32 + 4 ];
target.max.z = array[ nodeIndex32 + 5 ];
return target;
}
export function makeEmptyBounds( target ) {
target[ 0 ] = target[ 1 ] = target[ 2 ] = Infinity;
target[ 3 ] = target[ 4 ] = target[ 5 ] = - Infinity;
}
export function getLongestEdgeIndex( bounds ) {
let splitDimIdx = - 1;
let splitDist = - Infinity;
for ( let i = 0; i < 3; i ++ ) {
const dist = bounds[ i + 3 ] - bounds[ i ];
if ( dist > splitDist ) {
splitDist = dist;
splitDimIdx = i;
}
}
return splitDimIdx;
}
// copies bounds a into bounds b
export function copyBounds( source, target ) {
target.set( source );
}
// sets bounds target to the union of bounds a and b
export function unionBounds( a, b, target ) {
let aVal, bVal;
for ( let d = 0; d < 3; d ++ ) {
const d3 = d + 3;
// set the minimum values
aVal = a[ d ];
bVal = b[ d ];
target[ d ] = aVal < bVal ? aVal : bVal;
// set the max values
aVal = a[ d3 ];
bVal = b[ d3 ];
target[ d3 ] = aVal > bVal ? aVal : bVal;
}
}
// expands the given bounds by the provided triangle bounds
export function expandByTriangleBounds( startIndex, triangleBounds, bounds ) {
for ( let d = 0; d < 3; d ++ ) {
const tCenter = triangleBounds[ startIndex + 2 * d ];
const tHalf = triangleBounds[ startIndex + 2 * d + 1 ];
const tMin = tCenter - tHalf;
const tMax = tCenter + tHalf;
if ( tMin < bounds[ d ] ) {
bounds[ d ] = tMin;
}
if ( tMax > bounds[ d + 3 ] ) {
bounds[ d + 3 ] = tMax;
}
}
}
// compute bounds surface area
export function computeSurfaceArea( bounds ) {
const d0 = bounds[ 3 ] - bounds[ 0 ];
const d1 = bounds[ 4 ] - bounds[ 1 ];
const d2 = bounds[ 5 ] - bounds[ 2 ];
return 2 * ( d0 * d1 + d1 * d2 + d2 * d0 );
}

View File

@@ -0,0 +1,41 @@
export function isSharedArrayBufferSupported() {
return typeof SharedArrayBuffer !== 'undefined';
}
export function convertToBufferType( array, BufferConstructor ) {
if ( array === null ) {
return array;
} else if ( array.buffer ) {
const buffer = array.buffer;
if ( buffer.constructor === BufferConstructor ) {
return array;
}
const ArrayConstructor = array.constructor;
const result = new ArrayConstructor( new BufferConstructor( buffer.byteLength ) );
result.set( array );
return result;
} else {
if ( array.constructor === BufferConstructor ) {
return array;
}
const result = new BufferConstructor( array.byteLength );
new Uint8Array( result ).set( new Uint8Array( array ) );
return result;
}
}

View File

@@ -0,0 +1,14 @@
import { ExtendedTriangle } from '../math/ExtendedTriangle.js';
import { PrimitivePool } from './PrimitivePool.js';
class ExtendedTrianglePoolBase extends PrimitivePool {
constructor() {
super( () => new ExtendedTriangle() );
}
}
export const ExtendedTrianglePool = /* @__PURE__ */ new ExtendedTrianglePoolBase();

View File

@@ -0,0 +1,43 @@
import { Ray, Matrix4, Mesh } from 'three'
import { convertRaycastIntersect } from './GeometryRayIntersectUtilities.js'
import { MeshBVH } from '../core/MeshBVH.js'
const ray = /* @__PURE__ */ new Ray()
const tmpInverseMatrix = /* @__PURE__ */ new Matrix4()
const origMeshRaycastFunc = Mesh.prototype.raycast
export function acceleratedRaycast(raycaster, intersects) {
if (this.geometry.boundsTree) {
if (this.material === undefined) return
tmpInverseMatrix.copy(this.matrixWorld).invert()
ray.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix)
const bvh = this.geometry.boundsTree
if (raycaster.firstHitOnly === true) {
const hit = convertRaycastIntersect(bvh.raycastFirst(ray, this.material), this, raycaster)
if (hit) {
intersects.push(hit)
}
} else {
const hits = bvh.raycast(ray, this.material)
for (let i = 0, l = hits.length; i < l; i++) {
const hit = convertRaycastIntersect(hits[ i ], this, raycaster)
if (hit) {
intersects.push(hit)
}
}
}
} else {
origMeshRaycastFunc.call(this, raycaster, intersects)
}
}
export function computeBoundsTree(options) {
this.boundsTree = new MeshBVH(this, options)
return this.boundsTree
}
export function disposeBoundsTree() {
this.boundsTree = null
}

View File

@@ -0,0 +1,25 @@
// converts the given BVH raycast intersection to align with the three.js raycast
// structure (include object, world space distance and point).
export function convertRaycastIntersect( hit, object, raycaster ) {
if ( hit === null ) {
return null;
}
hit.point.applyMatrix4( object.matrixWorld );
hit.distance = hit.point.distanceTo( raycaster.ray.origin );
hit.object = object;
if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) {
return null;
} else {
return hit;
}
}

View File

@@ -0,0 +1,31 @@
export class PrimitivePool {
constructor( getNewPrimitive ) {
this._getNewPrimitive = getNewPrimitive;
this._primitives = [];
}
getPrimitive() {
const primitives = this._primitives;
if ( primitives.length === 0 ) {
return this._getNewPrimitive();
} else {
return primitives.pop();
}
}
releasePrimitive( primitive ) {
this._primitives.push( primitive );
}
}

View File

@@ -0,0 +1,555 @@
import { BufferAttribute, BufferGeometry, Vector3, Vector4, Matrix4, Matrix3 } from 'three'
const _positionVector = /* @__PURE__*/ new Vector3()
const _normalVector = /* @__PURE__*/ new Vector3()
const _tangentVector = /* @__PURE__*/ new Vector3()
const _tangentVector4 = /* @__PURE__*/ new Vector4()
const _morphVector = /* @__PURE__*/ new Vector3()
const _temp = /* @__PURE__*/ new Vector3()
const _skinIndex = /* @__PURE__*/ new Vector4()
const _skinWeight = /* @__PURE__*/ new Vector4()
const _matrix = /* @__PURE__*/ new Matrix4()
const _boneMatrix = /* @__PURE__*/ new Matrix4()
// Confirms that the two provided attributes are compatible
function validateAttributes(attr1, attr2) {
if (!attr1 && !attr2) {
return
}
const sameCount = attr1.count === attr2.count
const sameNormalized = attr1.normalized === attr2.normalized
const sameType = attr1.array.constructor === attr2.array.constructor
const sameItemSize = attr1.itemSize === attr2.itemSize
if (!sameCount || !sameNormalized || !sameType || !sameItemSize) {
throw new Error()
}
}
// Clones the given attribute with a new compatible buffer attribute but no data
function createAttributeClone(attr, countOverride = null) {
const cons = attr.array.constructor
const normalized = attr.normalized
const itemSize = attr.itemSize
const count = countOverride === null ? attr.count : countOverride
return new BufferAttribute(new cons(itemSize * count), itemSize, normalized)
}
// target offset is the number of elements in the target buffer stride to skip before copying the
// attributes contents in to.
function copyAttributeContents(attr, target, targetOffset = 0) {
if (attr.isInterleavedBufferAttribute) {
const itemSize = attr.itemSize
for (let i = 0, l = attr.count; i < l; i++) {
const io = i + targetOffset
target.setX(io, attr.getX(i))
if (itemSize >= 2) target.setY(io, attr.getY(i))
if (itemSize >= 3) target.setZ(io, attr.getZ(i))
if (itemSize >= 4) target.setW(io, attr.getW(i))
}
} else {
const array = target.array
const cons = array.constructor
const byteOffset = array.BYTES_PER_ELEMENT * attr.itemSize * targetOffset
const temp = new cons(array.buffer, byteOffset, attr.array.length)
temp.set(attr.array)
}
}
// Adds the "matrix" multiplied by "scale" to "target"
function addScaledMatrix(target, matrix, scale) {
const targetArray = target.elements
const matrixArray = matrix.elements
for (let i = 0, l = matrixArray.length; i < l; i++) {
targetArray[ i ] += matrixArray[ i ] * scale
}
}
// A version of "SkinnedMesh.boneTransform" for normals
function boneNormalTransform(mesh, index, target) {
const skeleton = mesh.skeleton
const geometry = mesh.geometry
const bones = skeleton.bones
const boneInverses = skeleton.boneInverses
_skinIndex.fromBufferAttribute(geometry.attributes.skinIndex, index)
_skinWeight.fromBufferAttribute(geometry.attributes.skinWeight, index)
_matrix.elements.fill(0)
for (let i = 0; i < 4; i++) {
const weight = _skinWeight.getComponent(i)
if (weight !== 0) {
const boneIndex = _skinIndex.getComponent(i)
_boneMatrix.multiplyMatrices(bones[ boneIndex ].matrixWorld, boneInverses[ boneIndex ])
addScaledMatrix(_matrix, _boneMatrix, weight)
}
}
_matrix.multiply(mesh.bindMatrix).premultiply(mesh.bindMatrixInverse)
target.transformDirection(_matrix)
return target
}
// Applies the morph target data to the target vector
function applyMorphTarget(morphData, morphInfluences, morphTargetsRelative, i, target) {
_morphVector.set(0, 0, 0)
for (let j = 0, jl = morphData.length; j < jl; j++) {
const influence = morphInfluences[ j ]
const morphAttribute = morphData[ j ]
if (influence === 0) continue
_temp.fromBufferAttribute(morphAttribute, i)
if (morphTargetsRelative) {
_morphVector.addScaledVector(_temp, influence)
} else {
_morphVector.addScaledVector(_temp.sub(target), influence)
}
}
target.add(_morphVector)
}
// Modified version of BufferGeometryUtils.mergeBufferGeometries that ignores morph targets and updates a attributes in place
function mergeBufferGeometries(geometries, options = { useGroups: false, updateIndex: false, skipAttributes: [] }, targetGeometry = new BufferGeometry()) {
const isIndexed = geometries[ 0 ].index !== null
const { useGroups = false, updateIndex = false, skipAttributes = [] } = options
const attributesUsed = new Set(Object.keys(geometries[ 0 ].attributes))
const attributes = {}
let offset = 0
targetGeometry.clearGroups()
for (let i = 0; i < geometries.length; ++i) {
const geometry = geometries[ i ]
let attributesCount = 0
// ensure that all geometries are indexed, or none
if (isIndexed !== (geometry.index !== null)) {
throw new Error('StaticGeometryGenerator: All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.')
}
// gather attributes, exit early if they're different
for (const name in geometry.attributes) {
if (!attributesUsed.has(name)) {
throw new Error('StaticGeometryGenerator: All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.')
}
if (attributes[ name ] === undefined) {
attributes[ name ] = []
}
attributes[ name ].push(geometry.attributes[ name ])
attributesCount++
}
// ensure geometries have the same number of attributes
if (attributesCount !== attributesUsed.size) {
throw new Error('StaticGeometryGenerator: Make sure all geometries have the same number of attributes.')
}
if (useGroups) {
let count
if (isIndexed) {
count = geometry.index.count
} else if (geometry.attributes.position !== undefined) {
count = geometry.attributes.position.count
} else {
throw new Error('StaticGeometryGenerator: The geometry must have either an index or a position attribute')
}
targetGeometry.addGroup(offset, count, i)
offset += count
}
}
// merge indices
if (isIndexed) {
let forceUpdateIndex = false
if (!targetGeometry.index) {
let indexCount = 0
for (let i = 0; i < geometries.length; ++i) {
indexCount += geometries[ i ].index.count
}
targetGeometry.setIndex(new BufferAttribute(new Uint32Array(indexCount), 1, false))
forceUpdateIndex = true
}
if (updateIndex || forceUpdateIndex) {
const targetIndex = targetGeometry.index
let targetOffset = 0
let indexOffset = 0
for (let i = 0; i < geometries.length; ++i) {
const geometry = geometries[ i ]
const index = geometry.index
if (skipAttributes[ i ] !== true) {
for (let j = 0; j < index.count; ++j) {
targetIndex.setX(targetOffset, index.getX(j) + indexOffset)
targetOffset++
}
}
indexOffset += geometry.attributes.position.count
}
}
}
// merge attributes
for (const name in attributes) {
const attrList = attributes[ name ]
if (!(name in targetGeometry.attributes)) {
let count = 0
for (const key in attrList) {
count += attrList[ key ].count
}
targetGeometry.setAttribute(name, createAttributeClone(attributes[ name ][ 0 ], count))
}
const targetAttribute = targetGeometry.attributes[ name ]
let offset = 0
for (let i = 0, l = attrList.length; i < l; i++) {
const attr = attrList[ i ]
if (skipAttributes[ i ] !== true) {
copyAttributeContents(attr, targetAttribute, offset)
}
offset += attr.count
}
}
return targetGeometry
}
function checkTypedArrayEquality(a, b) {
if (a === null || b === null) {
return a === b
}
if (a.length !== b.length) {
return false
}
for (let i = 0, l = a.length; i < l; i++) {
if (a[ i ] !== b[ i ]) {
return false
}
}
return true
}
function invertGeometry(geometry) {
const { index, attributes } = geometry
if (index) {
for (let i = 0, l = index.count; i < l; i += 3) {
const v0 = index.getX(i)
const v2 = index.getX(i + 2)
index.setX(i, v2)
index.setX(i + 2, v0)
}
} else {
for (const key in attributes) {
const attr = attributes[ key ]
const itemSize = attr.itemSize
for (let i = 0, l = attr.count; i < l; i += 3) {
for (let j = 0; j < itemSize; j++) {
const v0 = attr.getComponent(i, j)
const v2 = attr.getComponent(i + 2, j)
attr.setComponent(i, j, v2)
attr.setComponent(i + 2, j, v0)
}
}
}
}
return geometry
}
// Checks whether the geometry changed between this and last evaluation
class GeometryDiff {
constructor(mesh) {
this.matrixWorld = new Matrix4()
this.geometryHash = null
this.boneMatrices = null
this.primitiveCount = -1
this.mesh = mesh
this.update()
}
update() {
const mesh = this.mesh
const geometry = mesh.geometry
const skeleton = mesh.skeleton
const primitiveCount = (geometry.index ? geometry.index.count : geometry.attributes.position.count) / 3
this.matrixWorld.copy(mesh.matrixWorld)
this.geometryHash = geometry.attributes.position.version
this.primitiveCount = primitiveCount
if (skeleton) {
// ensure the bone matrix array is updated to the appropriate length
if (!skeleton.boneTexture) {
skeleton.computeBoneTexture()
}
skeleton.update()
// copy data if possible otherwise clone it
const boneMatrices = skeleton.boneMatrices
if (!this.boneMatrices || this.boneMatrices.length !== boneMatrices.length) {
this.boneMatrices = boneMatrices.slice()
} else {
this.boneMatrices.set(boneMatrices)
}
} else {
this.boneMatrices = null
}
}
didChange() {
const mesh = this.mesh
const geometry = mesh.geometry
const primitiveCount = (geometry.index ? geometry.index.count : geometry.attributes.position.count) / 3
const identical =
this.matrixWorld.equals(mesh.matrixWorld) &&
this.geometryHash === geometry.attributes.position.version &&
checkTypedArrayEquality(mesh.skeleton && mesh.skeleton.boneMatrices || null, this.boneMatrices) &&
this.primitiveCount === primitiveCount
return !identical
}
}
export class StaticGeometryGenerator {
constructor(meshes) {
if (!Array.isArray(meshes)) {
meshes = [meshes]
}
const finalMeshes = []
meshes.forEach(object => {
object.traverseVisible(c => {
if (c.isMesh) {
finalMeshes.push(c)
}
})
})
this.meshes = finalMeshes
this.useGroups = true
this.applyWorldTransforms = true
this.attributes = ['position', 'normal', 'color', 'tangent', 'uv', 'uv2']
this._intermediateGeometry = new Array(finalMeshes.length).fill().map(() => new BufferGeometry())
this._diffMap = new WeakMap()
}
getMaterials() {
const materials = []
this.meshes.forEach(mesh => {
if (Array.isArray(mesh.material)) {
materials.push(...mesh.material)
} else {
materials.push(mesh.material)
}
})
return materials
}
generate(targetGeometry = new BufferGeometry()) {
// track which attributes have been updated and which to skip to avoid unnecessary attribute copies
const skipAttributes = []
const { meshes, useGroups, _intermediateGeometry, _diffMap } = this
for (let i = 0, l = meshes.length; i < l; i++) {
const mesh = meshes[ i ]
const geom = _intermediateGeometry[ i ]
const diff = _diffMap.get(mesh)
if (!diff || diff.didChange(mesh)) {
this._convertToStaticGeometry(mesh, geom)
skipAttributes.push(false)
if (!diff) {
_diffMap.set(mesh, new GeometryDiff(mesh))
} else {
diff.update()
}
} else {
skipAttributes.push(true)
}
}
if (_intermediateGeometry.length === 0) {
// if there are no geometries then just create a fake empty geometry to provide
targetGeometry.setIndex(null)
// remove all geometry
const attrs = targetGeometry.attributes
for (const key in attrs) {
targetGeometry.deleteAttribute(key)
}
// create dummy attributes
for (const key in this.attributes) {
targetGeometry.setAttribute(this.attributes[ key ], new BufferAttribute(new Float32Array(0), 4, false))
}
} else {
mergeBufferGeometries(_intermediateGeometry, { useGroups, skipAttributes }, targetGeometry)
}
for (const key in targetGeometry.attributes) {
targetGeometry.attributes[ key ].needsUpdate = true
}
return targetGeometry
}
_convertToStaticGeometry(mesh, targetGeometry = new BufferGeometry()) {
const geometry = mesh.geometry
const applyWorldTransforms = this.applyWorldTransforms
const includeNormal = this.attributes.includes('normal')
const includeTangent = this.attributes.includes('tangent')
const attributes = geometry.attributes
const targetAttributes = targetGeometry.attributes
// initialize the attributes if they don't exist
if (!targetGeometry.index && geometry.index) {
targetGeometry.index = geometry.index.clone()
}
if (!targetAttributes.position) {
targetGeometry.setAttribute('position', createAttributeClone(attributes.position))
}
if (includeNormal && !targetAttributes.normal && attributes.normal) {
targetGeometry.setAttribute('normal', createAttributeClone(attributes.normal))
}
if (includeTangent && !targetAttributes.tangent && attributes.tangent) {
targetGeometry.setAttribute('tangent', createAttributeClone(attributes.tangent))
}
// ensure the attributes are consistent
validateAttributes(geometry.index, targetGeometry.index)
validateAttributes(attributes.position, targetAttributes.position)
if (includeNormal) {
validateAttributes(attributes.normal, targetAttributes.normal)
}
if (includeTangent) {
validateAttributes(attributes.tangent, targetAttributes.tangent)
}
// generate transformed vertex attribute data
const position = attributes.position
const normal = includeNormal ? attributes.normal : null
const tangent = includeTangent ? attributes.tangent : null
const morphPosition = geometry.morphAttributes.position
const morphNormal = geometry.morphAttributes.normal
const morphTangent = geometry.morphAttributes.tangent
const morphTargetsRelative = geometry.morphTargetsRelative
const morphInfluences = mesh.morphTargetInfluences
const normalMatrix = new Matrix3()
normalMatrix.getNormalMatrix(mesh.matrixWorld)
// copy the index
if (geometry.index) {
targetGeometry.index.array.set(geometry.index.array)
}
// copy and apply other attributes
for (let i = 0, l = attributes.position.count; i < l; i++) {
_positionVector.fromBufferAttribute(position, i)
if (normal) {
_normalVector.fromBufferAttribute(normal, i)
}
if (tangent) {
_tangentVector4.fromBufferAttribute(tangent, i)
_tangentVector.fromBufferAttribute(tangent, i)
}
// apply morph target transform
if (morphInfluences) {
if (morphPosition) {
applyMorphTarget(morphPosition, morphInfluences, morphTargetsRelative, i, _positionVector)
}
if (morphNormal) {
applyMorphTarget(morphNormal, morphInfluences, morphTargetsRelative, i, _normalVector)
}
if (morphTangent) {
applyMorphTarget(morphTangent, morphInfluences, morphTargetsRelative, i, _tangentVector)
}
}
// apply bone transform
if (mesh.isSkinnedMesh) {
mesh.applyBoneTransform(i, _positionVector)
if (normal) {
boneNormalTransform(mesh, i, _normalVector)
}
if (tangent) {
boneNormalTransform(mesh, i, _tangentVector)
}
}
// update the vectors of the attributes
if (applyWorldTransforms) {
_positionVector.applyMatrix4(mesh.matrixWorld)
}
targetAttributes.position.setXYZ(i, _positionVector.x, _positionVector.y, _positionVector.z)
if (normal) {
if (applyWorldTransforms) {
_normalVector.applyNormalMatrix(normalMatrix)
}
targetAttributes.normal.setXYZ(i, _normalVector.x, _normalVector.y, _normalVector.z)
}
if (tangent) {
if (applyWorldTransforms) {
_tangentVector.transformDirection(mesh.matrixWorld)
}
targetAttributes.tangent.setXYZW(i, _tangentVector.x, _tangentVector.y, _tangentVector.z, _tangentVector4.w)
}
}
// copy other attributes over
for (const i in this.attributes) {
const key = this.attributes[ i ]
if (key === 'position' || key === 'tangent' || key === 'normal' || !(key in attributes)) {
continue
}
if (!targetAttributes[ key ]) {
targetGeometry.setAttribute(key, createAttributeClone(attributes[ key ]))
}
validateAttributes(attributes[ key ], targetAttributes[ key ])
copyAttributeContents(attributes[ key ], targetAttributes[ key ])
}
if (mesh.matrixWorld.determinant() < 0) {
invertGeometry(targetGeometry)
}
return targetGeometry
}
}

View File

@@ -0,0 +1,116 @@
import { Vector3, Vector2, Triangle, DoubleSide, BackSide } from 'three'
// Ripped and modified From THREE.js Mesh raycast
// https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L115
const _vA = /* @__PURE__ */ new Vector3()
const _vB = /* @__PURE__ */ new Vector3()
const _vC = /* @__PURE__ */ new Vector3()
const _uvA = /* @__PURE__ */ new Vector2()
const _uvB = /* @__PURE__ */ new Vector2()
const _uvC = /* @__PURE__ */ new Vector2()
const _normalA = /* @__PURE__ */ new Vector3()
const _normalB = /* @__PURE__ */ new Vector3()
const _normalC = /* @__PURE__ */ new Vector3()
const _intersectionPoint = /* @__PURE__ */ new Vector3()
function checkIntersection(ray, pA, pB, pC, point, side) {
let intersect
if (side === BackSide) {
intersect = ray.intersectTriangle(pC, pB, pA, true, point)
} else {
intersect = ray.intersectTriangle(pA, pB, pC, side !== DoubleSide, point)
}
if (intersect === null) return null
const distance = ray.origin.distanceTo(point)
return {
distance: distance,
point: point.clone()
}
}
function checkBufferGeometryIntersection(ray, position, normal, uv, uv1, a, b, c, side) {
_vA.fromBufferAttribute(position, a)
_vB.fromBufferAttribute(position, b)
_vC.fromBufferAttribute(position, c)
const intersection = checkIntersection(ray, _vA, _vB, _vC, _intersectionPoint, side)
if (intersection) {
if (uv) {
_uvA.fromBufferAttribute(uv, a)
_uvB.fromBufferAttribute(uv, b)
_uvC.fromBufferAttribute(uv, c)
intersection.uv = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2())
}
if (uv1) {
_uvA.fromBufferAttribute(uv1, a)
_uvB.fromBufferAttribute(uv1, b)
_uvC.fromBufferAttribute(uv1, c)
intersection.uv1 = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2())
}
if (normal) {
_normalA.fromBufferAttribute(normal, a)
_normalB.fromBufferAttribute(normal, b)
_normalC.fromBufferAttribute(normal, c)
intersection.normal = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _normalA, _normalB, _normalC, new Vector3())
if (intersection.normal.dot(ray.direction) > 0) {
intersection.normal.multiplyScalar(-1)
}
}
const face = {
a: a,
b: b,
c: c,
normal: new Vector3(),
materialIndex: 0
}
Triangle.getNormal(_vA, _vB, _vC, face.normal)
intersection.face = face
intersection.faceIndex = a
}
return intersection
}
// https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L258
function intersectTri(geo, side, ray, tri, intersections) {
const triOffset = tri * 3
let a = triOffset + 0
let b = triOffset + 1
let c = triOffset + 2
const index = geo.index
if (geo.index) {
a = index.getX(a)
b = index.getX(b)
c = index.getX(c)
}
const { position, normal, uv, uv1 } = geo.attributes
const intersection = checkBufferGeometryIntersection(ray, position, normal, uv, uv1, a, b, c, side)
if (intersection) {
intersection.faceIndex = tri
if (intersections) intersections.push(intersection)
return intersection
}
return null
}
export { intersectTri }

View File

@@ -0,0 +1,103 @@
import { Vector2, Vector3, Triangle } from 'three'
// sets the vertices of triangle `tri` with the 3 vertices after i
export function setTriangle(tri, i, index, pos) {
const ta = tri.a
const tb = tri.b
const tc = tri.c
let i0 = i
let i1 = i + 1
let i2 = i + 2
if (index) {
i0 = index.getX(i0)
i1 = index.getX(i1)
i2 = index.getX(i2)
}
ta.x = pos.getX(i0)
ta.y = pos.getY(i0)
ta.z = pos.getZ(i0)
tb.x = pos.getX(i1)
tb.y = pos.getY(i1)
tb.z = pos.getZ(i1)
tc.x = pos.getX(i2)
tc.y = pos.getY(i2)
tc.z = pos.getZ(i2)
}
const tempV1 = /* @__PURE__ */ new Vector3()
const tempV2 = /* @__PURE__ */ new Vector3()
const tempV3 = /* @__PURE__ */ new Vector3()
const tempUV1 = /* @__PURE__ */ new Vector2()
const tempUV2 = /* @__PURE__ */ new Vector2()
const tempUV3 = /* @__PURE__ */ new Vector2()
export function getTriangleHitPointInfo(point, geometry, triangleIndex, target) {
const indices = geometry.getIndex().array
const positions = geometry.getAttribute('position')
const uvs = geometry.getAttribute('uv')
const a = indices[ triangleIndex * 3 ]
const b = indices[ triangleIndex * 3 + 1 ]
const c = indices[ triangleIndex * 3 + 2 ]
tempV1.fromBufferAttribute(positions, a)
tempV2.fromBufferAttribute(positions, b)
tempV3.fromBufferAttribute(positions, c)
// find the associated material index
let materialIndex = 0
const groups = geometry.groups
const firstVertexIndex = triangleIndex * 3
for (let i = 0, l = groups.length; i < l; i++) {
const group = groups[ i ]
const { start, count } = group
if (firstVertexIndex >= start && firstVertexIndex < start + count) {
materialIndex = group.materialIndex
break
}
}
// extract uvs
let uv = null
if (uvs) {
tempUV1.fromBufferAttribute(uvs, a)
tempUV2.fromBufferAttribute(uvs, b)
tempUV3.fromBufferAttribute(uvs, c)
if (target && target.uv) uv = target.uv
else uv = new Vector2()
Triangle.getInterpolation(point, tempV1, tempV2, tempV3, tempUV1, tempUV2, tempUV3, uv)
}
// adjust the provided target or create a new one
if (target) {
if (!target.face) target.face = { }
target.face.a = a
target.face.b = b
target.face.c = c
target.face.materialIndex = materialIndex
if (!target.face.normal) target.face.normal = new Vector3()
Triangle.getNormal(tempV1, tempV2, tempV3, target.face.normal)
if (uv) target.uv = uv
return target
} else {
return {
face: {
a: a,
b: b,
c: c,
materialIndex: materialIndex,
normal: Triangle.getNormal(tempV1, tempV2, tempV3, new Vector3())
},
uv: uv
}
}
}

View File

@@ -0,0 +1,88 @@
import { Box3, BufferAttribute } from 'three'
import { MeshBVH } from '../core/MeshBVH.js'
import { WorkerBase } from './utils/WorkerBase.js'
export class GenerateMeshBVHWorker extends WorkerBase {
constructor() {
const worker = new Worker(new URL('./generateMeshBVH.worker.js', import.meta.url), { type: 'module' })
super(worker)
this.name = 'GenerateMeshBVHWorker'
}
runTask(worker, geometry, options = {}) {
return new Promise((resolve, reject) => {
if (
geometry.getAttribute('position').isInterleavedBufferAttribute ||
geometry.index && geometry.index.isInterleavedBufferAttribute
) {
throw new Error('GenerateMeshBVHWorker: InterleavedBufferAttribute are not supported for the geometry attributes.')
}
worker.onerror = e => {
reject(new Error(`GenerateMeshBVHWorker: ${e.message}`))
}
worker.onmessage = e => {
const { data } = e
if (data.error) {
reject(new Error(data.error))
worker.onmessage = null
} else if (data.serialized) {
const { serialized, position } = data
const bvh = MeshBVH.deserialize(serialized, geometry, { setIndex: false })
const boundsOptions = Object.assign({
setBoundingBox: true
}, options)
// we need to replace the arrays because they're neutered entirely by the
// webworker transfer.
geometry.attributes.position.array = position
if (serialized.index) {
if (geometry.index) {
geometry.index.array = serialized.index
} else {
const newIndex = new BufferAttribute(serialized.index, 1, false)
geometry.setIndex(newIndex)
}
}
if (boundsOptions.setBoundingBox) {
geometry.boundingBox = bvh.getBoundingBox(new Box3())
}
if (options.onProgress) {
options.onProgress(data.progress)
}
resolve(bvh)
worker.onmessage = null
} else if (options.onProgress) {
options.onProgress(data.progress)
}
}
const index = geometry.index ? geometry.index.array : null
const position = geometry.attributes.position.array
const transferable = [position]
if (index) {
transferable.push(index)
}
worker.postMessage({
index,
position,
options: {
...options,
onProgress: null,
includedProgressCallback: Boolean(options.onProgress),
groups: [... geometry.groups]
}
}, transferable.map(arr => arr.buffer).filter(v => (typeof SharedArrayBuffer === 'undefined') || !(v instanceof SharedArrayBuffer)))
})
}
}

View File

@@ -0,0 +1,112 @@
import { Box3, BufferAttribute } from 'three'
import { MeshBVH } from '../core/MeshBVH.js'
import { WorkerBase } from './utils/WorkerBase.js'
import { convertToBufferType, isSharedArrayBufferSupported } from '../utils/BufferUtils.js'
import { GenerateMeshBVHWorker } from './GenerateMeshBVHWorker.js'
import { ensureIndex } from '../core/build/geometryUtils.js'
const DEFAULT_WORKER_COUNT = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : 4
class _ParallelMeshBVHWorker extends WorkerBase {
constructor() {
const worker = new Worker(new URL('./parallelMeshBVH.worker.js', import.meta.url), { type: 'module' })
super(worker)
this.name = 'ParallelMeshBVHWorker'
this.maxWorkerCount = Math.max(DEFAULT_WORKER_COUNT, 4)
if (!isSharedArrayBufferSupported()) {
throw new Error('ParallelMeshBVHWorker: Shared Array Buffers are not supported.')
}
}
runTask(worker, geometry, options = {}) {
return new Promise((resolve, reject) => {
if (!geometry.index && !options.indirect) {
ensureIndex(geometry, options)
}
if (
geometry.getAttribute('position').isInterleavedBufferAttribute ||
geometry.index && geometry.index.isInterleavedBufferAttribute
) {
throw new Error('ParallelMeshBVHWorker: InterleavedBufferAttribute are not supported for the geometry attributes.')
}
worker.onerror = e => {
reject(new Error(`ParallelMeshBVHWorker: ${e.message}`))
}
worker.onmessage = e => {
const { data } = e
if (data.error) {
reject(new Error(data.error))
worker.onmessage = null
} else if (data.serialized) {
const { serialized, position } = data
const bvh = MeshBVH.deserialize(serialized, geometry, { setIndex: false })
const boundsOptions = {
setBoundingBox: true,
...options
}
// we need to replace the arrays because they're neutered entirely by the
// webworker transfer.
geometry.attributes.position.array = position
if (serialized.index) {
if (geometry.index) {
geometry.index.array = serialized.index
} else {
const newIndex = new BufferAttribute(serialized.index, 1, false)
geometry.setIndex(newIndex)
}
}
if (boundsOptions.setBoundingBox) {
geometry.boundingBox = bvh.getBoundingBox(new Box3())
}
if (options.onProgress) {
options.onProgress(data.progress)
}
resolve(bvh)
worker.onmessage = null
} else if (options.onProgress) {
options.onProgress(data.progress)
}
}
const index = geometry.index ? geometry.index.array : null
const position = geometry.attributes.position.array
worker.postMessage({
operation: 'BUILD_BVH',
maxWorkerCount: this.maxWorkerCount,
index: convertToBufferType(index, SharedArrayBuffer),
position: convertToBufferType(position, SharedArrayBuffer),
options: {
...options,
onProgress: null,
includedProgressCallback: Boolean(options.onProgress),
groups: [... geometry.groups]
}
})
})
}
}
export class ParallelMeshBVHWorker {
constructor() {
if (isSharedArrayBufferSupported()) {
return new _ParallelMeshBVHWorker()
} else {
console.warn('ParallelMeshBVHWorker: SharedArrayBuffers not supported. Falling back to single-threaded GenerateMeshBVHWorker.')
const object = new GenerateMeshBVHWorker()
object.maxWorkerCount = DEFAULT_WORKER_COUNT
return object
}
}
}

View File

@@ -0,0 +1,78 @@
import {
BufferGeometry,
BufferAttribute
} from 'three'
import { MeshBVH } from '../core/MeshBVH.js'
onmessage = ({ data }) => {
let prevTime = performance.now()
function onProgressCallback(progress) {
// account for error
progress = Math.min(progress, 1)
const currTime = performance.now()
if (currTime - prevTime >= 10 && progress !== 1.0) {
postMessage({
error: null,
serialized: null,
position: null,
progress
})
prevTime = currTime
}
}
const { index, position, options } = data
try {
const geometry = new BufferGeometry()
geometry.setAttribute('position', new BufferAttribute(position, 3, false))
if (index) {
geometry.setIndex(new BufferAttribute(index, 1, false))
}
if (options.includedProgressCallback) {
options.onProgress = onProgressCallback
}
if (options.groups) {
const groups = options.groups
for (const i in groups) {
const group = groups[ i ]
geometry.addGroup(group.start, group.count, group.materialIndex)
}
}
const bvh = new MeshBVH(geometry, options)
const serialized = MeshBVH.serialize(bvh, { copyIndexBuffer: false })
let toTransfer = [position.buffer, ...serialized.roots]
if (serialized.index) {
toTransfer.push(serialized.index.buffer)
}
toTransfer = toTransfer.filter(v => (typeof SharedArrayBuffer === 'undefined') || !(v instanceof SharedArrayBuffer))
if (bvh._indirectBuffer) {
toTransfer.push(serialized.indirectBuffer.buffer)
}
postMessage({
error: null,
serialized,
position,
progress: 1
}, toTransfer)
} catch (error) {
postMessage({
error,
serialized: null,
position: null,
progress: 1
})
}
}

View File

@@ -0,0 +1,260 @@
import { MathUtils, BufferGeometry, BufferAttribute } from 'three'
import { WorkerPool } from './utils/WorkerPool.js'
import { BYTES_PER_NODE } from '../core/Constants.js'
import { buildTree, generateIndirectBuffer } from '../core/build/buildTree.js'
import { countNodes, populateBuffer } from '../core/build/buildUtils.js'
import { computeTriangleBounds } from '../core/build/computeBoundsUtils.js'
import { getFullGeometryRange, getRootIndexRanges, getTriCount } from '../core/build/geometryUtils.js'
import { DEFAULT_OPTIONS } from '../core/MeshBVH.js'
let isRunning = false
let prevTime = 0
const workerPool = new WorkerPool(() => new Worker(new URL('./parallelMeshBVH.worker.js', import.meta.url), { type: 'module' }))
onmessage = async({ data }) => {
if (isRunning) {
throw new Error('Worker is already running a task.')
}
const { operation } = data
if (operation === 'BUILD_BVH') {
isRunning = true
const {
maxWorkerCount,
index,
position,
options
} = data
// initialize the number of workers balanced for a binary tree
workerPool.setWorkerCount(MathUtils.floorPowerOfTwo(maxWorkerCount))
// generate necessary buffers and objects
const geometry = getGeometry(index, position)
const geometryRanges = options.indirect ? getFullGeometryRange(geometry) : getRootIndexRanges(geometry)
const indirectBuffer = options.indirect ? generateIndirectBuffer(geometry, true) : null
const triCount = getTriCount(geometry)
const triangleBounds = new Float32Array(new SharedArrayBuffer(triCount * 6 * 4))
// generate portions of the triangle bounds buffer over multiple frames
const boundsPromises = []
for (let i = 0, l = workerPool.workerCount; i < l; i++) {
const countPerWorker = Math.ceil(triCount / l)
const offset = i * countPerWorker
const count = Math.min(countPerWorker, triCount - offset)
boundsPromises.push(workerPool.runSubTask(
i,
{
operation: 'BUILD_TRIANGLE_BOUNDS',
offset,
count,
index,
position,
triangleBounds
}
))
}
await Promise.all(boundsPromises)
// create a proxy bvh structure
const proxyBvh = {
_indirectBuffer: indirectBuffer,
geometry: geometry
}
let totalProgress = 0
const localOptions = {
...DEFAULT_OPTIONS,
...options,
verbose: false,
maxDepth: Math.round(Math.log2(workerPool.workerCount)),
onProgress: options.includedProgressCallback
? getOnProgressDeltaCallback(delta => {
totalProgress += 0.1 * delta
triggerOnProgress(totalProgress)
})
: null
}
// generate the ranges for all roots asynchronously
const packedRoots = []
for (let i = 0, l = geometryRanges.length; i < l; i++) {
// build the tree down to the necessary depth
const promises = []
const range = geometryRanges[ i ]
const root = buildTree(proxyBvh, triangleBounds, range.offset, range.count, localOptions)
const flatNodes = flattenNodes(root)
let bufferLengths = 0
let remainingNodes = 0
let nextWorker = 0
// trigger workers for each generated leaf node
for (let j = 0, l = flatNodes.length; j < l; j++) {
const node = flatNodes[ j ]
const isLeaf = Boolean(node.count)
if (isLeaf) {
// adjust the maxDepth to account for the depth we've already traversed
const workerOptions = {
...DEFAULT_OPTIONS,
...options
}
workerOptions.maxDepth = workerOptions.maxDepth - node.depth
const pr = workerPool.runSubTask(
nextWorker++,
{
operation: 'BUILD_SUBTREE',
offset: node.offset,
count: node.count,
indirectBuffer,
index,
position,
triangleBounds,
options: workerOptions
},
getOnProgressDeltaCallback(delta => {
totalProgress += 0.9 * delta / nextWorker
triggerOnProgress(totalProgress)
}),
).then(data => {
const buffer = data.buffer
node.buffer = buffer
bufferLengths += buffer.byteLength
})
promises.push(pr)
} else {
remainingNodes++
}
}
// wait for the sub trees to complete
await Promise.all(promises)
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer
const buffer = new BufferConstructor(bufferLengths + remainingNodes * BYTES_PER_NODE)
populateBuffer(0, root, buffer)
packedRoots.push(buffer)
}
// transfer the data back
postMessage({
error: null,
serialized: {
roots: packedRoots,
index: index,
indirectBuffer: indirectBuffer
},
position,
progress: 1
})
isRunning = false
} else if (operation === 'BUILD_SUBTREE') {
const {
offset,
count,
indirectBuffer,
index,
position,
triangleBounds,
options
} = data
const proxyBvh = {
_indirectBuffer: indirectBuffer,
geometry: getGeometry(index, position)
}
const localOptions = {
...DEFAULT_OPTIONS,
...options,
onProgress: options.includedProgressCallback ? triggerOnProgress : null
}
const root = buildTree(proxyBvh, triangleBounds, offset, count, localOptions)
const nodeCount = countNodes(root)
const buffer = new ArrayBuffer(BYTES_PER_NODE * nodeCount)
populateBuffer(0, root, buffer)
postMessage({ type: 'result', buffer, progress: 1 }, [buffer])
} else if (operation === 'BUILD_TRIANGLE_BOUNDS') {
const {
index,
position,
triangleBounds,
offset,
count
} = data
const geometry = getGeometry(index, position)
computeTriangleBounds(geometry, triangleBounds, offset, count)
postMessage({ type: 'result' })
} else if (operation === 'REFIT') {
// TODO
} else if (operation === 'REFIT_SUBTREE') {
// TODO
}
}
// Helper functions and utils
function getOnProgressDeltaCallback(cb) {
let lastProgress = 0
return function onProgressDeltaCallback(progress) {
cb(progress - lastProgress)
lastProgress = progress
}
}
function triggerOnProgress(progress) {
// account for error
progress = Math.min(progress, 1)
const currTime = performance.now()
if (currTime - prevTime >= 10 && progress !== 1.0) {
postMessage({
error: null,
progress,
type: 'progress'
})
prevTime = currTime
}
}
function getGeometry(index, position) {
const geometry = new BufferGeometry()
if (index) {
geometry.index = new BufferAttribute(index, 1, false)
}
geometry.setAttribute('position', new BufferAttribute(position, 3))
return geometry
}
function flattenNodes(node) {
const arr = []
traverse(node)
return arr
function traverse(node, depth = 0) {
node.depth = depth
arr.push(node)
const isLeaf = Boolean(node.count)
if (!isLeaf) {
traverse(node.left, depth + 1)
traverse(node.right, depth + 1)
}
}
}

View File

@@ -0,0 +1,60 @@
export class WorkerBase {
constructor( worker ) {
this.name = 'WorkerBase';
this.running = false;
this.worker = worker;
this.worker.onerror = e => {
if ( e.message ) {
throw new Error( `${ this.name }: Could not create Web Worker with error "${ e.message }"` );
} else {
throw new Error( `${ this.name }: Could not create Web Worker.` );
}
};
}
runTask() {}
generate( ...args ) {
if ( this.running ) {
throw new Error( 'GenerateMeshBVHWorker: Already running job.' );
}
if ( this.worker === null ) {
throw new Error( 'GenerateMeshBVHWorker: Worker has been disposed.' );
}
this.running = true;
const promise = this.runTask( this.worker, ...args );
promise.finally( () => {
this.running = false;
} );
return promise;
}
dispose() {
this.worker.terminate();
this.worker = null;
}
}

View File

@@ -0,0 +1,82 @@
export class WorkerPool {
get workerCount() {
return this.workers.length;
}
constructor( getWorkerCallback ) {
this.workers = [];
this._getWorker = getWorkerCallback;
}
setWorkerCount( count ) {
const workers = this.workers;
while ( workers.length < count ) {
workers.push( this._getWorker() );
}
while ( workers.length > count ) {
workers.pop().terminate();
}
}
runSubTask( i, msg, onProgress ) {
return new Promise( ( resolve, reject ) => {
const worker = this.workers[ i ];
if ( worker.isRunning ) {
throw new Error( `${ this.name }: Worker ${ i } is already running.` );
}
worker.isRunning = true;
worker.postMessage( msg );
worker.onerror = e => {
worker.isRunning = false;
reject( e );
};
worker.onmessage = e => {
if ( e.data.type === 'progress' ) {
if ( onProgress ) {
onProgress( e.data.progress );
}
} else {
if ( onProgress ) {
onProgress( 1 );
}
worker.isRunning = false;
resolve( e.data );
}
};
} );
}
}

View File

@@ -0,0 +1,49 @@
// Inspired by https://github.com/Inndy/vue-clipboard2
const Clipboard = require('clipboard')
if (!Clipboard) {
throw new Error('you shold npm install `clipboard` --save at first ')
}
export default {
bind(el, binding) {
if (binding.arg === 'success') {
el._v_clipboard_success = binding.value
} else if (binding.arg === 'error') {
el._v_clipboard_error = binding.value
} else {
const clipboard = new Clipboard(el, {
text() { return binding.value },
action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
})
clipboard.on('success', e => {
const callback = el._v_clipboard_success
callback && callback(e) // eslint-disable-line
})
clipboard.on('error', e => {
const callback = el._v_clipboard_error
callback && callback(e) // eslint-disable-line
})
el._v_clipboard = clipboard
}
},
update(el, binding) {
if (binding.arg === 'success') {
el._v_clipboard_success = binding.value
} else if (binding.arg === 'error') {
el._v_clipboard_error = binding.value
} else {
el._v_clipboard.text = function() { return binding.value }
el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
}
},
unbind(el, binding) {
if (binding.arg === 'success') {
delete el._v_clipboard_success
} else if (binding.arg === 'error') {
delete el._v_clipboard_error
} else {
el._v_clipboard.destroy()
delete el._v_clipboard
}
}
}

View File

@@ -0,0 +1,13 @@
import Clipboard from './clipboard'
const install = function(Vue) {
Vue.directive('Clipboard', Clipboard)
}
if (window.Vue) {
window.clipboard = Clipboard
Vue.use(install); // eslint-disable-line
}
Clipboard.install = install
export default Clipboard

View File

@@ -0,0 +1,46 @@
export default{
bind(el, binding) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style = 'cursor:move;'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
} else {
styL = +sty.left.replace(/\px/g, '')
styT = +sty.top.replace(/\px/g, '')
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
const l = e.clientX - disX
const t = e.clientY - disY
// 移动当前元素
dragDom.style.left = `${l + styL}px`
dragDom.style.top = `${t + styT}px`
// 将此时的位置传出去
// binding.value({x:e.pageX,y:e.pageY})
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
}

View File

@@ -0,0 +1,13 @@
import drag from './drag'
const install = function(Vue) {
Vue.directive('el-drag-dialog', drag)
}
if (window.Vue) {
window['el-drag-dialog'] = drag
Vue.use(install); // eslint-disable-line
}
drag.install = install
export default drag

91
src/directive/sticky.js Normal file
View File

@@ -0,0 +1,91 @@
const vueSticky = {}
let listenAction
vueSticky.install = Vue => {
Vue.directive('sticky', {
inserted(el, binding) {
const params = binding.value || {}
const stickyTop = params.stickyTop || 0
const zIndex = params.zIndex || 1000
const elStyle = el.style
elStyle.position = '-webkit-sticky'
elStyle.position = 'sticky'
// if the browser support css stickyCurrently Safari, Firefox and Chrome Canary
// if (~elStyle.position.indexOf('sticky')) {
// elStyle.top = `${stickyTop}px`;
// elStyle.zIndex = zIndex;
// return
// }
const elHeight = el.getBoundingClientRect().height
const elWidth = el.getBoundingClientRect().width
elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
const parentElm = el.parentNode || document.documentElement
const placeholder = document.createElement('div')
placeholder.style.display = 'none'
placeholder.style.width = `${elWidth}px`
placeholder.style.height = `${elHeight}px`
parentElm.insertBefore(placeholder, el)
let active = false
const getScroll = (target, top) => {
const prop = top ? 'pageYOffset' : 'pageXOffset'
const method = top ? 'scrollTop' : 'scrollLeft'
let ret = target[prop]
if (typeof ret !== 'number') {
ret = window.document.documentElement[method]
}
return ret
}
const sticky = () => {
if (active) {
return
}
if (!elStyle.height) {
elStyle.height = `${el.offsetHeight}px`
}
elStyle.position = 'fixed'
elStyle.width = `${elWidth}px`
placeholder.style.display = 'inline-block'
active = true
}
const reset = () => {
if (!active) {
return
}
elStyle.position = ''
placeholder.style.display = 'none'
active = false
}
const check = () => {
const scrollTop = getScroll(window, true)
const offsetTop = el.getBoundingClientRect().top
if (offsetTop < stickyTop) {
sticky()
} else {
if (scrollTop < elHeight + stickyTop) {
reset()
}
}
}
listenAction = () => {
check()
}
window.addEventListener('scroll', listenAction)
},
unbind() {
window.removeEventListener('scroll', listenAction)
}
})
}
export default vueSticky

View File

@@ -0,0 +1,13 @@
import waves from './waves'
const install = function(Vue) {
Vue.directive('waves', waves)
}
if (window.Vue) {
window.waves = waves
Vue.use(install); // eslint-disable-line
}
waves.install = install
export default waves

View File

@@ -0,0 +1,26 @@
.waves-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
background-clip: padding-box;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transform: scale(0);
-ms-transform: scale(0);
transform: scale(0);
opacity: 1;
}
.waves-ripple.z-active {
opacity: 0;
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}

View File

@@ -0,0 +1,42 @@
import './waves.css'
export default{
bind(el, binding) {
el.addEventListener('click', e => {
const customOpts = Object.assign({}, binding.value)
const opts = Object.assign({
ele: el, // 波纹作用元素
type: 'hit', // hit点击位置扩散center中心点扩展
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
}, customOpts)
const target = opts.ele
if (target) {
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
switch (opts.type) {
case 'center':
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
break
default:
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
}
}, false)
}
}

370
src/icon/demo.css Normal file
View File

@@ -0,0 +1,370 @@
*{margin: 0;padding: 0;list-style: none;}
/*
KISSY CSS Reset
理念1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。
2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。
3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。
特色1. 适应中文2. 基于最新主流浏览器。
维护:玉伯<lifesinger@gmail.com>, 正淳<ragecarrier@gmail.com>
*/
/** 清除内外边距 **/
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */
dl, dt, dd, ul, ol, li, /* list elements 列表元素 */
pre, /* text formatting elements 文本格式元素 */
form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */
th, td /* table elements 表格元素 */ {
margin: 0;
padding: 0;
}
/** 设置默认字体 **/
body,
button, input, select, textarea /* for ie */ {
font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif;
}
h1, h2, h3, h4, h5, h6 { font-size: 100%; }
address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */
code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */
small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
/** 重置列表元素 **/
ul, ol { list-style: none; }
/** 重置文本格式元素 **/
a { text-decoration: none; }
a:hover { text-decoration: underline; }
/** 重置表单元素 **/
legend { color: #000; } /* for ie6 */
fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */
button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */
/* 注optgroup 无法扶正 */
/** 重置表格元素 **/
table { border-collapse: collapse; border-spacing: 0; }
/* 清除浮动 */
.ks-clear:after, .clear:after {
content: '\20';
display: block;
height: 0;
clear: both;
}
.ks-clear, .clear {
*zoom: 1;
}
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;}
.helps{margin-top:40px;}
.helps pre{
padding:20px;
margin:10px 0;
border:solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists{
width: 100% !important;
}
.icon_lists li{
float:left;
width: 100px;
height:180px;
text-align: center;
list-style: none !important;
}
.icon_lists .icon{
font-size: 42px;
line-height: 100px;
margin: 10px 0;
color:#333;
-webkit-transition: font-size 0.25s ease-out 0s;
-moz-transition: font-size 0.25s ease-out 0s;
transition: font-size 0.25s ease-out 0s;
}
.icon_lists .icon:hover{
font-size: 100px;
}
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p,
.markdown pre {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown pre {
border-radius: 6px;
background: #f7f7f7;
padding: 20px;
}
.markdown pre code {
border: none;
background: #f7f7f7;
margin: 0;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown > table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown > table th,
.markdown > table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown > table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
font-style: italic;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
pre{
background: #fff;
}

30
src/icon/iconfont.css Normal file
View File

@@ -0,0 +1,30 @@
@font-face {font-family: "fontFamily";
src: url('iconfont.eot?t=1527493375212'); /* IE9*/
src: url('iconfont.eot?t=1527493375212#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAXoAAsAAAAACHwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW7kf3Y21hcAAAAYAAAABjAAABnM7GacRnbHlmAAAB5AAAAfsAAAIsdf0weGhlYWQAAAPgAAAALwAAADYRguv+aGhlYQAABBAAAAAcAAAAJAfeA4VobXR4AAAELAAAABAAAAAQD+kAAGxvY2EAAAQ8AAAACgAAAAoBjAC6bWF4cAAABEgAAAAfAAAAIAEaAF1uYW1lAAAEaAAAAVMAAAKFV8s9R3Bvc3QAAAW8AAAAKgAAADsY6RIYeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/sU4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDzjZm7438AQw9zA0AAUZgTJAQAk0wx5eJzFkNENgCAMRK+ChIij+GkcyC9HYOKugdfCDxNw5JX2ckkJAHYAgVwkAvJBYHrpivsBh/sRD+fMs/GumrS0NnUm8UT2LlhSEpZJ1q2edXq9x2T/XQd8oqaO+Vo62H4fLw+3AHicLZC/b9NAFMffu6udpM0PbMdx4jQ/LhfXoDRBtpN2IGkXkArNgNQpKkiNYlSBRNd2YKiQEB0YWFDFCkiIiYEfytAJVbDTv6CFjYXSFhbsckE53dP3fZ/e6X3egQRwfkj3aBY0uAgOXIWbACjXsJIkBWR2s0FqqDNJN9JJanObRXilQTtoVOR0xp1rzhhyRE5hEovoMXfObhAbW80FcgXdTAExlzdXVGtapU9xMmsXH4U3yAvUS3w6tVAPr88upt2yFt2Mq2pOVZ9EZUmKEjKRSuJ9IxOTYpNy+EpKmfpe6RIpYTxnm91eopxX+zvNjYJlxBC3t1HLl5OvFxVTEfeBmdHUXORCIpo1E7yaxs3vU1ktXpj5BuJExK7P6A/agwmIQhxyUAAGltiXKUyxmM5GicaF4Yqns5ZnoCjqns5RBK0GlO4Hy7gTbuFRcESKu6vBvdVdAurL4DF2w/cHB1jvdGjvb5uy9Q/B3VFt1E27x8cPw2vtdpt83QIxGc6f0190A6hgmoKU+HsD8lACDpfBg3kAQ4AYo5ljRcFEecuTRG6NeSzeEpj/QTkVfl5EFofh0trp6dpY8RMOBycnwbLrkpLjBIdC3w1+zvaxduez73/x/XUc3jrDjzi8/RtXwiXx7u2Z7zhOmHbq+8JXB3/CN/1+H+AfnVR/vQB4nGNgZGBgAOLuWSW34/ltvjJwszCAwHXDy/8R9P+pLAzMeUAuBwMTSBQAWOoL7AB4nGNgZGBgbvjfwBDDwgACQJKRARWwAABHCgJtBAAAAAPpAAAEAAAABAAAAAAAAAAAdgC6ARYAAHicY2BkYGBgYQhk4GEAASYg5gJCBob/YD4DABGzAXgAeJxtkk1uwjAQhZ/LT9UgddHSdlmvWBQp/CzZooZlJRbsQ3AgKIkjxyDRXQ/Q8/QQPUE3vUHv0EcwQkIk8uibN2/GthIAd/iFwOF54DqwgMfswFe4xrPjGvWO4zq577iBFkaOm9RfHXvo4s1xC/d45wRRv2H2gk/HAm18Ob7CLb4d16j/OK6T/xw38CiE4yba4smxh5noOm6hIz68sVGhVQs538kk0nmsc+vtQxBmSbqbquUmDc1JONFMmTLRuRz4/ZM4Ubkyx3nldjm0Npax0ZkM6FFpqmVh9FpF1l9ZW4x6vdjpfqQzHm8MA4UQlnEBiTl2jAkiaOSIq2jpO1JAb8Z6St+UPUtsyCGnXHJc0mbsMiiZ7WsSA/j8ZJecEzrzyn1+vhJb7jykatkpuQz7M1Lg5ihOSMkSRVVbU4mo+1hVXQV/jR7f+MzvVzfP/gHCFnRdAHicY2BigAAuBuyAhZGJkZmRhZGVgbGCycCSI7WoPDUzN9GQgQEAJkQEKgAA') format('woff'),
url('iconfont.ttf?t=1527493375212') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg?t=1527493375212#fontFamily') format('svg'); /* iOS 4.1- */
}
.fontFamily {
font-family:"fontFamily" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[class^="el-icon-cjn"], [class*=" el-icon-cjn"] {
font-family:"fontFamily" !important;
/* 以下内容参照第三方图标库本身的规则 */
font-size: 13px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.el-icon-cjn-09:before { content: "\e60b"; }
.el-icon-cjn-erweima1:before { content: "\e606"; }

BIN
src/icon/iconfont.eot Normal file

Binary file not shown.

1
src/icon/iconfont.js Normal file
View File

@@ -0,0 +1 @@
(function(window) { var svgSprite = '<svg><symbol id="el-icon-cjn-09" viewBox="0 0 1024 1024"><path d="M804.141176 39.152941H39.152941v963.764706h963.764706V201.788235l-198.776471-162.635294z m-493.929411 60.235294h391.529411v180.705883h-391.529411v-180.705883z m481.882353 843.294118h-542.117647v-331.294118h542.117647v331.294118z m150.588235 0h-90.352941v-391.529412h-662.588236v391.529412h-90.352941v-843.294118h150.588236v240.941177h512v-240.941177h18.070588l162.635294 129.505883v713.788235z" fill="#2c2c2c" ></path><path d="M310.211765 671.623529h331.294117v60.235295h-331.294117zM310.211765 792.094118h210.823529v60.235294h-210.823529zM611.388235 129.505882h60.235294v120.470589h-60.235294z" fill="#2c2c2c" ></path></symbol><symbol id="el-icon-cjn-erweima1" viewBox="0 0 1024 1024"><path d="M23.92225303 998.84837371l439.58598408 0L463.50823711 559.26238962 23.92225303 559.26238962 23.92225303 998.84837371zM119.78212494 651.01398129l243.75796004 0 0 245.12738674L119.78212494 896.14136803 119.78212494 651.01398129z" ></path><path d="M23.92225303 457.92481072l439.58598408 0 0-439.58598412L23.92225303 18.33882658 23.92225303 457.92481072zM119.78212494 111.45984508l243.75796004 0 0 245.12738672L119.78212494 356.5872318 119.78212494 111.45984508z" ></path><path d="M570.32352297 18.33882658l0 439.58598414L1009.90950704 457.92481072l0-439.58598412L570.32352297 18.33882658zM911.31078166 355.21780509L667.55282162 355.21780509l0-245.12738681 243.75796004 0L911.31078166 355.21780509z" ></path><path d="M218.38085034 210.05857039l49.29936273 0 0 49.29936276-49.29936273 0 0-49.29936276Z" ></path><path d="M760.67384005 210.05857039l49.29936271 0 0 49.29936276-49.29936273 0 0-49.29936276Z" ></path><path d="M218.38085034 750.98213347l49.29936273 0 0 49.29936266-49.29936273 0 0-49.29936266Z" ></path><path d="M908.57192818 755.09041369L809.97320276 755.09041369 809.97320276 559.26238962 570.32352297 559.26238962 570.32352297 998.84837371 614.14517874 998.84837371 614.14517874 707.16047766 711.37447735 707.16047766 711.37447735 805.75920314 1009.90950704 805.75920314 1009.90950704 559.26238962 908.57192818 559.26238962Z" ></path><path d="M711.37447735 901.61907503l99.96815218 0 0 97.22929868-99.96815218 0 0-97.22929868Z" ></path><path d="M909.94135491 901.61907503l99.96815213 0 0 97.22929868-99.96815213 0 0-97.22929868Z" ></path></symbol></svg>'; var script = (function() { var scripts = document.getElementsByTagName('script'); return scripts[scripts.length - 1] }()); var shouldInjectCss = script.getAttribute('data-injectcss'); var ready = function(fn) { if (document.addEventListener) { if (~['complete', 'loaded', 'interactive'].indexOf(document.readyState)) { setTimeout(fn, 0) } else { var loadFn = function() { document.removeEventListener('DOMContentLoaded', loadFn, false); fn() }; document.addEventListener('DOMContentLoaded', loadFn, false) } } else if (document.attachEvent) { IEContentLoaded(window, fn) } function IEContentLoaded(w, fn) { var d = w.document, done = false, init = function() { if (!done) { done = true; fn() } }; var polling = function() { try { d.documentElement.doScroll('left') } catch (e) { setTimeout(polling, 50); return }init() }; polling(); d.onreadystatechange = function() { if (d.readyState == 'complete') { d.onreadystatechange = null; init() } } } }; var before = function(el, target) { target.parentNode.insertBefore(el, target) }; var prepend = function(el, target) { if (target.firstChild) { before(el, target.firstChild) } else { target.appendChild(el) } }; function appendSvg() { var div, svg; div = document.createElement('div'); div.innerHTML = svgSprite; svgSprite = null; svg = div.getElementsByTagName('svg')[0]; if (svg) { svg.setAttribute('aria-hidden', 'true'); svg.style.position = 'absolute'; svg.style.width = 0; svg.style.height = 0; svg.style.overflow = 'hidden'; prepend(svg, document.body) } } if (shouldInjectCss && !window.__iconfont__svg__cssinject__) { window.__iconfont__svg__cssinject__ = true; try { document.write('<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>') } catch (e) { console && console.log(e) } }ready(appendSvg) })(window)

39
src/icon/iconfont.svg Normal file
View File

@@ -0,0 +1,39 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="fontFamily" horiz-adv-x="1024" >
<font-face
font-family="fontFamily"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="x" unicode="x" horiz-adv-x="1001"
d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
<glyph glyph-name="09" unicode="&#58891;" d="M804.141 856.847h-764.988v-963.765h963.765v801.129l-198.776 162.635zM310.212 796.612h391.529v-180.706h-391.529v180.706zM792.094-46.682h-542.118v331.294h542.118v-331.294zM942.682-46.682h-90.353v391.529h-662.588v-391.529h-90.353v843.294h150.588v-240.941h512v240.941h18.071l162.635-129.506v-713.788zM310.212 224.376h331.294v-60.235h-331.294zM310.212 103.906h210.824v-60.235h-210.824zM611.388 766.494h60.235v-120.471h-60.235z" horiz-adv-x="1024" />
<glyph glyph-name="erweima1" unicode="&#58886;" d="M23.92225303-102.84837371000003l439.58598408 0L463.50823711 336.73761038 23.92225303 336.73761038 23.92225303-102.84837371000003zM119.78212494 244.98601871000005l243.75796004 0 0-245.12738674L119.78212494-0.1413680299999669 119.78212494 244.98601871000005zM23.92225303 438.07518928l439.58598408 0 0 439.58598412L23.92225303 877.66117342 23.92225303 438.07518928zM119.78212494 784.54015492l243.75796004 0 0-245.12738672L119.78212494 539.4127682000001 119.78212494 784.54015492zM570.32352297 877.66117342l0-439.58598414L1009.90950704 438.07518928l0 439.58598412L570.32352297 877.66117342zM911.31078166 540.78219491L667.55282162 540.78219491l0 245.12738681 243.75796004 0L911.31078166 540.78219491zM218.38085034 685.94142961l49.29936273 0 0-49.29936276-49.29936273 0 0 49.29936276ZM760.67384005 685.94142961l49.29936271 0 0-49.29936276-49.29936273 0 0 49.29936276ZM218.38085034 145.01786653l49.29936273 0 0-49.29936266-49.29936273 0 0 49.29936266ZM908.57192818 140.90958631L809.97320276 140.90958631 809.97320276 336.73761038 570.32352297 336.73761038 570.32352297-102.84837371000003 614.14517874-102.84837371000003 614.14517874 188.83952234000003 711.37447735 188.83952234000003 711.37447735 90.24079686000005 1009.90950704 90.24079686000005 1009.90950704 336.73761038 908.57192818 336.73761038ZM711.37447735-5.619075029999976l99.96815218 0 0-97.22929868-99.96815218 0 0 97.22929868ZM909.94135491-5.619075029999976l99.96815213 0 0-97.22929868-99.96815213 0 0 97.22929868Z" horiz-adv-x="1024" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/icon/iconfont.ttf Normal file

Binary file not shown.

BIN
src/icon/iconfont.woff Normal file

Binary file not shown.

22
src/icons/svgo.yml Normal file
View File

@@ -0,0 +1,22 @@
# replace default config
# multipass: true
# full: true
plugins:
# - name
#
# or:
# - name: false
# - name: true
#
# or:
# - name:
# param1: 1
# param2: 2
- removeAttrs:
attrs:
- 'fill'
- 'fill-rule'

BIN
src/img/ActualOutput.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/img/FaultsTime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/img/NumberFaults.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

BIN
src/img/OEEAnalysis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/img/ProductionPlan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More