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/styles": "^6.1.6",
|
||||||
"@mui/x-date-pickers": "^7.26.0",
|
"@mui/x-date-pickers": "^7.26.0",
|
||||||
"@react-pdf/renderer": "^4.3.0",
|
"@react-pdf/renderer": "^4.3.0",
|
||||||
|
"@react-three/fiber": "^8.18.0",
|
||||||
"@sentry/react": "^8.38.0",
|
"@sentry/react": "^8.38.0",
|
||||||
"@turf/area": "^7.2.0",
|
"@turf/area": "^7.2.0",
|
||||||
"@turf/turf": "^7.2.0",
|
"@turf/turf": "^7.2.0",
|
||||||
"@types/classnames": "^2.3.0",
|
"@types/classnames": "^2.3.0",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"crisp-sdk-web": "^1.0.26",
|
"crisp-sdk-web": "^1.0.26",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
|
@ -62,6 +64,7 @@
|
||||||
"react-virtualized-auto-sizer": "^1.0.25",
|
"react-virtualized-auto-sizer": "^1.0.25",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
|
"three": "^0.183.2",
|
||||||
"victory": "^37.3.6",
|
"victory": "^37.3.6",
|
||||||
"weather-icons-react": "^1.2.0"
|
"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": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.13.5",
|
"version": "11.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
|
|
@ -3223,6 +3232,64 @@
|
||||||
"@react-pdf/stylesheet": "^6.1.2"
|
"@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": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||||
|
|
@ -5534,6 +5601,12 @@
|
||||||
"url": "https://opencollective.com/turf"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -5930,6 +6003,15 @@
|
||||||
"@types/viewport-mercator-project": "*"
|
"@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": {
|
"node_modules/@types/react-redux": {
|
||||||
"version": "7.1.34",
|
"version": "7.1.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||||
|
|
@ -5984,6 +6066,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/supercluster": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
|
@ -5993,6 +6081,21 @@
|
||||||
"@types/geojson": "*"
|
"@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": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
|
@ -6010,6 +6113,12 @@
|
||||||
"gl-matrix": "^3.2.0"
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.0",
|
"version": "8.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||||
|
|
@ -6450,6 +6559,12 @@
|
||||||
"url": "https://opencollective.com/vitest"
|
"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": {
|
"node_modules/abs-svg-path": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
"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": "^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": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
|
|
@ -9211,6 +9356,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -9793,6 +9958,27 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/jackspeak": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||||
|
|
@ -10414,6 +10600,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"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"
|
"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": {
|
"node_modules/react-redux": {
|
||||||
"version": "7.2.9",
|
"version": "7.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||||
|
|
@ -11767,6 +11984,21 @@
|
||||||
"react-dom": ">=16.6.0"
|
"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": {
|
"node_modules/react-virtualized-auto-sizer": {
|
||||||
"version": "1.0.26",
|
"version": "1.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
"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"
|
"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": {
|
"node_modules/svg-arc-to-cubic-bezier": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
|
"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": ">=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": {
|
"node_modules/tiny-inflate": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
||||||
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
||||||
"license": "MIT"
|
"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/styles": "^6.1.6",
|
||||||
"@mui/x-date-pickers": "^7.26.0",
|
"@mui/x-date-pickers": "^7.26.0",
|
||||||
"@react-pdf/renderer": "^4.3.0",
|
"@react-pdf/renderer": "^4.3.0",
|
||||||
|
"@react-three/fiber": "^8.18.0",
|
||||||
"@sentry/react": "^8.38.0",
|
"@sentry/react": "^8.38.0",
|
||||||
"@turf/area": "^7.2.0",
|
"@turf/area": "^7.2.0",
|
||||||
"@turf/turf": "^7.2.0",
|
"@turf/turf": "^7.2.0",
|
||||||
"@types/classnames": "^2.3.0",
|
"@types/classnames": "^2.3.0",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"crisp-sdk-web": "^1.0.26",
|
"crisp-sdk-web": "^1.0.26",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
|
@ -74,6 +76,7 @@
|
||||||
"react-virtualized-auto-sizer": "^1.0.25",
|
"react-virtualized-auto-sizer": "^1.0.25",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
|
"three": "^0.183.2",
|
||||||
"victory": "^37.3.6",
|
"victory": "^37.3.6",
|
||||||
"weather-icons-react": "^1.2.0"
|
"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;
|
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 {
|
public key(): string {
|
||||||
return this.settings.key;
|
return this.settings.key;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
Typography,
|
Typography,
|
||||||
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import BinActions from "bin/BinActions";
|
import BinActions from "bin/BinActions";
|
||||||
import BinHistory from "bin/BinHistory";
|
import BinHistory from "bin/BinHistory";
|
||||||
|
|
@ -65,6 +66,7 @@ import ButtonGroup from "common/ButtonGroup";
|
||||||
import BinTransactions from "bin/BinTransactions";
|
import BinTransactions from "bin/BinTransactions";
|
||||||
import { CO2 } from "models/CO2";
|
import { CO2 } from "models/CO2";
|
||||||
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
|
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
|
||||||
|
import Bin3dView from "bin/3dView/Bin3dView";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -773,6 +775,8 @@ export default function Bin(props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [fillPercent, setFillPercent] = useState(0)
|
||||||
|
|
||||||
const desktopView = () => {
|
const desktopView = () => {
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 1 }}>
|
<Card sx={{ padding: 1 }}>
|
||||||
|
|
@ -802,6 +806,17 @@ export default function Bin(props: Props) {
|
||||||
</Box>
|
</Box>
|
||||||
{overview()}
|
{overview()}
|
||||||
{preferences && binComponents(preferences)}
|
{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>
|
||||||
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
|
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
|
||||||
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue