built out a simple framework for building 3d objects
an orbital camera and some basic shapes are used to build the 3d bin shell and the flat inventory when using manual or lidar are built
This commit is contained in:
parent
3d1c11646f
commit
cb6609bdb1
13 changed files with 995 additions and 0 deletions
264
package-lock.json
generated
264
package-lock.json
generated
|
|
@ -25,10 +25,12 @@
|
|||
"@mui/styles": "^6.1.6",
|
||||
"@mui/x-date-pickers": "^7.26.0",
|
||||
"@react-pdf/renderer": "^4.3.0",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"@sentry/react": "^8.38.0",
|
||||
"@turf/area": "^7.2.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@types/classnames": "^2.3.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"axios": "^1.7.7",
|
||||
"crisp-sdk-web": "^1.0.26",
|
||||
"dayjs": "^1.11.13",
|
||||
|
|
@ -62,6 +64,7 @@
|
|||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
"recharts": "^2.15.1",
|
||||
"semver": "^7.7.1",
|
||||
"three": "^0.183.2",
|
||||
"victory": "^37.3.6",
|
||||
"weather-icons-react": "^1.2.0"
|
||||
},
|
||||
|
|
@ -1776,6 +1779,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||
|
|
@ -3223,6 +3232,64 @@
|
|||
"@react-pdf/stylesheet": "^6.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
|
||||
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/react-reconciler": "^0.26.7",
|
||||
"@types/webxr": "*",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"its-fine": "^1.0.6",
|
||||
"react-reconciler": "^0.27.0",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"scheduler": "^0.21.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"zustand": "^3.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=43.0",
|
||||
"expo-asset": ">=8.4",
|
||||
"expo-file-system": ">=11.0",
|
||||
"expo-gl": ">=11.0",
|
||||
"react": ">=18 <19",
|
||||
"react-dom": ">=18 <19",
|
||||
"react-native": ">=0.64",
|
||||
"three": ">=0.133"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-asset": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-file-system": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-gl": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber/node_modules/scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
|
|
@ -5534,6 +5601,12 @@
|
|||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -5930,6 +6003,15 @@
|
|||
"@types/viewport-mercator-project": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-reconciler": {
|
||||
"version": "0.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
|
||||
"integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||
|
|
@ -5984,6 +6066,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
|
|
@ -5993,6 +6081,21 @@
|
|||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.183.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
|
|
@ -6010,6 +6113,12 @@
|
|||
"gl-matrix": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||
|
|
@ -6450,6 +6559,12 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.69",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/abs-svg-path": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
||||
|
|
@ -6882,6 +6997,30 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -8386,6 +8525,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
|
@ -9211,6 +9356,26 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
@ -9793,6 +9958,27 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/its-fine": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
|
||||
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-reconciler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/its-fine/node_modules/@types/react-reconciler": {
|
||||
"version": "0.28.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||
|
|
@ -10414,6 +10600,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
|
@ -11663,6 +11855,31 @@
|
|||
"react-dom": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
|
||||
"integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler/node_modules/scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
|
|
@ -11767,6 +11984,21 @@
|
|||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-measure": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13",
|
||||
"react-dom": ">=16.13"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
|
|
@ -12995,6 +13227,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/suspend-react": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-arc-to-cubic-bezier": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
|
||||
|
|
@ -13071,6 +13312,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
|
|
@ -15278,6 +15525,23 @@
|
|||
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
||||
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
|
||||
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@
|
|||
"@mui/styles": "^6.1.6",
|
||||
"@mui/x-date-pickers": "^7.26.0",
|
||||
"@react-pdf/renderer": "^4.3.0",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"@sentry/react": "^8.38.0",
|
||||
"@turf/area": "^7.2.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@types/classnames": "^2.3.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"axios": "^1.7.7",
|
||||
"crisp-sdk-web": "^1.0.26",
|
||||
"dayjs": "^1.11.13",
|
||||
|
|
@ -74,6 +76,7 @@
|
|||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
"recharts": "^2.15.1",
|
||||
"semver": "^7.7.1",
|
||||
"three": "^0.183.2",
|
||||
"victory": "^37.3.6",
|
||||
"weather-icons-react": "^1.2.0"
|
||||
},
|
||||
|
|
|
|||
119
src/3dModels/CameraControls/OrbitCameraControls.tsx
Normal file
119
src/3dModels/CameraControls/OrbitCameraControls.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { useThree } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface CameraTarget {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
target?: CameraTarget;
|
||||
clampVerticalRotation?: boolean;
|
||||
/**
|
||||
* The minimum vertical rotation in radians
|
||||
* @default 0.01
|
||||
*/
|
||||
minPhi?: number;
|
||||
/**
|
||||
* The maximum vertical rotation in radians
|
||||
* @default Math.PI - 0.01
|
||||
*/
|
||||
maxPhi?: number;
|
||||
}
|
||||
|
||||
export default function OrbitCameraControls(props: Props) {
|
||||
const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props;
|
||||
const { camera, gl } = useThree();
|
||||
|
||||
const isDragging = useRef(false);
|
||||
const lastPos = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Spherical coords
|
||||
const spherical = useRef({
|
||||
radius: 10,
|
||||
theta: 0, // horizontal
|
||||
phi: Math.PI / 2, // vertical
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = gl.domElement;
|
||||
|
||||
const updateCamera = () => {
|
||||
const { radius, theta, phi } = spherical.current;
|
||||
|
||||
const x = radius * Math.sin(phi) * Math.sin(theta);
|
||||
const y = radius * Math.cos(phi);
|
||||
const z = radius * Math.sin(phi) * Math.cos(theta);
|
||||
|
||||
camera.position.set(x, y, z);
|
||||
camera.lookAt(target?.x ?? 0, target?.y ?? 0, target?.z ?? 0);
|
||||
};
|
||||
|
||||
updateCamera();
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (e.target !== canvas) return;
|
||||
isDragging.current = true;
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.current) return;
|
||||
|
||||
const deltaX = e.clientX - lastPos.current.x;
|
||||
const deltaY = e.clientY - lastPos.current.y;
|
||||
|
||||
spherical.current.theta -= deltaX * 0.005;
|
||||
spherical.current.phi -= deltaY * 0.005;
|
||||
|
||||
// Clamp vertical rotation (prevents flipping)
|
||||
if (clampVerticalRotation) {
|
||||
spherical.current.phi = Math.max(
|
||||
minPhi,
|
||||
Math.min(maxPhi, spherical.current.phi)
|
||||
);
|
||||
}
|
||||
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
|
||||
updateCamera();
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging.current = false;
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.target !== canvas) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
spherical.current.radius += e.deltaY * 0.01;
|
||||
|
||||
// Clamp zoom
|
||||
spherical.current.radius = Math.max(
|
||||
3,
|
||||
Math.min(30, spherical.current.radius)
|
||||
);
|
||||
|
||||
updateCamera();
|
||||
};
|
||||
|
||||
window.addEventListener("mousedown", handleMouseDown);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousedown", handleMouseDown);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
canvas.removeEventListener("wheel", handleWheel);
|
||||
};
|
||||
}, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi]);
|
||||
|
||||
return null;
|
||||
}
|
||||
34
src/3dModels/Shapes/3D/Circle.tsx
Normal file
34
src/3dModels/Shapes/3D/Circle.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import BaseMesh, { BaseMeshProps } from "../BaseMesh";
|
||||
|
||||
export interface CircleGeometry{
|
||||
radius: number
|
||||
radialSegments?: number
|
||||
thetaStart?: number
|
||||
thetaLength?: number
|
||||
}
|
||||
|
||||
interface Props extends Omit<BaseMeshProps, "geometry"> {
|
||||
geometry: CircleGeometry;
|
||||
}
|
||||
|
||||
export default function Circle(props: Props) {
|
||||
const { geometry } = props;
|
||||
|
||||
const geo = useMemo(() => {
|
||||
return new THREE.CircleGeometry(
|
||||
geometry.radius,
|
||||
geometry.radialSegments ?? 24,
|
||||
geometry.thetaStart ?? 0,
|
||||
geometry.thetaLength ?? Math.PI * 2
|
||||
);
|
||||
}, [
|
||||
geometry.radius,
|
||||
geometry.radialSegments,
|
||||
geometry.thetaStart,
|
||||
geometry.thetaLength
|
||||
]);
|
||||
|
||||
return <BaseMesh {...props} geometry={geo} />;
|
||||
}
|
||||
43
src/3dModels/Shapes/3D/Cone.tsx
Normal file
43
src/3dModels/Shapes/3D/Cone.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import BaseMesh, { BaseMeshProps } from "../BaseMesh";
|
||||
|
||||
export interface ConeGeometry {
|
||||
radius: number;
|
||||
height: number;
|
||||
radialSegments?: number;
|
||||
heightSegments?: number;
|
||||
openEnded?: boolean;
|
||||
thetaStart?: number;
|
||||
thetaLength?: number;
|
||||
}
|
||||
|
||||
interface Props extends Omit<BaseMeshProps, "geometry"> {
|
||||
geometry: ConeGeometry;
|
||||
}
|
||||
|
||||
export default function Cone(props: Props) {
|
||||
const { geometry } = props;
|
||||
|
||||
const geo = useMemo(() => {
|
||||
return new THREE.ConeGeometry(
|
||||
geometry.radius,
|
||||
geometry.height,
|
||||
geometry.radialSegments ?? 24,
|
||||
geometry.heightSegments ?? 1,
|
||||
geometry.openEnded ?? false,
|
||||
geometry.thetaStart ?? 0,
|
||||
geometry.thetaLength ?? Math.PI * 2
|
||||
);
|
||||
}, [
|
||||
geometry.radius,
|
||||
geometry.height,
|
||||
geometry.radialSegments,
|
||||
geometry.heightSegments,
|
||||
geometry.openEnded,
|
||||
geometry.thetaStart,
|
||||
geometry.thetaLength
|
||||
]);
|
||||
|
||||
return <BaseMesh {...props} geometry={geo} />;
|
||||
}
|
||||
40
src/3dModels/Shapes/3D/Cube.tsx
Normal file
40
src/3dModels/Shapes/3D/Cube.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import BaseMesh, { BaseMeshProps } from "../BaseMesh";
|
||||
|
||||
export interface CubeGeometry {
|
||||
height: number;
|
||||
width: number;
|
||||
depth: number;
|
||||
widthSegments?: number;
|
||||
heightSegments?: number;
|
||||
depthSegments?: number;
|
||||
}
|
||||
|
||||
interface Props extends Omit<BaseMeshProps, "geometry"> {
|
||||
geometry: CubeGeometry;
|
||||
}
|
||||
|
||||
export default function Cube(props: Props) {
|
||||
const { geometry } = props;
|
||||
|
||||
const geo = useMemo(() => {
|
||||
return new THREE.BoxGeometry(
|
||||
geometry.height,
|
||||
geometry.width,
|
||||
geometry.depth,
|
||||
geometry.widthSegments ?? 2,
|
||||
geometry.heightSegments ?? 2,
|
||||
geometry.depthSegments ?? 2
|
||||
);
|
||||
}, [
|
||||
geometry.height,
|
||||
geometry.width,
|
||||
geometry.depth,
|
||||
geometry.widthSegments,
|
||||
geometry.heightSegments,
|
||||
geometry.depthSegments
|
||||
]);
|
||||
|
||||
return <BaseMesh {...props} geometry={geo} />;
|
||||
}
|
||||
46
src/3dModels/Shapes/3D/Cylinder.tsx
Normal file
46
src/3dModels/Shapes/3D/Cylinder.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import BaseMesh, { BaseMeshProps } from "../BaseMesh";
|
||||
|
||||
export interface CylinderGeometry {
|
||||
radiusTop: number;
|
||||
radiusBottom: number;
|
||||
height: number;
|
||||
radialSegments?: number;
|
||||
heightSegments?: number;
|
||||
openEnded?: boolean;
|
||||
thetaStart?: number;
|
||||
thetaLength?: number;
|
||||
}
|
||||
|
||||
interface Props extends Omit<BaseMeshProps, "geometry"> {
|
||||
geometry: CylinderGeometry;
|
||||
}
|
||||
|
||||
export default function Cylinder(props: Props) {
|
||||
const { geometry } = props;
|
||||
|
||||
const geo = useMemo(() => {
|
||||
return new THREE.CylinderGeometry(
|
||||
geometry.radiusTop,
|
||||
geometry.radiusBottom,
|
||||
geometry.height,
|
||||
geometry.radialSegments ?? 24,
|
||||
geometry.heightSegments ?? 1,
|
||||
geometry.openEnded ?? false,
|
||||
geometry.thetaStart ?? 0,
|
||||
geometry.thetaLength ?? Math.PI * 2
|
||||
);
|
||||
}, [
|
||||
geometry.radiusTop,
|
||||
geometry.radiusBottom,
|
||||
geometry.height,
|
||||
geometry.radialSegments,
|
||||
geometry.heightSegments,
|
||||
geometry.openEnded,
|
||||
geometry.thetaStart,
|
||||
geometry.thetaLength
|
||||
]);
|
||||
|
||||
return <BaseMesh {...props} geometry={geo} />;
|
||||
}
|
||||
87
src/3dModels/Shapes/BaseMesh.tsx
Normal file
87
src/3dModels/Shapes/BaseMesh.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Euler, Vector3, BufferGeometry, MaterialParameters, Side } from "three";
|
||||
|
||||
export interface BaseMeshProps {
|
||||
geometry: BufferGeometry;
|
||||
position?: Vector3;
|
||||
rotation?: Euler;
|
||||
colour?: string;
|
||||
opacity?: number;
|
||||
metalness?: number;
|
||||
roughness?: number;
|
||||
wireframe?: boolean;
|
||||
frameColour?: string;
|
||||
materialType?: "standard" | "basic";
|
||||
materialProps?: Partial<MaterialParameters>;
|
||||
materialOverride?: React.ReactNode;
|
||||
side?: Side
|
||||
}
|
||||
|
||||
export default function BaseMesh(props: BaseMeshProps) {
|
||||
const {
|
||||
geometry,
|
||||
position,
|
||||
rotation,
|
||||
colour = "#fff",
|
||||
opacity = 1,
|
||||
metalness = 0,
|
||||
roughness = 1,
|
||||
wireframe = false,
|
||||
frameColour = "black",
|
||||
materialType,
|
||||
materialProps,
|
||||
materialOverride,
|
||||
side
|
||||
} = props;
|
||||
|
||||
const isTransparent = opacity < 1;
|
||||
const buildMaterial = () => {
|
||||
if (materialOverride) return materialOverride;
|
||||
switch (materialType) {
|
||||
case "basic":
|
||||
return (
|
||||
<meshBasicMaterial
|
||||
color={colour}
|
||||
transparent={isTransparent}
|
||||
opacity={opacity}
|
||||
depthWrite={!isTransparent}
|
||||
side={side}
|
||||
{...materialProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case "standard":
|
||||
default:
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={colour}
|
||||
metalness={metalness}
|
||||
roughness={roughness}
|
||||
transparent={isTransparent}
|
||||
opacity={opacity}
|
||||
depthWrite={!isTransparent}
|
||||
side={side}
|
||||
{...materialProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<group position={position} rotation={rotation}>
|
||||
{/* Main surface */}
|
||||
<mesh geometry={geometry}>
|
||||
{buildMaterial()}
|
||||
</mesh>
|
||||
|
||||
{/* Optional wireframe overlay */}
|
||||
{wireframe && (
|
||||
<mesh geometry={geometry}>
|
||||
<meshStandardMaterial
|
||||
wireframe
|
||||
color={frameColour}
|
||||
/>
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
74
src/bin/3dView/Bin3dView.tsx
Normal file
74
src/bin/3dView/Bin3dView.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Bin } from "models";
|
||||
import BinShell from "./BinParts/BinShell";
|
||||
import GrainFillFlat from "./grainFill/GrainFillFlat";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The bin to generate a 3D model of
|
||||
*/
|
||||
bin: Bin
|
||||
/**
|
||||
* The scale to apply to the bin dimensions
|
||||
* ie 100 would make the bin 1:100 scale
|
||||
* @default 100
|
||||
*/
|
||||
scale: number
|
||||
/**
|
||||
* optional: the percent of the bin to fill using a level top, this will be used with manual and lidar controls
|
||||
*/
|
||||
fillPercent?: number
|
||||
}
|
||||
|
||||
export default function Bin3dView(props: Props){
|
||||
//this function will generate a 3D model of a bin based on its settings using multiple meshes, cylinder for the body, cone for the roof
|
||||
// and either a cone for the hopper or circle for flat bottom, it is possible to also use lathe geometry for this as well,
|
||||
// it might even work better because we can control the smoothness easier with it being only one mesh rather than 3
|
||||
const {bin, scale = 100, fillPercent} = props
|
||||
|
||||
console.log(bin)
|
||||
|
||||
return (
|
||||
<Canvas>
|
||||
<group scale={[1/scale, 1/scale, 1/scale]}>
|
||||
<BinShell
|
||||
binBodyColour="#fff"
|
||||
radialSegments={20}
|
||||
binMetalness={0.5}
|
||||
binRoughness={0.7}
|
||||
binOpacity={0.5}
|
||||
diameter={bin.diameter()}
|
||||
sidewallHeight={bin.sidewallHeight()}
|
||||
roofHeight={bin.roofHeight()}
|
||||
hopperHeight={bin.hopperHeight()}
|
||||
/>
|
||||
|
||||
{/* grain - cylinder*/}
|
||||
{fillPercent !== undefined && (
|
||||
<GrainFillFlat
|
||||
diameter={bin.diameter()}
|
||||
sidewallHeight={bin.sidewallHeight()}
|
||||
hopperHeight={bin.hopperHeight()}
|
||||
fillPercent={fillPercent}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
{/* cables */}
|
||||
|
||||
{/* lighting */}
|
||||
<ambientLight intensity={0.2} color={"white"}/>
|
||||
<directionalLight intensity={0.2} color={"white"} position={[0,0,5]}/>
|
||||
<directionalLight intensity={0.2} color={"white"} position={[0,0,-5]}/>
|
||||
<directionalLight intensity={0.2} color={"white"} position={[5,0,0]}/>
|
||||
<directionalLight intensity={0.2} color={"white"} position={[-5,0,0]}/>
|
||||
{/* <hemisphereLight
|
||||
color={"white"}
|
||||
groundColor={"#444"}
|
||||
intensity={0.7}
|
||||
/> */}
|
||||
{/* camera controls */}
|
||||
<OrbitCameraControls clampVerticalRotation />
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
127
src/bin/3dView/BinParts/BinShell.tsx
Normal file
127
src/bin/3dView/BinParts/BinShell.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import Circle, { CircleGeometry } from "3dModels/Shapes/3D/Circle"
|
||||
import Cone, { ConeGeometry } from "3dModels/Shapes/3D/Cone"
|
||||
import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder"
|
||||
import React from "react"
|
||||
import { Euler, Vector3 } from "three"
|
||||
|
||||
interface Props {
|
||||
radialSegments: number
|
||||
binBodyColour: string
|
||||
binMetalness: number
|
||||
binRoughness: number
|
||||
binOpacity: number
|
||||
diameter: number
|
||||
sidewallHeight: number
|
||||
roofHeight: number
|
||||
hopperHeight?: number
|
||||
wireframe?: boolean
|
||||
}
|
||||
|
||||
export default function BinShell(props: Props) {
|
||||
const { radialSegments, binBodyColour, binMetalness, binRoughness, binOpacity, diameter, sidewallHeight, roofHeight, hopperHeight, wireframe } = props
|
||||
const cylinderGeometry = React.useMemo(() => ({
|
||||
radiusTop: diameter / 2,
|
||||
radiusBottom: diameter / 2,
|
||||
height: sidewallHeight,
|
||||
radialSegments,
|
||||
heightSegments: 2,
|
||||
openEnded: true,
|
||||
} as CylinderGeometry), [diameter, sidewallHeight, radialSegments]);
|
||||
|
||||
const roofGeometry = React.useMemo(() => ({
|
||||
height: roofHeight,
|
||||
radius: diameter /2,
|
||||
radialSegments: radialSegments,
|
||||
openEnded: true
|
||||
} as ConeGeometry), [diameter, roofHeight, radialSegments])
|
||||
|
||||
const hopperGeometry = React.useMemo(() => ({
|
||||
height: hopperHeight,
|
||||
radius: diameter /2,
|
||||
radialSegments: radialSegments,
|
||||
openEnded: true
|
||||
} as ConeGeometry),[hopperHeight, diameter, radialSegments])
|
||||
|
||||
const flatBottomGeometry = React.useMemo(() => ({
|
||||
radius: diameter/2,
|
||||
radialSegments: radialSegments
|
||||
} as CircleGeometry),[diameter, radialSegments])
|
||||
|
||||
const roofPosition = React.useMemo(
|
||||
() => new Vector3(0, sidewallHeight/2 + roofHeight/2, 0),
|
||||
[sidewallHeight, roofHeight]
|
||||
);
|
||||
|
||||
const bottomPosition = React.useMemo(
|
||||
() => {
|
||||
if(hopperHeight !== undefined && hopperHeight > 0){
|
||||
//this returns the position for the center of the cone for the hopper
|
||||
return new Vector3(0, (sidewallHeight/2 + hopperHeight/2)*-1, 0)
|
||||
} else {
|
||||
//this returns the position for the circle for the flat bottom
|
||||
return new Vector3(0, -sidewallHeight/2, 0)
|
||||
}
|
||||
},
|
||||
[sidewallHeight, hopperHeight]
|
||||
)
|
||||
|
||||
const hopperRotation = React.useMemo(
|
||||
() => new Euler(Math.PI, 0, 0),
|
||||
[]
|
||||
);
|
||||
|
||||
const flatBottomRotation = React.useMemo(
|
||||
() => new Euler(-Math.PI / 2, 0, 0),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* bin roof - cone */}
|
||||
<Cone
|
||||
geometry={roofGeometry}
|
||||
colour={binBodyColour}
|
||||
wireframe= {wireframe}
|
||||
metalness={binMetalness}
|
||||
position={roofPosition}
|
||||
roughness={binRoughness}
|
||||
side={2}
|
||||
opacity={binOpacity}/>
|
||||
{/* bin sidewall - cylinder (open ended for the roof and bottom)*/}
|
||||
<Cylinder
|
||||
geometry={cylinderGeometry}
|
||||
colour={binBodyColour}
|
||||
wireframe= {wireframe}
|
||||
metalness={binMetalness}
|
||||
roughness={binRoughness}
|
||||
opacity={binOpacity}/>
|
||||
{/* bin bottom - cone -OR- circle depending on the bins shape*/}
|
||||
{hopperHeight !== undefined && hopperHeight > 0 ?
|
||||
<Cone
|
||||
geometry={hopperGeometry}
|
||||
colour={binBodyColour}
|
||||
wireframe= {wireframe}
|
||||
metalness={binMetalness}
|
||||
position={bottomPosition}
|
||||
rotation={hopperRotation}
|
||||
roughness={binRoughness}
|
||||
opacity={binOpacity}
|
||||
side={2}
|
||||
/>
|
||||
:
|
||||
<Circle
|
||||
geometry={flatBottomGeometry}
|
||||
colour={binBodyColour}
|
||||
wireframe= {wireframe}
|
||||
metalness={binMetalness}
|
||||
position={bottomPosition}
|
||||
rotation={flatBottomRotation}
|
||||
roughness={binRoughness}
|
||||
opacity={binOpacity}
|
||||
side={2}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
}
|
||||
123
src/bin/3dView/grainFill/GrainFillFlat.tsx
Normal file
123
src/bin/3dView/grainFill/GrainFillFlat.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React, { useMemo } from "react";
|
||||
import Cylinder from "3dModels/Shapes/3D/Cylinder";
|
||||
import Cone from "3dModels/Shapes/3D/Cone";
|
||||
import { Vector3, Euler } from "three";
|
||||
|
||||
interface Props {
|
||||
diameter: number;
|
||||
sidewallHeight: number;
|
||||
hopperHeight?: number;
|
||||
fillPercent: number;
|
||||
}
|
||||
|
||||
export default function GrainFillFlat(props: Props) {
|
||||
const {
|
||||
diameter,
|
||||
sidewallHeight,
|
||||
hopperHeight = 0,
|
||||
fillPercent
|
||||
} = props;
|
||||
|
||||
const radius = diameter / 2;
|
||||
|
||||
// Slightly smaller to avoid z-fighting with shell
|
||||
const grainRadius = radius * 0.98;
|
||||
|
||||
// --- volumes ---
|
||||
const cylinderVolume = Math.PI * radius * radius * sidewallHeight;
|
||||
const hopperVolume = hopperHeight > 0
|
||||
? (1 / 3) * Math.PI * radius * radius * hopperHeight
|
||||
: 0;
|
||||
|
||||
const totalVolume = cylinderVolume + hopperVolume;
|
||||
const filledVolume = totalVolume * fillPercent;
|
||||
|
||||
// --- result values ---
|
||||
let hopperFillHeight = 0;
|
||||
let cylinderFillHeight = 0;
|
||||
|
||||
if (hopperHeight > 0 && filledVolume <= hopperVolume) {
|
||||
// ONLY hopper filling
|
||||
const ratio = filledVolume / hopperVolume;
|
||||
|
||||
hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3);
|
||||
cylinderFillHeight = 0;
|
||||
} else {
|
||||
// hopper full (or no hopper)
|
||||
hopperFillHeight = hopperHeight;
|
||||
|
||||
const remainingVolume = filledVolume - hopperVolume;
|
||||
|
||||
cylinderFillHeight = Math.max(
|
||||
0,
|
||||
remainingVolume / (Math.PI * radius * radius)
|
||||
);
|
||||
}
|
||||
|
||||
// --- positions ---
|
||||
|
||||
const hopperPosition = useMemo(
|
||||
() => new Vector3(
|
||||
0,
|
||||
-(sidewallHeight / 2 + hopperHeight) + hopperFillHeight / 2, //adding the hopper height so it starts at the cone and not the flat side
|
||||
0
|
||||
),
|
||||
[sidewallHeight, hopperFillHeight]
|
||||
);
|
||||
|
||||
//flip the cone so it matches the hoppers rotation with the cone pointed down
|
||||
const hopperRotation = useMemo(
|
||||
() => new Euler(Math.PI, 0, 0),
|
||||
[]
|
||||
);
|
||||
|
||||
const cylinderPosition = useMemo(
|
||||
() => new Vector3(
|
||||
0,
|
||||
-sidewallHeight / 2 + cylinderFillHeight / 2,
|
||||
0
|
||||
),
|
||||
[sidewallHeight, cylinderFillHeight]
|
||||
);
|
||||
|
||||
// --- material ---
|
||||
const grainColour = "#fff302";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hopper fill */}
|
||||
{hopperHeight > 0 && hopperFillHeight > 0 && (
|
||||
<Cone
|
||||
geometry={{
|
||||
radius: grainRadius * (hopperFillHeight / hopperHeight),
|
||||
height: hopperFillHeight,
|
||||
radialSegments: 20,
|
||||
openEnded: false
|
||||
}}
|
||||
position={hopperPosition}
|
||||
rotation={hopperRotation}
|
||||
colour={grainColour}
|
||||
roughness={1}
|
||||
metalness={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cylinder fill */}
|
||||
{cylinderFillHeight > 0 && (
|
||||
<Cylinder
|
||||
geometry={{
|
||||
radiusTop: grainRadius,
|
||||
radiusBottom: grainRadius,
|
||||
height: cylinderFillHeight,
|
||||
radialSegments: 20,
|
||||
openEnded: false
|
||||
}}
|
||||
position={cylinderPosition}
|
||||
colour={grainColour}
|
||||
roughness={1}
|
||||
metalness={0}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -52,6 +52,26 @@ export class Bin {
|
|||
return this.settings.specs?.shape;
|
||||
}
|
||||
|
||||
public diameter(): number {
|
||||
return this.settings.specs?.diameterCm ?? 0;
|
||||
}
|
||||
|
||||
public height(): number {
|
||||
return this.settings.specs?.heightCm ?? 0;
|
||||
}
|
||||
|
||||
public sidewallHeight(): number {
|
||||
return this.settings.specs?.advancedDimensions?.sidewallHeight ?? 0;
|
||||
}
|
||||
|
||||
public roofHeight() : number {
|
||||
return this.settings.specs?.advancedDimensions?.topConeHeight ?? 0;
|
||||
}
|
||||
|
||||
public hopperHeight() : number {
|
||||
return this.settings.specs?.advancedDimensions?.hopperHeight ?? 0;
|
||||
}
|
||||
|
||||
public key(): string {
|
||||
return this.settings.key;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Typography,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import BinActions from "bin/BinActions";
|
||||
import BinHistory from "bin/BinHistory";
|
||||
|
|
@ -65,6 +66,7 @@ import ButtonGroup from "common/ButtonGroup";
|
|||
import BinTransactions from "bin/BinTransactions";
|
||||
import { CO2 } from "models/CO2";
|
||||
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
|
||||
import Bin3dView from "bin/3dView/Bin3dView";
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -773,6 +775,8 @@ export default function Bin(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const [fillPercent, setFillPercent] = useState(0)
|
||||
|
||||
const desktopView = () => {
|
||||
return (
|
||||
<Card sx={{ padding: 1 }}>
|
||||
|
|
@ -802,6 +806,17 @@ export default function Bin(props: Props) {
|
|||
</Box>
|
||||
{overview()}
|
||||
{preferences && binComponents(preferences)}
|
||||
<Card>
|
||||
<Box height={1000}>
|
||||
<TextField
|
||||
type="number"
|
||||
onChange={e => {
|
||||
setFillPercent(+e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Bin3dView bin={bin} scale={100} fillPercent={fillPercent/100}/>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
|
||||
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue