Merge branch 'main' of github.com:specklesystems/speckle-server into gergo/node16
This commit is contained in:
+2
-7
@@ -3,12 +3,7 @@
|
||||
set -e
|
||||
|
||||
DOCKER_IMAGE_TAG=$DOCKER_IMAGE_TAG-$SPECKLE_SERVER_PACKAGE
|
||||
|
||||
IMAGE_VERSION_TAG=$CIRCLE_SHA1
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
IMAGE_VERSION_TAG=$CIRCLE_TAG
|
||||
fi
|
||||
IMAGE_VERSION_TAG=$(./.circleci/get_version.sh)
|
||||
|
||||
docker build --build-arg SPECKLE_SERVER_VERSION=$IMAGE_VERSION_TAG -t $DOCKER_IMAGE_TAG:latest . -f packages/$SPECKLE_SERVER_PACKAGE/Dockerfile
|
||||
docker tag $DOCKER_IMAGE_TAG:latest $DOCKER_IMAGE_TAG:$IMAGE_VERSION_TAG
|
||||
@@ -17,7 +12,7 @@ echo "$DOCKER_REG_PASS" | docker login -u "$DOCKER_REG_USER" --password-stdin $D
|
||||
docker push $DOCKER_IMAGE_TAG:latest
|
||||
docker push $DOCKER_IMAGE_TAG:$IMAGE_VERSION_TAG
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
if [[ "$IMAGE_VERSION_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker tag $DOCKER_IMAGE_TAG:latest $DOCKER_IMAGE_TAG:2
|
||||
docker push $DOCKER_IMAGE_TAG:2
|
||||
fi
|
||||
|
||||
+22
-12
@@ -32,8 +32,8 @@ workflows:
|
||||
context: main-builds
|
||||
filters:
|
||||
branches:
|
||||
only: ci/fileimport
|
||||
|
||||
only: cristi/nonexistent
|
||||
|
||||
jobs:
|
||||
test_server:
|
||||
docker:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
POSTGRES_USER: speckle
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: 'postgres://speckle:speckle@localhost:5432/speckle2_test'
|
||||
DATABASE_URL: "postgres://speckle:speckle@localhost:5432/speckle2_test"
|
||||
PGDATABASE: speckle2_test
|
||||
PGUSER: speckle
|
||||
SESSION_SECRET: 'keyboard cat'
|
||||
@@ -56,15 +56,15 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: 'npm install'
|
||||
working_directory: 'packages/server'
|
||||
- run: 'dockerize -wait tcp://localhost:5432 -timeout 1m'
|
||||
command: "npm install"
|
||||
working_directory: "packages/server"
|
||||
- run: "dockerize -wait tcp://localhost:5432 -timeout 1m"
|
||||
- run:
|
||||
command: 'npm run test:report'
|
||||
working_directory: 'packages/server'
|
||||
command: "npm run test:report"
|
||||
working_directory: "packages/server"
|
||||
- run:
|
||||
command: 'bash <(curl -s https://codecov.io/bash)'
|
||||
working_directory: 'packages/server'
|
||||
command: "bash <(curl -s https://codecov.io/bash)"
|
||||
working_directory: "packages/server"
|
||||
|
||||
- store_test_results:
|
||||
path: packages/server/reports
|
||||
@@ -99,11 +99,20 @@ jobs:
|
||||
- run:
|
||||
name: Build FileImport Service
|
||||
command: env SPECKLE_SERVER_PACKAGE=fileimport-service ./.circleci/build.sh
|
||||
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "18:74:c4:b9:dc:66:b2:66:1d:81:56:0d:0a:87:9b:b1"
|
||||
- run:
|
||||
name: Publish Helm Chart
|
||||
command: ./.circleci/publish_helm_chart.sh
|
||||
|
||||
- run:
|
||||
name: Deploy
|
||||
command: |
|
||||
./.circleci/deploy.sh
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
RELEASE_VERSION=$(./.circleci/get_version.sh)
|
||||
if [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_RELEASE} ./.circleci/deploy_in_new_setup.sh
|
||||
env K8S_CLUSTER=LON1 K8S_NAMESPACE=${K8S_NAMESPACE_LON1_1_RELEASE} ./.circleci/deploy_in_new_setup.sh
|
||||
env K8S_CLUSTER=LON1 K8S_NAMESPACE=${K8S_NAMESPACE_LON1_2_RELEASE} ./.circleci/deploy_in_new_setup.sh
|
||||
@@ -119,7 +128,8 @@ jobs:
|
||||
name: Test deployment
|
||||
command: |
|
||||
./utils/test-deployment/install_prerequisites.sh
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
RELEASE_VERSION=$(./.circleci/get_version.sh)
|
||||
if [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
./utils/test-deployment/run_tests.py https://speckle.xyz
|
||||
./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_RELEASE}
|
||||
else
|
||||
|
||||
+2
-5
@@ -4,12 +4,9 @@ set -e
|
||||
|
||||
|
||||
TARGET_SPECKLE_DEPLOYMENT=$SPECKLE_K8S_DEPLOYMENT
|
||||
IMAGE_VERSION_TAG=$CIRCLE_SHA1
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
TARGET_SPECKLE_DEPLOYMENT=$SPECKLE_K8S_DEPLOYMENT_PROD
|
||||
IMAGE_VERSION_TAG=$CIRCLE_TAG
|
||||
fi
|
||||
IMAGE_VERSION_TAG=$(./.circleci/get_version.sh)
|
||||
|
||||
|
||||
echo "$K8S_CLUSTER_CERTIFICATE" | base64 --decode > k8s_cert.crt
|
||||
|
||||
|
||||
@@ -13,11 +13,8 @@ K8S_SERVER=${!K8S_SERVER_VARIABLE}
|
||||
|
||||
# K8S_NAMESPACE
|
||||
|
||||
IMAGE_VERSION_TAG=$CIRCLE_SHA1
|
||||
IMAGE_VERSION_TAG=$(./.circleci/get_version.sh)
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
IMAGE_VERSION_TAG=$CIRCLE_TAG
|
||||
fi
|
||||
|
||||
echo "$K8S_CLUSTER_CERTIFICATE" | base64 --decode > k8s_cert.crt
|
||||
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
LAST_RELEASE=$(git describe --always --tags `git rev-list --tags` | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
||||
NEXT_RELEASE=$(echo ${LAST_RELEASE} | python -c "parts = input().split('.'); parts[-1] = str(int(parts[-1])+1); print('.'.join(parts))")
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo $CIRCLE_TAG
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$NEXT_RELEASE-alpha.$CIRCLE_BUILD_NUM"
|
||||
exit 0
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
RELEASE_VERSION=$(./.circleci/get_version.sh)
|
||||
|
||||
echo "Releasing Helm Chart version $RELEASE_VERSION"
|
||||
|
||||
git config --global user.email "devops+circleci@speckle.systems"
|
||||
git config --global user.name "CI"
|
||||
|
||||
git clone git@github.com:specklesystems/helm.git ~/helm
|
||||
rm -rf ~/helm/charts/speckle-server
|
||||
cp -r utils/helm/speckle-server ~/helm/charts/speckle-server
|
||||
|
||||
echo 'version: '$RELEASE_VERSION >> ~/helm/charts/speckle-server/Chart.yaml
|
||||
echo 'appVersion: "'$RELEASE_VERSION'"' >> ~/helm/charts/speckle-server/Chart.yaml
|
||||
|
||||
sed -i 's/docker_image_tag: [^\s]*/docker_image_tag: '$RELEASE_VERSION'/g' ~/helm/charts/speckle-server/values.yaml
|
||||
|
||||
cd ~/helm
|
||||
|
||||
git add .
|
||||
git commit -m "CircleCI commit"
|
||||
git push
|
||||
Generated
+349
-183
@@ -9935,54 +9935,66 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
||||
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz",
|
||||
"integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.14.5"
|
||||
"@babel/highlight": "^7.16.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
|
||||
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
|
||||
"version": "7.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
|
||||
"integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
|
||||
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz",
|
||||
"integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.5",
|
||||
"@babel/helper-validator-identifier": "^7.15.7",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@commitlint/execute-rule": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-13.0.0.tgz",
|
||||
"integrity": "sha512-lBz2bJhNAgkkU/rFMAw3XBNujbxhxlaFHY3lfKB/MxpAa+pIfmWB3ig9i1VKe0wCvujk02O0WiMleNaRn2KJqw==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-15.0.0.tgz",
|
||||
"integrity": "sha512-pyE4ApxjbWhb1TXz5vRiGwI2ssdMMgZbaaheZq1/7WC0xRnqnIhE1yUC1D2q20qPtvkZPstTYvMiRVtF+DvjUg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@commitlint/load": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/load/-/load-13.1.0.tgz",
|
||||
"integrity": "sha512-zlZbjJCWnWmBOSwTXis8H7I6pYk6JbDwOCuARA6B9Y/qt2PD+NCo0E/7EuaaFoxjHl+o56QR5QttuMBrf+BJzg==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/load/-/load-15.0.0.tgz",
|
||||
"integrity": "sha512-Ak1YPeOhvxmY3ioe0o6m1yLGvUAYb4BdfGgShU8jiTCmU3Mnmms0Xh/kfQz8AybhezCC3AmVTyBLaBZxOHR8kg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@commitlint/execute-rule": "^13.0.0",
|
||||
"@commitlint/resolve-extends": "^13.0.0",
|
||||
"@commitlint/types": "^13.1.0",
|
||||
"@commitlint/execute-rule": "^15.0.0",
|
||||
"@commitlint/resolve-extends": "^15.0.0",
|
||||
"@commitlint/types": "^15.0.0",
|
||||
"@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2",
|
||||
"chalk": "^4.0.0",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"lodash": "^4.17.19",
|
||||
"resolve-from": "^5.0.0"
|
||||
"resolve-from": "^5.0.0",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -9993,13 +10005,47 @@
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@commitlint/resolve-extends": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-13.0.0.tgz",
|
||||
"integrity": "sha512-1SyaE+UOsYTkQlTPUOoj4NwxQhGFtYildVS/d0TJuK8a9uAJLw7bhCLH2PEeH5cC2D1do4Eqhx/3bLDrSLH3hg==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-15.0.0.tgz",
|
||||
"integrity": "sha512-7apfRJjgJsKja7lHsPfEFixKjA/fk/UeD3owkOw1174yYu4u8xBDLSeU3IinGPdMuF9m245eX8wo7vLUy+EBSg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -10010,15 +10056,25 @@
|
||||
}
|
||||
},
|
||||
"@commitlint/types": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/types/-/types-13.1.0.tgz",
|
||||
"integrity": "sha512-zcVjuT+OfKt8h91vhBxt05RMcTGEx6DM7Q9QZeuMbXFk6xgbsSEDMMapbJPA1bCZ81fa/1OQBijSYPrKvtt06g==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@commitlint/types/-/types-15.0.0.tgz",
|
||||
"integrity": "sha512-OMSLX+QJnyNoTwws54ULv9sOvuw9GdVezln76oyUd4YbMMJyaav62aSXDuCdWyL2sm9hTkSzyEi52PNaIj/vqw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -10029,9 +10085,56 @@
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@endemolshinegroup/cosmiconfig-typescript-loader": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz",
|
||||
"integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lodash.get": "^4",
|
||||
"make-error": "^1",
|
||||
"ts-node": "^9",
|
||||
"tslib": "^2"
|
||||
}
|
||||
},
|
||||
"@evocateur/libnpmaccess": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz",
|
||||
@@ -11004,6 +11107,43 @@
|
||||
"@octokit/types": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"@octokit/core": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz",
|
||||
"integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@octokit/auth-token": "^2.4.4",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@octokit/request": "^5.6.0",
|
||||
"@octokit/request-error": "^2.0.5",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/request-error": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
|
||||
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@octokit/types": "^6.0.3",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
|
||||
@@ -11023,10 +11163,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
|
||||
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@octokit/request": "^5.6.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/openapi-types": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.1.0.tgz",
|
||||
"integrity": "sha512-dWZfYvCCdjZzDYA3lIAMF72Q0jld8xidqCq5Ryw09eBJXZdcM6he0vWBTvw/b5UnGYqexxOyHWgfrsTlUJL3Gw==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
|
||||
"integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==",
|
||||
"dev": true
|
||||
},
|
||||
"@octokit/plugin-enterprise-rest": {
|
||||
@@ -11163,18 +11324,18 @@
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "6.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.33.0.tgz",
|
||||
"integrity": "sha512-0zffZ048M0UhthyPXQHLz4038Ak46nMWZXkzlXvXB/M/L1jYPBceq4iZj4qjKVrvveaJrrgKdJ9+3yUuITfcCw==",
|
||||
"version": "6.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
|
||||
"integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@octokit/openapi-types": "^11.1.0"
|
||||
"@octokit/openapi-types": "^11.2.0"
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz",
|
||||
"integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/minimatch": "*",
|
||||
@@ -11194,9 +11355,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz",
|
||||
"integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==",
|
||||
"version": "16.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
|
||||
"integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
@@ -11266,19 +11427,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"any-promise": {
|
||||
@@ -11335,6 +11495,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -11414,9 +11581,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
@@ -11693,47 +11860,6 @@
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"chardet": {
|
||||
@@ -11918,21 +12044,19 @@
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"columnify": {
|
||||
"version": "1.5.4",
|
||||
@@ -12159,9 +12283,9 @@
|
||||
}
|
||||
},
|
||||
"conventional-commits-parser": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.2.tgz",
|
||||
"integrity": "sha512-Jr9KAKgqAkwXMRHjxDwO/zOCDKod1XdAESHAGuJX38iZ7ZzVti/tvVoysO0suMsdAObp9NQ2rHSsSbnAqZ5f5g==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.3.tgz",
|
||||
"integrity": "sha512-YyRDR7On9H07ICFpRm/igcdjIqebXbvf4Cff+Pf0BrBys1i1EOzx9iFXNlAbdrLAR8jf7bkUYkDAr8pEy0q4Pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-text-path": "^1.0.1",
|
||||
@@ -12333,6 +12457,13 @@
|
||||
"yaml": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
@@ -12527,6 +12658,13 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
|
||||
@@ -14150,11 +14288,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.2",
|
||||
@@ -14508,9 +14645,9 @@
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz",
|
||||
"integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
|
||||
"integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
@@ -14590,9 +14727,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -14868,9 +15005,9 @@
|
||||
}
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true
|
||||
},
|
||||
"load-json-file": {
|
||||
@@ -15035,6 +15172,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"make-fetch-happen": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz",
|
||||
@@ -15267,18 +15411,18 @@
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
|
||||
"integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==",
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
|
||||
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.33",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz",
|
||||
"integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==",
|
||||
"version": "2.1.34",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
|
||||
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.50.0"
|
||||
"mime-db": "1.51.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
@@ -15533,9 +15677,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
|
||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
@@ -16726,6 +16870,14 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
@@ -16835,9 +16987,9 @@
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
|
||||
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
|
||||
"dev": true
|
||||
},
|
||||
"slash": {
|
||||
@@ -17073,6 +17225,17 @@
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
@@ -17106,9 +17269,9 @@
|
||||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz",
|
||||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
|
||||
"integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
|
||||
"dev": true
|
||||
},
|
||||
"split": {
|
||||
@@ -17295,6 +17458,12 @@
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
@@ -17333,14 +17502,6 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-bom": {
|
||||
@@ -17382,13 +17543,12 @@
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
@@ -17549,11 +17709,27 @@
|
||||
"integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
|
||||
"integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"source-map-support": "^0.5.17",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
@@ -17582,10 +17758,17 @@
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.2.tgz",
|
||||
"integrity": "sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==",
|
||||
"version": "3.14.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz",
|
||||
"integrity": "sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -17839,12 +18022,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"windows-release": {
|
||||
@@ -17879,30 +18062,6 @@
|
||||
"strip-ansi": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
@@ -18081,6 +18240,13 @@
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +121,8 @@ export async function refreshToken() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmailValid(email) {
|
||||
const emailValidator = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return emailValidator.test(email)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<div
|
||||
style="cursor: pointer; min-height: 33px; line-height: 33px"
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-3' : 'lighten-3'} rounded-xl px-2`"
|
||||
:class="`${$vuetify.theme.dark ? 'black' : 'white'} rounded-xl px-2`"
|
||||
>
|
||||
<v-icon small>mdi-call-received</v-icon>
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
<!-- COMMITS -->
|
||||
<v-card
|
||||
v-else-if="lastActivity.resourceType === 'commit'"
|
||||
v-else-if="lastActivity.resourceType === 'commit' && commit"
|
||||
class="activity-card"
|
||||
:flat="$vuetify.theme.dark"
|
||||
>
|
||||
@@ -208,7 +208,9 @@
|
||||
small
|
||||
color="primary"
|
||||
>
|
||||
<v-icon small class="float-left" light>mdi-source-branch</v-icon>
|
||||
<v-icon v-if="commit" small class="float-left" light>
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
{{ commit.branchName }}
|
||||
</v-chip>
|
||||
<span v-if="lastActivity.actionType === 'commit_create'">
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
:height="height"
|
||||
:class="`${color ? '' : 'grasycale-img'} preview-img`"
|
||||
:src="currentPreviewImg"
|
||||
:gradient="`to top right, ${$vuetify.theme.dark ? 'rgba(100,115,201,.33), rgba(25,32,72,.7)' : 'rgba(100,115,231,.15), rgba(25,32,72,.05)'}`"
|
||||
:gradient="`to top right, ${
|
||||
$vuetify.theme.dark
|
||||
? 'rgba(100,115,201,.33), rgba(25,32,72,.7)'
|
||||
: 'rgba(100,115,231,.1), rgba(25,32,72,.05)'
|
||||
}`"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
@@ -38,8 +42,12 @@ export default {
|
||||
? { Authorization: `Bearer ${localStorage.getItem('AuthToken')}` }
|
||||
: {}
|
||||
})
|
||||
const blob = await res.blob()
|
||||
this.currentPreviewImg = URL.createObjectURL(blob)
|
||||
try {
|
||||
const blob = await res.blob()
|
||||
this.currentPreviewImg = URL.createObjectURL(blob)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,13 @@ export default {
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
opacity: 0.8;
|
||||
object-fit: cover;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-img:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stream-link a {
|
||||
|
||||
@@ -44,13 +44,20 @@
|
||||
:class="`${fullScreen ? 'fullscreen' : ''} ${darkMode ? 'dark' : ''}`"
|
||||
>
|
||||
<v-fade-transition>
|
||||
<div v-show="!hasLoadedModel" class="overlay cover-all">
|
||||
<!-- <div v-show="!hasLoadedModel" class="overlay cover-all"> -->
|
||||
<div v-show="loadProgress < 99" class="overlay cover-all">
|
||||
<transition name="fade">
|
||||
<div v-show="hasImg" ref="cover" class="overlay-abs bg-img"></div>
|
||||
<div
|
||||
v-show="hasImg"
|
||||
ref="cover"
|
||||
class="overlay-abs bg-img"
|
||||
:style="`opacity: ${100 - loadProgress}`"
|
||||
></div>
|
||||
</transition>
|
||||
<div class="overlay-abs radial-bg"></div>
|
||||
<div class="overlay-abs" style="pointer-events: none">
|
||||
<v-btn
|
||||
v-show="loadProgress === 0"
|
||||
color="primary"
|
||||
class="vertical-center"
|
||||
style="pointer-events: all"
|
||||
@@ -88,6 +95,15 @@
|
||||
<v-icon v-else small>mdi-cube</v-icon>
|
||||
({{ selectedObjects.length }})
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="`Toggle between perspective or ortho camera.`"
|
||||
small
|
||||
:color="`${perspectiveMode ? 'blue' : ''}`"
|
||||
@click="toggleCamera()"
|
||||
>
|
||||
<v-icon small>mdi-perspective-less</v-icon>
|
||||
<!-- <span class="caption">Perspective</span> -->
|
||||
</v-btn>
|
||||
<v-menu top close-on-click offset-y style="z-index: 100">
|
||||
<template #activator="{ on: onMenu, attrs: menuAttrs }">
|
||||
<v-tooltip top>
|
||||
@@ -258,7 +274,8 @@ export default {
|
||||
selectedObjects: [],
|
||||
showObjectDetails: false,
|
||||
hasImg: false,
|
||||
namedViews: []
|
||||
namedViews: [],
|
||||
perspectiveMode: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -311,6 +328,7 @@ export default {
|
||||
// - juggle the container div out of this component's dom when the component is managed out by vue
|
||||
// - juggle the container div back in of this component's dom when it's back.
|
||||
let renderDomElement = document.getElementById('renderer')
|
||||
this.hasLoadedModel = false
|
||||
|
||||
if (!renderDomElement) {
|
||||
renderDomElement = document.createElement('div')
|
||||
@@ -322,25 +340,26 @@ export default {
|
||||
this.$refs.rendererparent.appendChild(renderDomElement)
|
||||
|
||||
if (!window.__viewer) {
|
||||
window.__viewer = new Viewer({ container: renderDomElement })
|
||||
window.__viewer = new Viewer({ container: renderDomElement, showStats: false })
|
||||
}
|
||||
|
||||
window.__viewer.onWindowResize()
|
||||
window.__viewer.cameraHandler.onWindowResize()
|
||||
|
||||
if (window.__viewerLastLoadedUrl !== this.objectUrl) {
|
||||
window.__viewer.sceneManager.removeAllObjects()
|
||||
await this.getPreviewImage()
|
||||
if (window.__viewerLastLoadedUrl && !this.hasLoadedModel) {
|
||||
// console.log('unloading')
|
||||
await window.__viewer.unloadAll()
|
||||
}
|
||||
window.__viewerLastLoadedUrl = null
|
||||
this.getPreviewImage().then().catch()
|
||||
} else {
|
||||
this.hasLoadedModel = true
|
||||
this.loadProgress = 100
|
||||
this.setupEvents()
|
||||
}
|
||||
if (this.$route.query.embed) {
|
||||
this.fullScreen = true
|
||||
//TODO: Remove overflow from window
|
||||
document.body.classList.add('no-scrollbar')
|
||||
}
|
||||
|
||||
if (window.__viewer.cameraHandler.activeCam === 'ortho') this.perspectiveMode = false
|
||||
},
|
||||
beforeDestroy() {
|
||||
// NOTE: here's where we juggle the container div out, and do cleanup on the
|
||||
@@ -368,6 +387,10 @@ export default {
|
||||
if (this.$refs.cover) this.$refs.cover.style.backgroundImage = `url('${imgUrl}')`
|
||||
this.hasImg = true
|
||||
},
|
||||
toggleCamera() {
|
||||
window.__viewer.toggleCameraProjection()
|
||||
this.perspectiveMode = !this.perspectiveMode
|
||||
},
|
||||
zoomEx() {
|
||||
window.__viewer.interactions.zoomExtents()
|
||||
},
|
||||
@@ -378,7 +401,7 @@ export default {
|
||||
window.__viewer.interactions.setView(id)
|
||||
},
|
||||
sectionToggle() {
|
||||
window.__viewer.interactions.toggleSectionBox()
|
||||
window.__viewer.toggleSectionBox()
|
||||
},
|
||||
setupEvents() {
|
||||
window.__viewer.on('load-warning', ({ message }) => {
|
||||
@@ -413,7 +436,7 @@ export default {
|
||||
this.setupEvents()
|
||||
},
|
||||
unloadData() {
|
||||
window.__viewer.sceneManager.removeAllObjects()
|
||||
window.__viewer.unloadAll()
|
||||
this.hasLoadedModel = false
|
||||
this.loadProgress = 0
|
||||
this.namedViews.splice(0, this.namedViews.length)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<v-row class="pa-0 ma-0">
|
||||
{{ item.name }}
|
||||
<v-spacer></v-spacer>
|
||||
<span class="streamid">{{ item.id }}</span>
|
||||
<span class="streamId">{{ item.id }}</span>
|
||||
</v-row>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'ServerInviteDialog',
|
||||
@@ -53,7 +54,7 @@ export default {
|
||||
validation: {
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
(v) => isEmailValid(v) || 'E-mail must be valid'
|
||||
],
|
||||
messageRules: [
|
||||
(v) => {
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'StreamInviteDialog',
|
||||
@@ -73,7 +74,7 @@ export default {
|
||||
validation: {
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
(v) => isEmailValid(v) || 'E-mail must be valid'
|
||||
],
|
||||
messageRules: [
|
||||
(v) => {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
query Stream($streamId: String!, $branchName: String!) {
|
||||
query Stream($streamId: String!, $branchName: String!, $cursor: String) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
branch(name: $branchName) {
|
||||
id
|
||||
name
|
||||
description
|
||||
commits {
|
||||
commits(cursor: $cursor) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query Stream($streamid: String!, $id: String!) {
|
||||
stream(id: $streamid) {
|
||||
query Stream($streamId: String!, $id: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
@@ -13,14 +13,14 @@ query Stream($streamid: String!, $id: String!) {
|
||||
createdAt
|
||||
branchName
|
||||
sourceApplication
|
||||
activity(actionType: "commit_receive", limit: 200) {
|
||||
items {
|
||||
info
|
||||
time
|
||||
userId
|
||||
message
|
||||
}
|
||||
}
|
||||
# activity(actionType: "commit_receive", limit: 200) {
|
||||
# items {
|
||||
# info
|
||||
# time
|
||||
# userId
|
||||
# message
|
||||
# }
|
||||
# }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,11 +226,12 @@ const routes = [
|
||||
component: () => import('@/views/admin/AdminUsers.vue'),
|
||||
props: (route) => ({ ...route.query, ...route.props })
|
||||
},
|
||||
// {
|
||||
// name: 'Admin | Streams',
|
||||
// path: 'streams',
|
||||
// component: () => import('@/views/admin/AdminStreams.vue')
|
||||
// },
|
||||
{
|
||||
name: 'Admin | Streams',
|
||||
path: 'streams',
|
||||
component: () => import('@/views/admin/AdminStreams.vue'),
|
||||
props: (route) => ({ ...route.query, ...route.props })
|
||||
},
|
||||
{
|
||||
name: 'Admin | Settings',
|
||||
path: 'settings',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$heading-font-family: 'Space Grotesk';
|
||||
// $heading-font-family: 'Space Grotesk';
|
||||
|
||||
$primary-base: var(--v-primary-base);
|
||||
$primary-darken: var(--v-secondary-base);
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<v-list-item-subtitle class="caption">Edit server user details.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/invites">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-account-multiple-plus-outline</v-icon>
|
||||
@@ -58,6 +59,16 @@
|
||||
<v-list-item-subtitle class="caption">Manage server invitations.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/streams">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-blur</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Streams</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Manage streams.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
|
||||
@@ -87,7 +87,10 @@
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
import StreamSearchBar from '@/components/SearchBar'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'AdminInvites',
|
||||
components: { StreamSearchBar },
|
||||
data() {
|
||||
return {
|
||||
@@ -155,10 +158,6 @@ export default {
|
||||
remove(item) {
|
||||
this.chips.splice(this.chips.indexOf(item), 1)
|
||||
},
|
||||
validateEmail(email) {
|
||||
const re = /^\S+@\S+\.\S+$/
|
||||
return re.test(email)
|
||||
},
|
||||
keyDownHandler(val) {
|
||||
if (!(val.key === ' ' || val.key === ',' || val.key === 'Enter')) return
|
||||
this.validateAndCreateChips()
|
||||
@@ -168,7 +167,7 @@ export default {
|
||||
if (!this.emails || this.emails === '') return
|
||||
let splitEmails = this.emails.split(/[ ,]+/)
|
||||
for (let email of splitEmails) {
|
||||
let valid = this.validateEmail(email) && this.chips.indexOf(email) === -1
|
||||
let valid = isEmailValid(email) && this.chips.indexOf(email) === -1
|
||||
if (valid) {
|
||||
this.chips.push(email)
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,349 @@
|
||||
<template lang="html">
|
||||
<v-container>
|
||||
<p></p>
|
||||
</v-container>
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
Stream Administration
|
||||
<span v-if="adminStreams">
|
||||
({{ adminStreams.items.length }} of {{ adminStreams.totalCount }} streams)
|
||||
</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-subtitle>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
class="mx-4 mt-4"
|
||||
:prepend-inner-icon="'mdi-magnify'"
|
||||
:loading="$apollo.loading"
|
||||
label="Search streams"
|
||||
type="text"
|
||||
single-line
|
||||
clearable
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
></v-text-field>
|
||||
<div class="mx-4">
|
||||
<div class="d-flex">
|
||||
<v-select
|
||||
v-model="queryLimit"
|
||||
:items="[10, 25, 50]"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="streams per page"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="streamVisibility"
|
||||
:items="['all', 'public', 'private']"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="visibility"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="orderColumn"
|
||||
:items="['updatedAt', 'size']"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="order streams by"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-btn
|
||||
fab
|
||||
elevation="0"
|
||||
@click="orderDirection = orderDirection === 'desc' ? 'asc' : 'desc'"
|
||||
>
|
||||
<v-icon>mdi-arrow-expand-{{ orderDirection === 'desc' ? 'down' : 'up' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-subtitle>
|
||||
<v-list v-if="!$apollo.loading" rounded>
|
||||
<v-list-item-group class="ml-6">
|
||||
<v-list-item v-for="stream in adminStreams.items" :key="stream.id" two-line>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<router-link
|
||||
class="text-h6 text-decoration-none"
|
||||
:to="`/streams/${stream.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ stream.description ? stream.description : 'Stream has no description' }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="mt-1">
|
||||
<v-chip small>
|
||||
<v-icon small>
|
||||
{{ stream.isPublic ? 'mdi-lock-open-variant-outline' : 'mdi-lock-outline' }}
|
||||
</v-icon>
|
||||
</v-chip>
|
||||
<v-chip class="mx-2" small>
|
||||
Last activity
|
||||
<timeago :datetime="stream.updatedAt" class="ml-1 mr-"></timeago>
|
||||
</v-chip>
|
||||
<v-chip small class="mr-2 pr-5">
|
||||
<v-icon small class="mr-2">mdi-source-branch</v-icon>
|
||||
{{ stream.branches.totalCount }}
|
||||
</v-chip>
|
||||
<v-chip small class="mr-2">
|
||||
Data usage {{ `${(stream.size ? stream.size / 1048576 : 0.0).toFixed(2)} MB` }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<div
|
||||
style="cursor: pointer; min-height: 33px; line-height: 33px"
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-3' : 'lighten-3'} rounded-xl px-2`"
|
||||
>
|
||||
<user-avatar
|
||||
v-for="user in stream.collaborators.slice(0, 3)"
|
||||
v-show="$vuetify.breakpoint.smAndUp"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:show-hover="false"
|
||||
:size="25"
|
||||
></user-avatar>
|
||||
|
||||
<v-avatar
|
||||
v-if="stream.collaborators.length > 3"
|
||||
v-show="$vuetify.breakpoint.smAndUp"
|
||||
class="ml-1"
|
||||
size="25"
|
||||
color="primary"
|
||||
>
|
||||
<span class="white--text caption">+{{ stream.collaborators.length - 3 }}</span>
|
||||
</v-avatar>
|
||||
<v-avatar v-show="$vuetify.breakpoint.xsOnly" class="ml-1" size="25" color="primary">
|
||||
<span class="white--text caption">{{ stream.collaborators.length }}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</v-list-item-action>
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="initiateDeleteStreams(stream)">
|
||||
<v-icon>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
<div class="text-center">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="numberOfPages"
|
||||
:total-visible="7"
|
||||
circle
|
||||
></v-pagination>
|
||||
</div>
|
||||
</v-list>
|
||||
<v-skeleton-loader v-else class="mx-auto" type="card"></v-skeleton-loader>
|
||||
<v-dialog v-model="showDeleteDialog" persistent max-width="600px">
|
||||
<v-card v-if="showDeleteDialog">
|
||||
<v-toolbar flat class="mb-6">
|
||||
<v-toolbar-title>Confirm stream deletion</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert type="error">
|
||||
Confirm deletion of
|
||||
<b>{{ manipulatedStream.name }}</b>
|
||||
stream from the server.
|
||||
<br />
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="showDeleteDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="deleteStreams">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default {
|
||||
name: 'AdminStreams'
|
||||
name: 'AdminStreams',
|
||||
components: { UserAvatar },
|
||||
props: {
|
||||
page: { type: [Number, String], required: false, default: null },
|
||||
limit: { type: [Number, String], required: false, default: 10 },
|
||||
q: { type: String, required: false, default: null },
|
||||
orderBy: { type: String, required: false, default: 'updatedAt,desc' },
|
||||
visibility: { type: String, required: false, default: 'all' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteDialog: false,
|
||||
manipulatedStream: null,
|
||||
adminStreams: {
|
||||
items: [],
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
numberOfPages() {
|
||||
return Math.ceil(this.adminStreams.totalCount / this.queryLimit)
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.page ? parseInt(this.page) : 1
|
||||
},
|
||||
set(page) {
|
||||
this.navigateNext({ page })
|
||||
}
|
||||
},
|
||||
queryLimit: {
|
||||
get() {
|
||||
return this.limit ? parseInt(this.limit) : 10
|
||||
},
|
||||
set(limit) {
|
||||
this.navigateNext({ limit })
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
get() {
|
||||
return this.q
|
||||
},
|
||||
set: debounce(function (q) {
|
||||
this.navigateNext({ q })
|
||||
}, 1000)
|
||||
},
|
||||
orderColumn: {
|
||||
get() {
|
||||
let [column] = this.orderBy.split(',')
|
||||
return column
|
||||
},
|
||||
set(column) {
|
||||
let direction = this.orderBy.split(',').pop()
|
||||
let orderBy = `${column},${direction}`
|
||||
this.navigateNext({ orderBy })
|
||||
}
|
||||
},
|
||||
orderDirection: {
|
||||
get() {
|
||||
return this.orderBy.split(',').pop()
|
||||
},
|
||||
set(direction) {
|
||||
let [column] = this.orderBy.split(',')
|
||||
let orderBy = `${column},${direction}`
|
||||
this.navigateNext({ orderBy })
|
||||
}
|
||||
},
|
||||
streamVisibility: {
|
||||
get() {
|
||||
return this.visibility
|
||||
},
|
||||
set(visibility) {
|
||||
this.navigateNext({ visibility })
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initiateDeleteStreams(stream) {
|
||||
this.showDeleteDialog = true
|
||||
this.manipulatedStream = stream
|
||||
},
|
||||
async deleteStreams() {
|
||||
let ids = [this.manipulatedStream.id]
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($ids: [String!]) {
|
||||
streamsDelete(ids: $ids)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
ids: ids
|
||||
},
|
||||
update: () => {
|
||||
this.$apollo.queries.adminStreams.refetch()
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
this.manipulatedStream = null
|
||||
this.showDeleteDialog = false
|
||||
},
|
||||
navigateNext(routeParams) {
|
||||
this.$router.push(this._prepareRoute(routeParams))
|
||||
},
|
||||
_prepareRoute(routeParams) {
|
||||
let newRoute = 'streams'
|
||||
let newParams = { ...this.$props }
|
||||
|
||||
for (let key in routeParams) {
|
||||
newParams[key] = routeParams[key]
|
||||
}
|
||||
|
||||
Object.keys(newParams).forEach((attr, index) => {
|
||||
index === 0 ? (newRoute += '?') : (newRoute += '&')
|
||||
if (newParams[attr]) newRoute += `${attr}=${newParams[attr]}`
|
||||
})
|
||||
return newRoute
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
adminStreams: {
|
||||
query: gql`
|
||||
query Streams(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$orderBy: String
|
||||
$query: String
|
||||
$visibility: String
|
||||
) {
|
||||
adminStreams(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
orderBy: $orderBy
|
||||
query: $query
|
||||
visibility: $visibility
|
||||
) {
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
size
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
offset: (this.currentPage - 1) * this.queryLimit,
|
||||
limit: this.queryLimit,
|
||||
query: this.searchQuery,
|
||||
orderBy: this.orderBy,
|
||||
visibility: this.streamVisibility
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ user.name }}
|
||||
<router-link class="text-h6" :to="`/profile/${user.id}`" target="_blank">
|
||||
{{ user.name }}
|
||||
</router-link>
|
||||
</v-list-item-title>
|
||||
|
||||
<span class="caption">
|
||||
@@ -141,8 +143,8 @@ export default {
|
||||
items: [],
|
||||
totalCount: 0
|
||||
},
|
||||
currentPage: 1,
|
||||
searchQuery: null,
|
||||
// currentPage: 1,
|
||||
// searchQuery: null,
|
||||
showConfirmDialog: false,
|
||||
showDeleteDialog: false,
|
||||
manipulatedUser: null,
|
||||
@@ -153,6 +155,22 @@ export default {
|
||||
queryLimit() {
|
||||
return parseInt(this.limit)
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return parseInt(this.page)
|
||||
},
|
||||
set(newPage) {
|
||||
this.paginateNext(newPage)
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
get() {
|
||||
return this.q
|
||||
},
|
||||
set: debounce(function (q) {
|
||||
this.applySearch(q)
|
||||
}, 500)
|
||||
},
|
||||
queryOffset() {
|
||||
return (this.page - 1) * this.queryLimit
|
||||
},
|
||||
@@ -167,21 +185,12 @@ export default {
|
||||
return roleItems
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentPage: function (newPage) {
|
||||
this.paginateNext(newPage)
|
||||
},
|
||||
searchQuery: debounce(function (newQuery) {
|
||||
this.applySearch(newQuery)
|
||||
}, 1000)
|
||||
},
|
||||
methods: {
|
||||
initiateDeleteUser(user) {
|
||||
this.showDeleteDialog = true
|
||||
this.manipulatedUser = user
|
||||
},
|
||||
async deleteUser(user) {
|
||||
console.log('deleting', user.email)
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($userEmail: String!) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="`${serverInfo.inviteOnly ? 'border: 2px solid #047EFB' : ''}`"
|
||||
rounded="lg"
|
||||
>
|
||||
<div v-show="serverInfo.inviteOnly" class="caption text-center" style="background: #047EFB">
|
||||
<div v-show="serverInfo.inviteOnly" class="caption text-center" style="background: #047efb">
|
||||
<v-icon small>mdi-shield-alert-outline</v-icon>
|
||||
This Speckle server is invite only.
|
||||
</div>
|
||||
@@ -81,7 +81,8 @@
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import crs from 'crypto-random-string'
|
||||
import Strategies from '../../components/auth/Strategies'
|
||||
import Strategies from '@/components/auth/Strategies'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
@@ -119,7 +120,7 @@ export default {
|
||||
passwordRules: [(v) => !!v || 'Required'],
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
(v) => isEmailValid(v) || 'E-mail must be valid'
|
||||
]
|
||||
},
|
||||
registrationError: false,
|
||||
|
||||
@@ -160,7 +160,8 @@ import gql from 'graphql-tag'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
|
||||
import Strategies from '../../components/auth/Strategies'
|
||||
import Strategies from '@/components/auth/Strategies'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'Registration',
|
||||
@@ -213,7 +214,7 @@ export default {
|
||||
],
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
(v) => isEmailValid(v) || 'E-mail must be valid'
|
||||
]
|
||||
},
|
||||
passwordStrength: 1,
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
// import debounce from 'lodash.debounce'
|
||||
// import crs from 'crypto-random-string'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'ResetPasswordRequest',
|
||||
@@ -75,7 +74,7 @@ export default {
|
||||
validation: {
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
(v) => isEmailValid(v) || 'E-mail must be valid'
|
||||
]
|
||||
},
|
||||
success: false,
|
||||
|
||||
@@ -38,27 +38,120 @@
|
||||
</v-btn>
|
||||
</portal>
|
||||
<v-row no-gutters>
|
||||
<v-col v-if="$apollo.loading">
|
||||
<v-skeleton-loader type="article, article"></v-skeleton-loader>
|
||||
<v-col v-if="stream && stream.branch" cols="12" class="pa-4">
|
||||
<v-row v-if="stream.branch.commits.items.length > 0">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<router-link :to="`/streams/${streamId}/commits/${latestCommit.id}`">
|
||||
<preview-image
|
||||
:height="320"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${latestCommit.id}`"
|
||||
></preview-image>
|
||||
</router-link>
|
||||
<div style="position: absolute; top: 10px; right: 20px">
|
||||
<commit-received-receipts :stream-id="streamId" :commit-id="latestCommit.id" />
|
||||
</div>
|
||||
<div style="position: absolute; top: 10px; left: 12px">
|
||||
<source-app-avatar :application-name="latestCommit.sourceApplication" />
|
||||
</div>
|
||||
<v-list-item class="elevation-0">
|
||||
<v-list-item-icon class="">
|
||||
<user-avatar
|
||||
:id="latestCommit.authorId"
|
||||
:avatar="latestCommit.authorAvatar"
|
||||
:name="latestCommit.authorName"
|
||||
:size="40"
|
||||
/>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<router-link
|
||||
class="text-decoration-none"
|
||||
:to="`/streams/${streamId}/commits/${latestCommit.id}`"
|
||||
>
|
||||
<v-list-item-title class="mt-0 pt-0 py-1">
|
||||
{{ latestCommit.message }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ latestCommit.authorName }}</b>
|
||||
|
||||
<timeago :datetime="latestCommit.createdAt"></timeago>
|
||||
<!-- ({{ commitDate }}) -->
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>Older Commits</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-tooltip="`View as a ${listMode ? 'gallery' : 'list'}`"
|
||||
icon
|
||||
@click="listMode = !listMode"
|
||||
>
|
||||
<v-icon v-if="listMode">mdi-view-dashboard</v-icon>
|
||||
<v-icon v-else>mdi-view-list</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!listMode">
|
||||
<v-col
|
||||
v-for="commit in allPreviousCommits"
|
||||
:key="commit.id + 'card'"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
xl="3"
|
||||
>
|
||||
<v-card>
|
||||
<router-link :to="`/streams/${streamId}/commits/${commit.id}`">
|
||||
<preview-image
|
||||
:height="180"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${commit.id}`"
|
||||
></preview-image>
|
||||
</router-link>
|
||||
<div style="position: absolute; top: 10px; right: 20px">
|
||||
<commit-received-receipts :stream-id="streamId" :commit-id="commit.id" />
|
||||
</div>
|
||||
<div style="position: absolute; top: 10px; left: 12px">
|
||||
<source-app-avatar :application-name="commit.sourceApplication" />
|
||||
</div>
|
||||
<v-list-item class="elevation-0">
|
||||
<v-list-item-icon class="">
|
||||
<user-avatar
|
||||
:id="commit.authorId"
|
||||
:avatar="commit.authorAvatar"
|
||||
:name="commit.authorName"
|
||||
:size="40"
|
||||
/>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<router-link
|
||||
class="text-decoration-none"
|
||||
:to="`/streams/${streamId}/commits/${commit.id}`"
|
||||
>
|
||||
<v-list-item-title class="mt-0 pt-0 py-1">
|
||||
{{ commit.message }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ commit.authorName }}</b>
|
||||
|
||||
<timeago :datetime="commit.createdAt"></timeago>
|
||||
<!-- ({{ commitDate }}) -->
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col v-else-if="stream && stream.branch" cols="12" class="pa-0 ma-0">
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<div v-if="latestCommitObjectUrl" style="height: 60vh">
|
||||
<renderer :object-url="latestCommitObjectUrl" show-selection-helper />
|
||||
</div>
|
||||
|
||||
<v-col v-if="stream && stream.branch && listMode" cols="12" class="pa-0 ma-0">
|
||||
<v-list v-if="stream.branch.commits.items.length > 0" class="pa-0 ma-0">
|
||||
<list-item-commit
|
||||
:commit="latestCommit"
|
||||
:stream-id="streamId"
|
||||
class="elevation-3"
|
||||
show-received-receipts
|
||||
></list-item-commit>
|
||||
<v-divider></v-divider>
|
||||
<v-subheader class="ml-5">
|
||||
{{ stream.branch.commits.items.length > 1 ? 'Older commits:' : 'No other commits.' }}
|
||||
</v-subheader>
|
||||
<list-item-commit
|
||||
v-for="item in allPreviousCommits"
|
||||
:key="item.id"
|
||||
@@ -66,11 +159,40 @@
|
||||
:stream-id="streamId"
|
||||
show-received-receipts
|
||||
></list-item-commit>
|
||||
|
||||
<!-- TODO: pagination -->
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<infinite-loading
|
||||
v-if="stream && stream.branch.commits.totalCount !== 0"
|
||||
spinner="waveDots"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">
|
||||
<v-col>
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>
|
||||
You've reached the end - this branch has no more commits.
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</div>
|
||||
<div slot="no-results">
|
||||
<v-col>
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>
|
||||
You've reached the end - this branch has no more commits.
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</div>
|
||||
</infinite-loading>
|
||||
|
||||
<!-- <v-col v-if="$apollo.loading">
|
||||
<v-skeleton-loader type="article, article"></v-skeleton-loader>
|
||||
</v-col> -->
|
||||
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<no-data-placeholder
|
||||
v-if="!$apollo.loading && stream.branch && stream.branch.commits.totalCount === 0"
|
||||
>
|
||||
@@ -92,16 +214,22 @@ import branchQuery from '@/graphql/branch.gql'
|
||||
export default {
|
||||
name: 'Branch',
|
||||
components: {
|
||||
InfiniteLoading: () => import('vue-infinite-loading'),
|
||||
ListItemCommit: () => import('@/components/ListItemCommit'),
|
||||
BranchEditDialog: () => import('@/components/dialogs/BranchEditDialog'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
|
||||
PreviewImage: () => import('@/components/PreviewImage'),
|
||||
CommitReceivedReceipts: () => import('@/components/CommitReceivedReceipts'),
|
||||
UserAvatar: () => import('@/components/UserAvatar'),
|
||||
SourceAppAvatar: () => import('@/components/SourceAppAvatar'),
|
||||
Renderer: () => import('@/components/Renderer')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogEdit: false,
|
||||
error: null
|
||||
error: null,
|
||||
listMode: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -185,6 +313,39 @@ export default {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
})
|
||||
},
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.stream.fetchMore({
|
||||
variables: {
|
||||
cursor: this.stream.branch.commits.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.stream.branch.commits.items
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
stream: {
|
||||
__typename: previousResult.stream.__typename,
|
||||
name: previousResult.stream.name,
|
||||
id: previousResult.stream.id,
|
||||
branch: {
|
||||
id: fetchMoreResult.stream.branch.id,
|
||||
name: fetchMoreResult.stream.branch.name,
|
||||
description: fetchMoreResult.stream.branch.description,
|
||||
__typename: previousResult.stream.branch.__typename,
|
||||
commits: {
|
||||
__typename: previousResult.stream.branch.commits.__typename,
|
||||
cursor: fetchMoreResult.stream.branch.commits.cursor,
|
||||
totalCount: fetchMoreResult.stream.branch.commits.totalCount,
|
||||
items: [...previousResult.stream.branch.commits.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,11 +150,38 @@ export default {
|
||||
query: streamCommitQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamid: this.$route.params.streamId,
|
||||
streamId: this.$route.params.streamId,
|
||||
id: this.$route.params.commitId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// commitActivitiy: {
|
||||
// query: `
|
||||
// query CommitActivity($streamid: String!, $id: String!) {
|
||||
// stream(id: $streamid) {
|
||||
// id
|
||||
// commit(id: $id) {
|
||||
// id
|
||||
// activity(actionType: "commit_receive", limit: 200) {
|
||||
// items {
|
||||
// info
|
||||
// time
|
||||
// userId
|
||||
// message
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `,
|
||||
// variables() {
|
||||
// return {
|
||||
// streamid: this.$route.params.streamId,
|
||||
// id: this.$route.params.commitId
|
||||
// }
|
||||
// },
|
||||
// update:(data) => data.stream.commit.activity
|
||||
// }
|
||||
},
|
||||
computed: {
|
||||
loggedInUserId() {
|
||||
|
||||
+276
-23
@@ -4,21 +4,20 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default class ObjectLoader {
|
||||
|
||||
/**
|
||||
* Creates a new object loader instance.
|
||||
* @param {*} param0
|
||||
*/
|
||||
constructor( { serverUrl, streamId, token, objectId, options = { fullyTraverseArrays: false, excludeProps: [ ] } } ) {
|
||||
constructor( { serverUrl, streamId, token, objectId, options = { enableCaching: true, fullyTraverseArrays: false, excludeProps: [ ] } } ) {
|
||||
this.INTERVAL_MS = 20
|
||||
this.TIMEOUT_MS = 180000 // three mins
|
||||
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
this.streamId = streamId
|
||||
this.objectId = objectId
|
||||
console.log('Object loader constructor called!!!')
|
||||
console.log('Object loader constructor called!')
|
||||
try {
|
||||
this.token = token || localStorage.getItem( 'AuthToken' )
|
||||
} catch (error) {
|
||||
@@ -33,7 +32,8 @@ export default class ObjectLoader {
|
||||
this.headers['Authorization'] = `Bearer ${this.token}`
|
||||
}
|
||||
|
||||
this.requestUrl = `${this.serverUrl}/objects/${this.streamId}/${this.objectId}`
|
||||
this.requestUrlRootObj = `${this.serverUrl}/objects/${this.streamId}/${this.objectId}/single`
|
||||
this.requestUrlChildren = `${this.serverUrl}/api/getobjects/${this.streamId}`
|
||||
this.promises = []
|
||||
this.intervals = {}
|
||||
this.buffer = []
|
||||
@@ -41,6 +41,28 @@ export default class ObjectLoader {
|
||||
this.totalChildrenCount = 0
|
||||
this.traversedReferencesCount = 0
|
||||
this.options = options
|
||||
this.options.numConnections = this.options.numConnections || 4
|
||||
|
||||
this.cacheDB = null
|
||||
|
||||
this.lastAsyncPause = Date.now()
|
||||
this.existingAsyncPause = null
|
||||
|
||||
}
|
||||
|
||||
async asyncPause() {
|
||||
// Don't freeze the UI
|
||||
// while ( this.existingAsyncPause ) {
|
||||
// await this.existingAsyncPause
|
||||
// }
|
||||
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
||||
this.lastAsyncPause = Date.now()
|
||||
this.existingAsyncPause = new Promise( resolve => setTimeout( resolve, 0 ) )
|
||||
await this.existingAsyncPause
|
||||
this.existingAsyncPause = null
|
||||
if (Date.now() - this.lastAsyncPause > 500) console.log("Loader Event loop lag: ", Date.now() - this.lastAsyncPause)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -177,11 +199,15 @@ export default class ObjectLoader {
|
||||
}
|
||||
|
||||
async * getObjectIterator( ) {
|
||||
let t0 = Date.now()
|
||||
let count = 0
|
||||
for await ( let line of this.getRawObjectIterator() ) {
|
||||
let { id, obj } = this.processLine( line )
|
||||
this.buffer[ id ] = obj
|
||||
count += 1
|
||||
yield obj
|
||||
}
|
||||
console.log(`Loaded ${count} objects in: ${(Date.now() - t0) / 1000}`)
|
||||
}
|
||||
|
||||
processLine( chunk ) {
|
||||
@@ -190,31 +216,258 @@ export default class ObjectLoader {
|
||||
}
|
||||
|
||||
async * getRawObjectIterator() {
|
||||
const decoder = new TextDecoder()
|
||||
const response = await fetch( this.requestUrl, { headers: this.headers } )
|
||||
const reader = response.body.getReader()
|
||||
let { value: chunk, done: readerDone } = await reader.read()
|
||||
chunk = chunk ? decoder.decode( chunk ) : ''
|
||||
let tSTART = Date.now()
|
||||
|
||||
let re = /\r\n|\n|\r/gm
|
||||
let startIndex = 0
|
||||
if ( this.options.enableCaching && window.indexedDB && this.cacheDB === null) {
|
||||
await safariFix()
|
||||
let idbOpenRequest = indexedDB.open('speckle-object-cache', 1)
|
||||
idbOpenRequest.onupgradeneeded = () => idbOpenRequest.result.createObjectStore('objects');
|
||||
this.cacheDB = await this.promisifyIdbRequest( idbOpenRequest )
|
||||
}
|
||||
|
||||
const rootObjJson = await this.getRawRootObject()
|
||||
// console.log("Root in: ", Date.now() - tSTART)
|
||||
|
||||
yield `${this.objectId}\t${rootObjJson}`
|
||||
|
||||
const rootObj = JSON.parse(rootObjJson)
|
||||
if ( !rootObj.__closure ) return
|
||||
|
||||
let childrenIds = Object.keys(rootObj.__closure).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
||||
if ( childrenIds.length === 0 ) return
|
||||
|
||||
let splitHttpRequests = []
|
||||
|
||||
if ( childrenIds.length > 50 ) {
|
||||
// split into 5%, 15%, 40%, 40% (5% for the high priority children: the ones with lower minDepth)
|
||||
let splitBeforeCacheCheck = [ [], [], [], [] ]
|
||||
let crtChildIndex = 0
|
||||
|
||||
for ( ; crtChildIndex < 0.05 * childrenIds.length; crtChildIndex++ ) {
|
||||
splitBeforeCacheCheck[0].push( childrenIds[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < 0.2 * childrenIds.length; crtChildIndex++ ) {
|
||||
splitBeforeCacheCheck[1].push( childrenIds[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < 0.6 * childrenIds.length; crtChildIndex++ ) {
|
||||
splitBeforeCacheCheck[2].push( childrenIds[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < childrenIds.length; crtChildIndex++ ) {
|
||||
splitBeforeCacheCheck[3].push( childrenIds[ crtChildIndex ] )
|
||||
}
|
||||
|
||||
|
||||
console.log("Cache check for: ", splitBeforeCacheCheck)
|
||||
|
||||
let newChildren = []
|
||||
let nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ 0 ] )
|
||||
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
let cachedObjects = await nextCachePromise
|
||||
if ( i < 3 ) nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ i + 1 ] )
|
||||
|
||||
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
||||
for ( let id of sortedCachedKeys ) {
|
||||
yield `${id}\t${cachedObjects[ id ]}`
|
||||
}
|
||||
let newChildrenForBatch = splitBeforeCacheCheck[i].filter( id => !( id in cachedObjects ) )
|
||||
newChildren.push( ...newChildrenForBatch )
|
||||
}
|
||||
|
||||
if ( newChildren.length === 0 ) return
|
||||
|
||||
if ( newChildren.length <= 50 ) {
|
||||
// we have almost all of children in the cache. do only 1 requests for the remaining new children
|
||||
splitHttpRequests.push( newChildren )
|
||||
} else {
|
||||
// we now set up the batches for 4 http requests, starting from `newChildren` (already sorted by priority)
|
||||
splitHttpRequests = [ [], [], [], [] ]
|
||||
crtChildIndex = 0
|
||||
|
||||
for ( ; crtChildIndex < 0.05 * newChildren.length; crtChildIndex++ ) {
|
||||
splitHttpRequests[0].push( newChildren[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < 0.2 * newChildren.length; crtChildIndex++ ) {
|
||||
splitHttpRequests[1].push( newChildren[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < 0.6 * newChildren.length; crtChildIndex++ ) {
|
||||
splitHttpRequests[2].push( newChildren[ crtChildIndex ] )
|
||||
}
|
||||
for ( ; crtChildIndex < newChildren.length; crtChildIndex++ ) {
|
||||
splitHttpRequests[3].push( newChildren[ crtChildIndex ] )
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// small object with <= 50 children. check cache and make only 1 request
|
||||
const cachedObjects = await this.cacheGetObjects( childrenIds )
|
||||
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
||||
for ( let id of sortedCachedKeys ) {
|
||||
yield `${id}\t${cachedObjects[ id ]}`
|
||||
}
|
||||
childrenIds = childrenIds.filter(id => !( id in cachedObjects ) )
|
||||
if ( childrenIds.length === 0 ) return
|
||||
|
||||
// only 1 http request with the remaining children ( <= 50 )
|
||||
splitHttpRequests.push( childrenIds )
|
||||
}
|
||||
|
||||
// Starting http requests for batches in `splitHttpRequests`
|
||||
|
||||
const decoders = []
|
||||
const readers = []
|
||||
const readPromisses = []
|
||||
const startIndexes = []
|
||||
const readBuffers = []
|
||||
const finishedRequests = []
|
||||
|
||||
for (let i = 0; i < splitHttpRequests.length; i++) {
|
||||
decoders.push(new TextDecoder())
|
||||
readers.push( null )
|
||||
readPromisses.push( null )
|
||||
startIndexes.push( 0 )
|
||||
readBuffers.push( '' )
|
||||
finishedRequests.push( false )
|
||||
|
||||
fetch(
|
||||
this.requestUrlChildren,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( { objects: JSON.stringify( splitHttpRequests[i] ) } )
|
||||
}
|
||||
).then( crtResponse => {
|
||||
let crtReader = crtResponse.body.getReader()
|
||||
readers[i] = crtReader
|
||||
let crtReadPromise = crtReader.read().then(x => { x.reqId = i; return x })
|
||||
readPromisses[i] = crtReadPromise
|
||||
})
|
||||
}
|
||||
|
||||
while ( true ) {
|
||||
let result = re.exec( chunk )
|
||||
if ( !result ) {
|
||||
if ( readerDone ) break
|
||||
let remainder = chunk.substr( startIndex )
|
||||
;( { value: chunk, done: readerDone } = await reader.read() ) // PS: semicolon of doom
|
||||
chunk = remainder + ( chunk ? decoder.decode( chunk ) : '' )
|
||||
startIndex = re.lastIndex = 0
|
||||
let validReadPromises = readPromisses.filter(x => x != null)
|
||||
if ( validReadPromises.length === 0 ) {
|
||||
// Check if all requests finished
|
||||
if ( finishedRequests.every(x => x) ) {
|
||||
break
|
||||
}
|
||||
// Sleep 10 ms
|
||||
await new Promise( ( resolve ) => {
|
||||
setTimeout( resolve, 10 )
|
||||
} )
|
||||
continue
|
||||
}
|
||||
yield chunk.substring( startIndex, result.index )
|
||||
startIndex = re.lastIndex
|
||||
}
|
||||
|
||||
if ( startIndex < chunk.length ) {
|
||||
yield chunk.substr( startIndex )
|
||||
// Wait for data on any running request
|
||||
let data = await Promise.any( validReadPromises )
|
||||
let { value: crtDataChunk, done: readerDone, reqId } = data
|
||||
finishedRequests[ reqId ] = readerDone
|
||||
|
||||
// Replace read promise on this request with a new `read` call
|
||||
if ( !readerDone ) {
|
||||
let crtReadPromise = readers[ reqId ].read().then(x => { x.reqId = reqId; return x })
|
||||
readPromisses[ reqId ] = crtReadPromise
|
||||
} else {
|
||||
// This request finished. "Flush any non-newline-terminated text"
|
||||
if ( readBuffers[ reqId ].length > 0 ) {
|
||||
yield readBuffers[ reqId ]
|
||||
readBuffers[ reqId ] = ''
|
||||
}
|
||||
// no other read calls for this request
|
||||
readPromisses[ reqId ] = null
|
||||
}
|
||||
|
||||
if ( !crtDataChunk )
|
||||
continue
|
||||
|
||||
crtDataChunk = decoders[ reqId ].decode( crtDataChunk )
|
||||
let unprocessedText = readBuffers[ reqId ] + crtDataChunk
|
||||
let unprocessedLines = unprocessedText.split(/\r\n|\n|\r/)
|
||||
let remainderText = unprocessedLines.pop()
|
||||
readBuffers[ reqId ] = remainderText
|
||||
|
||||
for ( let line of unprocessedLines ) {
|
||||
yield line
|
||||
}
|
||||
this.cacheStoreObjects(unprocessedLines)
|
||||
}
|
||||
}
|
||||
|
||||
async getRawRootObject() {
|
||||
const cachedRootObject = await this.cacheGetObjects( [ this.objectId ] )
|
||||
if ( cachedRootObject[ this.objectId ] )
|
||||
return cachedRootObject[ this.objectId ]
|
||||
const response = await fetch( this.requestUrlRootObj, { headers: this.headers } )
|
||||
const responseText = await response.text()
|
||||
this.cacheStoreObjects( [ `${this.objectId}\t${responseText}` ] )
|
||||
return responseText
|
||||
}
|
||||
|
||||
promisifyIdbRequest(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
||||
request.onabort = request.onerror = () => reject(request.error);
|
||||
})
|
||||
}
|
||||
|
||||
async cacheGetObjects(ids) {
|
||||
if ( !this.options.enableCaching || !window.indexedDB ) {
|
||||
return {}
|
||||
}
|
||||
|
||||
let ret = {}
|
||||
|
||||
for (let i = 0; i < ids.length; i += 500) {
|
||||
let idsChunk = ids.slice(i, i + 500)
|
||||
let t0 = Date.now()
|
||||
|
||||
let store = this.cacheDB.transaction('objects', 'readonly').objectStore('objects')
|
||||
let idbChildrenPromises = idsChunk.map( id => this.promisifyIdbRequest( store.get( id ) ).then( data => ( { id, data } ) ) )
|
||||
let cachedData = await Promise.all(idbChildrenPromises)
|
||||
|
||||
// console.log("Cache check for : ", idsChunk.length, Date.now() - t0)
|
||||
|
||||
for ( let cachedObj of cachedData ) {
|
||||
if ( !cachedObj.data ) // non-existent objects are retrieved with `undefined` data
|
||||
continue
|
||||
ret[ cachedObj.id ] = cachedObj.data
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
cacheStoreObjects(objects) {
|
||||
if ( !this.options.enableCaching || !window.indexedDB ) {
|
||||
return {}
|
||||
}
|
||||
|
||||
let store = this.cacheDB.transaction('objects', 'readwrite').objectStore('objects')
|
||||
for ( let obj of objects ) {
|
||||
let idAndData = obj.split( '\t' )
|
||||
store.put(idAndData[1], idAndData[0])
|
||||
}
|
||||
|
||||
return this.promisifyIdbRequest( store.transaction )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix
|
||||
function safariFix() {
|
||||
const isSafari =
|
||||
!navigator.userAgentData &&
|
||||
/Safari\//.test(navigator.userAgent) &&
|
||||
!/Chrom(e|ium)\//.test(navigator.userAgent)
|
||||
|
||||
// No point putting other browsers or older versions of Safari through this mess.
|
||||
if (!isSafari || !indexedDB.databases) return Promise.resolve()
|
||||
|
||||
let intervalId
|
||||
|
||||
return new Promise( ( resolve ) => {
|
||||
const tryIdb = () => indexedDB.databases().finally(resolve)
|
||||
intervalId = setInterval(tryIdb, 100)
|
||||
tryIdb()
|
||||
}).finally( () => clearInterval(intervalId) )
|
||||
}
|
||||
|
||||
Generated
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@speckle/objectloader",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@speckle/objectloader",
|
||||
"version": "2.1.1",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@speckle/objectloader",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.0",
|
||||
"description": "Simple API helper to stream in objects from the Speckle Server.",
|
||||
"main": "index.js",
|
||||
"homepage": "https://speckle.systems",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"build-fe": "webpack --env dev --config webpack.config.render_page.js && webpack --env build --config webpack.config.render_page.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@speckle/objectloader": "file:../objectloader",
|
||||
"@speckle/viewer": "file:../viewer",
|
||||
"@speckle/objectloader": "^2.2.0",
|
||||
"@speckle/viewer": "^2.2.0",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"crypto": "^1.0.1",
|
||||
"debug": "~2.6.9",
|
||||
|
||||
@@ -44,21 +44,28 @@ module.exports = async ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
|
||||
let user = req.body
|
||||
|
||||
if ( serverInfo.inviteOnly && !req.session.inviteId ) {
|
||||
// 1. if the server is invite only you must have an invite
|
||||
if ( serverInfo.inviteOnly && !req.session.inviteId )
|
||||
throw new Error( 'This server is invite only. Please provide an invite id.' )
|
||||
}
|
||||
|
||||
// 2. if you have an invite it must be valid, both for invite only and public servers
|
||||
if ( req.session.inviteId ) {
|
||||
const valid = await validateInvite( { id: req.session.inviteId, email: user.email } )
|
||||
if ( !valid )
|
||||
const isInviteValid = await validateInvite( { id: req.session.inviteId, email: user.email } )
|
||||
if ( !isInviteValid )
|
||||
throw new Error( 'Invite email mismatch. Please use the original email the invite was sent to register.' )
|
||||
|
||||
await useInvite( { id: req.session.inviteId, email: user.email } )
|
||||
}
|
||||
|
||||
// 3. at this point we know, that we have one of these cases:
|
||||
// * the server is invite only and the user has a valid invite
|
||||
// * the server public and the user has a valid invite
|
||||
// * the server public and the user doesn't have an invite
|
||||
// so we go ahead and register the user
|
||||
let userId = await createUser( user )
|
||||
req.user = { id: userId, email: user.email }
|
||||
|
||||
// 4. if the user had an invite, its used up
|
||||
if ( req.session.inviteId ) await useInvite( { id: req.session.inviteId, email: user.email } )
|
||||
|
||||
return next( )
|
||||
} catch ( err ) {
|
||||
debug( 'speckle:errors' )( err )
|
||||
|
||||
@@ -4,12 +4,12 @@ const chai = require( 'chai' )
|
||||
const request = require( 'supertest' )
|
||||
const assert = require( 'assert' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const { createStream, getStream } = require( `${appRoot}/modules/core/services/streams` )
|
||||
const { init, startHttp } = require( `${appRoot}/app` )
|
||||
|
||||
const { updateServerInfo } = require( `${appRoot}/modules/core/services/generic` )
|
||||
const { getUserByEmail } = require( `${appRoot}/modules/core/services/users` )
|
||||
const { createAndSendInvite } = require( `${appRoot}/modules/serverinvites/services` )
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
@@ -38,42 +38,45 @@ describe( 'Auth @auth', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'Should register a new user (speckle frontend)', async ( ) => {
|
||||
let res =
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 302 )
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 302 )
|
||||
} )
|
||||
|
||||
it( 'Should fail to register a new user w/o password (speckle frontend)', async ( ) => {
|
||||
let res =
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu' } )
|
||||
.expect( 400 )
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu' } )
|
||||
.expect( 400 )
|
||||
} )
|
||||
|
||||
it( 'Should not register a new user without an invite id in an invite id only server', async() => {
|
||||
await updateServerInfo( { inviteOnly: true } )
|
||||
|
||||
// No invite
|
||||
let res =
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 400 )
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test' )
|
||||
.send( { email: 'spam@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 400 )
|
||||
|
||||
let user = await getUserByEmail( { email: 'spam@speckle.systems' } )
|
||||
let inviteId = await createAndSendInvite( { email: 'bunny@speckle.systems', inviterId: user.id } )
|
||||
|
||||
// Mismatched invite
|
||||
res = await request( expressApp )
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test&inviteId=' + inviteId )
|
||||
.send( { email: 'spam-super@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 400 )
|
||||
|
||||
// Invalid inviteId
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test&inviteId=' + 'inviteId' )
|
||||
.send( { email: 'spam-super@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 400 )
|
||||
|
||||
// finally correct
|
||||
res = await request( expressApp )
|
||||
await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test&inviteId=' + inviteId )
|
||||
.send( { email: 'bunny@speckle.systems', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 302 )
|
||||
@@ -81,6 +84,21 @@ describe( 'Auth @auth', ( ) => {
|
||||
await updateServerInfo( { inviteOnly: false } )
|
||||
} )
|
||||
|
||||
it ( 'Should add resource access to newly registered user if the invite contains it', async ( ) => {
|
||||
let user = await getUserByEmail( { email: 'spam@speckle.systems' } )
|
||||
const streamId = await createStream( { ownerId: user.id } )
|
||||
const inviteId = await createAndSendInvite( { email: 'new@stream.collaborator', inviterId: user.id, resourceTarget: 'streams', resourceId: streamId, role: 'stream:reviewer' } )
|
||||
|
||||
const res = await request( expressApp )
|
||||
.post( '/auth/local/register?challenge=test&suuid=test&inviteId=' + inviteId )
|
||||
.send( { email: 'new@stream.collaborator', name: 'dimitrie stefanescu', company: 'speckle', password: 'roll saving throws' } )
|
||||
.expect( 302 )
|
||||
|
||||
const collaborator = await getUserByEmail( { email: 'new@stream.collaborator' } )
|
||||
const stream = await getStream( { streamId, userId: collaborator.id } )
|
||||
expect( stream.role ).to.equal( 'stream:reviewer' )
|
||||
} )
|
||||
|
||||
it( 'Should log in (speckle frontend)', async ( ) => {
|
||||
let res =
|
||||
await request( expressApp )
|
||||
|
||||
@@ -5,13 +5,14 @@ const appRoot = require( 'app-root-path' )
|
||||
const {
|
||||
createStream,
|
||||
getStream,
|
||||
getStreams,
|
||||
updateStream,
|
||||
deleteStream,
|
||||
getUserStreams,
|
||||
getUserStreamsCount,
|
||||
getStreamUsers,
|
||||
grantPermissionsStream,
|
||||
revokePermissionsStream
|
||||
revokePermissionsStream,
|
||||
} = require( '../../services/streams' )
|
||||
|
||||
const { authorizeResolver, validateScopes, validateServerRole, pubsub } = require( `${appRoot}/modules/shared` )
|
||||
@@ -29,6 +30,35 @@ function sleep( ms ) {
|
||||
} )
|
||||
}
|
||||
|
||||
const _deleteStream = async ( parent, args, context, info ) => {
|
||||
await saveActivity( {
|
||||
streamId: args.id,
|
||||
resourceType: 'stream',
|
||||
resourceId: args.id,
|
||||
actionType: 'stream_delete',
|
||||
userId: context.userId,
|
||||
info: { },
|
||||
message: 'Stream deleted'
|
||||
} )
|
||||
|
||||
// Notify any listeners on the streamId
|
||||
await pubsub.publish( STREAM_DELETED, { streamDeleted: { streamId: args.id }, streamId: args.id } )
|
||||
|
||||
// Notify all stream users
|
||||
let users = await getStreamUsers( { streamId: args.id } )
|
||||
|
||||
for ( let user of users ) {
|
||||
await pubsub.publish( USER_STREAM_REMOVED, { userStreamRemoved: { id: args.id }, ownerId: user.id } )
|
||||
}
|
||||
|
||||
// delay deletion by a bit so we can do auth checks
|
||||
await sleep( 250 )
|
||||
|
||||
// Delete after event so we can do authz
|
||||
await deleteStream( { streamId: args.id } )
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
|
||||
@@ -38,7 +68,7 @@ module.exports = {
|
||||
throw new ApolloError( 'Stream not found' )
|
||||
|
||||
if ( !stream.isPublic && context.auth === false )
|
||||
throw new ForbiddenError( 'You are not authorised.' )
|
||||
throw new ForbiddenError( 'You are not authorized.' )
|
||||
|
||||
if ( !stream.isPublic ) {
|
||||
await validateServerRole( context, 'server:user' )
|
||||
@@ -57,6 +87,15 @@ module.exports = {
|
||||
|
||||
let { cursor, streams } = await getUserStreams( { userId: context.userId, limit: args.limit, cursor: args.cursor, publicOnly: false, searchQuery: args.query } )
|
||||
return { totalCount, cursor: cursor, items: streams }
|
||||
},
|
||||
|
||||
async adminStreams( parent, args, context, info ) {
|
||||
if ( args.limit && args.limit > 50 )
|
||||
throw new UserInputError( 'Cannot return more than 50 items at a time.' )
|
||||
|
||||
let { streams, totalCount } = await getStreams(
|
||||
{ offset: args.offset, limit: args.limit, orderBy: args.orderBy, publicOnly: args.publicOnly, searchQuery: args.query, visibility: args.visibility } )
|
||||
return { totalCount, items: streams }
|
||||
}
|
||||
|
||||
},
|
||||
@@ -66,8 +105,12 @@ module.exports = {
|
||||
async collaborators( parent, args, context, info ) {
|
||||
let users = await getStreamUsers( { streamId: parent.id } )
|
||||
return users
|
||||
}
|
||||
},
|
||||
|
||||
// async size ( parent, args, context, info ) {
|
||||
// let size = await streamSize( { streamId: parent.id } )
|
||||
// return size
|
||||
// }
|
||||
},
|
||||
|
||||
User: {
|
||||
@@ -127,33 +170,16 @@ module.exports = {
|
||||
|
||||
async streamDelete( parent, args, context, info ) {
|
||||
await authorizeResolver( context.userId, args.id, 'stream:owner' )
|
||||
return await _deleteStream( parent, args, context, info )
|
||||
},
|
||||
|
||||
await saveActivity( {
|
||||
streamId: args.id,
|
||||
resourceType: 'stream',
|
||||
resourceId: args.id,
|
||||
actionType: 'stream_delete',
|
||||
userId: context.userId,
|
||||
info: { },
|
||||
message: 'Stream deleted'
|
||||
} )
|
||||
|
||||
// Notify any listeners on the streamId
|
||||
await pubsub.publish( STREAM_DELETED, { streamDeleted: { streamId: args.id }, streamId: args.id } )
|
||||
|
||||
// Notify all stream users
|
||||
let users = await getStreamUsers( { streamId: args.id } )
|
||||
|
||||
for ( let user of users ) {
|
||||
await pubsub.publish( USER_STREAM_REMOVED, { userStreamRemoved: { id: args.id }, ownerId: user.id } )
|
||||
}
|
||||
|
||||
// delay deletion by a bit so we can do auth checks
|
||||
await sleep( 250 )
|
||||
|
||||
// Delete after event so we can do authz
|
||||
await deleteStream( { streamId: args.id } )
|
||||
return true
|
||||
async streamsDelete( parent, args, context, info ) {
|
||||
const results = await Promise.all( args.ids.map( async id => {
|
||||
let newArgs = { ...args }
|
||||
newArgs.id = id
|
||||
return await _deleteStream( parent, newArgs, context, info )
|
||||
} ) )
|
||||
return results.every( res => res === true )
|
||||
},
|
||||
|
||||
async streamGrantPermission( parent, args, context, info ) {
|
||||
|
||||
@@ -9,6 +9,9 @@ extend type Query {
|
||||
"""
|
||||
streams( query: String, limit: Int = 25, cursor: String ): StreamCollection
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
adminStreams( offset: Int = 0, query: String, orderBy: String, visibility: String, limit: Int = 25 ): StreamCollection
|
||||
@hasRole(role: "server:admin")
|
||||
}
|
||||
|
||||
type Stream {
|
||||
@@ -23,6 +26,7 @@ type Stream {
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
collaborators: [ StreamCollaborator ]!
|
||||
size: String
|
||||
}
|
||||
|
||||
extend type User {
|
||||
@@ -66,6 +70,9 @@ extend type Mutation {
|
||||
streamDelete( id: String! ): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
streamsDelete( ids: [String!] ): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
"""
|
||||
Grants permissions to a user on a given stream.
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,7 @@ extend type Query {
|
||||
"""
|
||||
Get users from the server in a paginated view. The query search for matches in name, company and email.
|
||||
"""
|
||||
users(limit: Int!=25, offset: Int!=0, query: String=null) : UserCollection
|
||||
users(limit: Int! = 25, offset: Int! = 0, query: String=null) : UserCollection
|
||||
userSearch(
|
||||
query: String!
|
||||
limit: Int! = 25
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// /* istanbul ignore file */
|
||||
/*
|
||||
This migration is fixing the duplicate user problem reported
|
||||
https://speckle.community/t/error-in-grasshopper-while-receiving-data-you-dont-have-access-to-stream-xxxxx-on-server-https-speckle-xyz-or-the-stream-does-not-exist/2003
|
||||
*/
|
||||
|
||||
exports.up = async ( knex ) => {
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const roles = require( `${appRoot}/modules/core/roles.js` )
|
||||
|
||||
const Users = ( ) => knex( 'users' )
|
||||
|
||||
// tableName, columnName that need migration
|
||||
const migrationTargets = [
|
||||
[ 'api_tokens' , 'owner' ],
|
||||
[ 'authorization_codes' , 'userId' ],
|
||||
[ 'branches' , 'authorId' ],
|
||||
[ 'commits' , 'author' ],
|
||||
[ 'file_uploads' , 'userId' ],
|
||||
[ 'personal_api_tokens' , 'userId' ],
|
||||
[ 'refresh_tokens' , 'userId' ],
|
||||
// [ 'server_acl' , 'userId' ], //userId is a PrimaryKey in this table, act accordingly
|
||||
[ 'server_apps' , 'authorId' ],
|
||||
[ 'server_invites' , 'inviterId' ],
|
||||
// [ 'stream_acl' , 'userId' ],//userId, with resourceId is a PrimaryKey in this table, act accordingly
|
||||
[ 'stream_activity' , 'userId' ],
|
||||
]
|
||||
|
||||
const migrateColumnValue = async( tableName, columnName, oldUser, newUser ) =>
|
||||
await knex( tableName )
|
||||
.where( { [columnName]: oldUser.id } )
|
||||
.update( { [columnName]: newUser.id } )
|
||||
|
||||
const serverAclMigration = async ( { lowerUser, upperUser } ) => {
|
||||
const oldAcl = await knex( 'server_acl' ).where( { userId: upperUser.id } ).first()
|
||||
// if the old user was admin, make the target admin too
|
||||
if ( oldAcl.role === 'server:admin' ) await knex( 'server_acl' ).where( { userId: lowerUser.id } ).update( { role: 'server:admin' } )
|
||||
}
|
||||
|
||||
const _migrateSingleStreamAccess = async ( { lowerUser, upperUser, upperStreamAcl } ) => {
|
||||
const upperRole = roles.filter( r => r.name === upperStreamAcl.role )[0]
|
||||
const lowerAcl = await knex( 'stream_acl' ).where( { userId: lowerUser.id, resourceId: upperStreamAcl.resourceId } ).first()
|
||||
// see if the lowerUser has access to the stream
|
||||
if ( lowerAcl ) {
|
||||
// if the upper user had more access, migrate the lower user up
|
||||
const lowerRole = roles.filter( r => r.name === lowerAcl.role )[0]
|
||||
if ( lowerRole.weight < upperRole.weight )
|
||||
await knex( 'stream_acl' )
|
||||
.where( { userId: lowerUser.id, resourceId: upperStreamAcl.resourceId } )
|
||||
.update( { role: upperRole.name } )
|
||||
} else {
|
||||
// if it didn't have access, just add it
|
||||
let lowerStreamAcl = { ...upperStreamAcl }
|
||||
lowerStreamAcl.userId = lowerUser.id
|
||||
await knex( 'stream_acl' ).insert( lowerStreamAcl )
|
||||
}
|
||||
}
|
||||
|
||||
const streamAclMigration = async ( { lowerUser, upperUser } ) => {
|
||||
const upperAcl = await knex( 'stream_acl' ).where( { userId: upperUser.id } )
|
||||
|
||||
await Promise.all( upperAcl.map( async upperStreamAcl => await _migrateSingleStreamAccess( { lowerUser, upperUser, upperStreamAcl } ) ) )
|
||||
}
|
||||
|
||||
const createMigrations = ( { lowerUser, upperUser } ) => migrationTargets.map( ( [ tableName, columnName ] ) => {
|
||||
migrateColumnValue( tableName, columnName, upperUser,lowerUser ) } )
|
||||
|
||||
|
||||
const userByEmailQuery = email => Users( ).where( { email } )
|
||||
|
||||
const getDuplicateUsers = async ( ) => {
|
||||
let duplicates = await knex.raw( 'select lower(email) as lowered, count(id) as reg_count from users group by lowered having count(id) > 1' )
|
||||
return await Promise.all( duplicates.rows.map( async dup => {
|
||||
let lowerEmail = dup.lowered
|
||||
|
||||
let lowerUser = await userByEmailQuery( lowerEmail ).first( )
|
||||
// if no user found migrate to a random one?
|
||||
// TODO: decide 👆
|
||||
// my idea, take the first one and run with it
|
||||
if ( !lowerUser ) lowerUser = await Users( ).whereRaw( 'lower(email) = lower(?)',[ lowerEmail ] ).first()
|
||||
let upperUser = await Users( ).whereRaw( 'lower(email) = lower(?)',[ lowerEmail ] ).whereNot( { id: lowerUser.id } ).first( )
|
||||
return { lowerUser,upperUser }
|
||||
} ) )
|
||||
}
|
||||
|
||||
const runMigrations = async ( ) => {
|
||||
const duplicateUsers = await getDuplicateUsers( )
|
||||
await Promise.all( duplicateUsers.map( async userDouble => {
|
||||
const migrations = createMigrations( userDouble )
|
||||
await Promise.all( migrations.map( async migrationStep => await migrationStep ) )
|
||||
await serverAclMigration( userDouble )
|
||||
await streamAclMigration( userDouble )
|
||||
|
||||
// remove the now defunct user
|
||||
await userByEmailQuery( userDouble.upperUser.email ).delete( )
|
||||
} ) )
|
||||
}
|
||||
await runMigrations( )
|
||||
}
|
||||
|
||||
exports.down = async ( knex ) => {
|
||||
|
||||
}
|
||||
@@ -15,36 +15,26 @@ module.exports = {
|
||||
},
|
||||
|
||||
async getAllScopes( ) {
|
||||
|
||||
return await Scopes( ).select( '*' )
|
||||
|
||||
},
|
||||
|
||||
async getPublicScopes() {
|
||||
|
||||
return await Scopes( ).select( '*' ).where( { public: true } )
|
||||
|
||||
},
|
||||
|
||||
async getAllRoles( ) {
|
||||
|
||||
return await Roles( ).select( '*' )
|
||||
|
||||
},
|
||||
|
||||
async getPublicRoles() {
|
||||
|
||||
return await Roles( ).select( '*' ).where( { public: true } )
|
||||
|
||||
},
|
||||
|
||||
async updateServerInfo( { name, company, description, adminContact, termsOfService, inviteOnly } ) {
|
||||
|
||||
let serverInfo = await Info( ).select( '*' ).first( )
|
||||
if ( !serverInfo )
|
||||
return await Info( ).insert( { name, company, description, adminContact, termsOfService, inviteOnly, completed: true } )
|
||||
else
|
||||
return await Info( ).where( { id: 0 } ).update( { name, company, description, adminContact, termsOfService, inviteOnly, completed: true } )
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,47 @@ module.exports = {
|
||||
return { streams: rows, cursor: rows.length > 0 ? rows[ rows.length - 1 ].updatedAt.toISOString( ) : null }
|
||||
},
|
||||
|
||||
async getStreams( { offset, limit, orderBy, visibility, searchQuery } ) {
|
||||
let query = knex
|
||||
.column( 'streams.*', knex.raw( 'coalesce(sum(pg_column_size(objects.data)),0) as size' ) )
|
||||
.select()
|
||||
.from( 'streams' )
|
||||
.leftJoin( 'objects', 'streams.id', 'objects.streamId' )
|
||||
.groupBy( 'streams.id' )
|
||||
|
||||
let countQuery = Streams( )
|
||||
|
||||
if ( searchQuery ) {
|
||||
const whereFunc = function () {
|
||||
this.where( 'streams.name', 'ILIKE', `%${ searchQuery }%` )
|
||||
.orWhere( 'streams.description', 'ILIKE', `%${searchQuery}%` )
|
||||
}
|
||||
query.where( whereFunc )
|
||||
countQuery.where( whereFunc )
|
||||
}
|
||||
if ( visibility && visibility !== 'all' ) {
|
||||
if ( ![ 'private', 'public' ].includes( visibility ) ) throw new Error( 'Stream visibility should be either private, public or all' )
|
||||
let isPublic = visibility === 'public'
|
||||
const publicFunc = function() {
|
||||
this.where( { isPublic } )
|
||||
}
|
||||
query.andWhere( publicFunc )
|
||||
countQuery.andWhere( publicFunc )
|
||||
}
|
||||
let [ res ] = await countQuery.count( )
|
||||
let count = parseInt( res.count )
|
||||
|
||||
if ( !count ) return { streams: [], totalCount: 0 }
|
||||
|
||||
orderBy = orderBy || 'updatedAt,desc'
|
||||
|
||||
let [ columnName, order ] = orderBy.split( ',' )
|
||||
|
||||
let rows = await query.orderBy( `${columnName}`, order ).offset( offset ).limit( limit )
|
||||
|
||||
return { streams: rows, totalCount: count }
|
||||
},
|
||||
|
||||
async getUserStreamsCount( { userId, publicOnly, searchQuery } ) {
|
||||
publicOnly = publicOnly !== false //defaults to true if not provided
|
||||
|
||||
@@ -168,7 +209,7 @@ module.exports = {
|
||||
.orderBy( 'stream_acl.role' )
|
||||
|
||||
return await query
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const adjectives = [
|
||||
|
||||
@@ -323,6 +323,14 @@ describe( 'GraphQL API Core @core-api', ( ) => {
|
||||
expect( res.body.errors[ 0 ].extensions.code ).to.equal( 'FORBIDDEN' )
|
||||
} )
|
||||
|
||||
it( 'Should fail to delete streams if not admin', async ( ) => {
|
||||
const res = await sendRequest( userB.token, { query: `mutation { streamsDelete( ids:"[${ts4}]")}` } )
|
||||
expect( res ).to.be.json
|
||||
|
||||
expect( res.body.errors ).to.exist
|
||||
expect( res.body.errors[ 0 ].extensions.code ).to.equal( 'FORBIDDEN' )
|
||||
} )
|
||||
|
||||
it( 'Should delete a stream', async ( ) => {
|
||||
const res = await sendRequest( userB.token, { query: `mutation { streamDelete( id:"${ts4}")}` } )
|
||||
|
||||
@@ -331,6 +339,98 @@ describe( 'GraphQL API Core @core-api', ( ) => {
|
||||
expect( res.body.data ).to.have.property( 'streamDelete' )
|
||||
expect( res.body.data.streamDelete ).to.equal( true )
|
||||
} )
|
||||
|
||||
it ( 'Should query streams', async ( ) => {
|
||||
let streamResults = await sendRequest( userA.token, {
|
||||
query: '{ streams(limit: 200) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.errors ).to.exist
|
||||
expect( streamResults.body.errors[ 0 ].extensions.code ).to.equal( 'BAD_USER_INPUT' )
|
||||
} )
|
||||
|
||||
it ( 'Should be forbidden to query admin streams if not admin', async ( ) => {
|
||||
let res = await sendRequest( userC.token, {
|
||||
query: '{ adminStreams { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( res ).to.be.json
|
||||
|
||||
expect( res.body.errors ).to.exist
|
||||
expect( res.body.errors[ 0 ].extensions.code ).to.equal( 'FORBIDDEN' )
|
||||
} )
|
||||
|
||||
it ( 'Should query admin streams', async ( ) => {
|
||||
let streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams { totalCount items { id name } } }'
|
||||
} )
|
||||
|
||||
expect( streamResults.body.data.adminStreams.totalCount ).to.equal( 4 )
|
||||
|
||||
await Promise.all( [
|
||||
await sendRequest( userC.token, { query: 'mutation { streamCreate(stream: { name: "Admin TS1 (u A) Private", description: "Hello World", isPublic:false } ) }' } ),
|
||||
await sendRequest( userA.token, { query: 'mutation { streamCreate(stream: { name: "Admin TS2 (u A)", description: "Hello Darkness", isPublic:true } ) }' } ),
|
||||
await sendRequest( userB.token, { query: 'mutation { streamCreate(stream: { name: "Admin TS3 (u B) Private", description: "Hello Pumba", isPublic:false } ) }' } ),
|
||||
await sendRequest( userB.token, { query: 'mutation { streamCreate(stream: { name: "Admin TS4 (u B)", description: "Hello Julian", isPublic:true } ) }' } ),
|
||||
await sendRequest( userB.token, { query: 'mutation { streamCreate(stream: { name: "Admin TS5 (u B)", description: "Hello King", isPublic:true } ) }' } )
|
||||
] )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.totalCount ).to.equal( 9 )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams(limit: 200) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.errors ).to.exist
|
||||
expect( streamResults.body.errors[ 0 ].extensions.code ).to.equal( 'BAD_USER_INPUT' )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams(limit: 2) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.totalCount ).to.equal( 9 )
|
||||
expect( streamResults.body.data.adminStreams.items.length ).to.equal( 2 )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams(offset: 5) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.items.length ).to.equal( 4 )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams( query: "Admin" ) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.totalCount ).to.equal( 5 )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams( orderBy: "updatedAt,asc" ) { totalCount items { id name updatedAt } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.items.pop().name ).to.equal( 'Admin TS5 (u B)' )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams( visibility: "private" ) { totalCount items { id name isPublic } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.items )
|
||||
.to.satisfy( ( streams ) => streams.every( stream => !stream.isPublic ) )
|
||||
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams( visibility: "public" ) { totalCount items { id name isPublic } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.items )
|
||||
.to.satisfy( ( streams ) => streams.every( stream => stream.isPublic ) )
|
||||
} )
|
||||
|
||||
it( 'Should delete streams', async ( ) => {
|
||||
streamResults = await sendRequest( userA.token, {
|
||||
query: '{ adminStreams( query: "Admin" ) { totalCount items { id name } } }'
|
||||
} )
|
||||
expect( streamResults.body.data.adminStreams.totalCount ).to.equal( 5 )
|
||||
const streamIds = streamResults.body.data.adminStreams.items.map( stream => stream.id )
|
||||
const res = await sendRequest( userA.token, { query: 'mutation ( $ids: [String!] ){ streamsDelete( ids: $ids )}', variables:{ ids: streamIds } } )
|
||||
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.not.exist
|
||||
expect( res.body.data ).to.have.property( 'streamsDelete' )
|
||||
expect( res.body.data.streamsDelete ).to.equal( true )
|
||||
} )
|
||||
} )
|
||||
|
||||
describe( 'Objects, Commits & Branches', ( ) => {
|
||||
|
||||
@@ -66,7 +66,7 @@ describe( 'Upload/Download Routes @api-rest', ( ) => {
|
||||
res = await chai.request( expressApp ).get( `/objects/${testStream.id}/null` ).set( 'Authorization', 'this is a hoax' )
|
||||
expect( res ).to.have.status( 404 )
|
||||
|
||||
// invalid streamid
|
||||
// invalid streamId
|
||||
res = await chai.request( expressApp ).get( `/objects/${'thisDoesNotExist'}/null` ).set( 'Authorization', userA.token )
|
||||
expect( res ).to.have.status( 404 )
|
||||
|
||||
@@ -120,7 +120,7 @@ describe( 'Upload/Download Routes @api-rest', ( ) => {
|
||||
res = await chai.request( expressApp ).post( `/objects/${testStream.id}` ).set( 'Authorization', 'this is a hoax' )
|
||||
expect( res ).to.have.status( 401 )
|
||||
|
||||
// invalid streamid
|
||||
// invalid streamId
|
||||
res = await chai.request( expressApp ).post( `/objects/${'thisDoesNotExist'}` ).set( 'Authorization', userA.token )
|
||||
expect( res ).to.have.status( 401 )
|
||||
} )
|
||||
|
||||
@@ -12,13 +12,12 @@ chai.use( chaiHttp )
|
||||
|
||||
|
||||
const { createUser, createPersonalAccessToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/users' )
|
||||
const { createStream, getStream, updateStream, deleteStream, getUserStreams, getStreamUsers, grantPermissionsStream, revokePermissionsStream } = require( '../services/streams' )
|
||||
const { createStream, getStream, updateStream, deleteStream, deleteStreams, getUserStreams, getStreamUsers, grantPermissionsStream, revokePermissionsStream } = require( '../services/streams' )
|
||||
const { createBranch, getBranchByNameAndStreamId, updateBranch, deleteBranchById } = require( '../services/branches' )
|
||||
const { createObject, createObjects } = require( '../services/objects' )
|
||||
const { createCommitByBranchName } = require( '../services/commits' )
|
||||
|
||||
describe( 'Streams @core-streams', ( ) => {
|
||||
|
||||
let userOne = {
|
||||
name: 'Dimitrie Stefanescu',
|
||||
email: 'didimitrie@gmail.com',
|
||||
@@ -100,7 +99,6 @@ describe( 'Streams @core-streams', ( ) => {
|
||||
} )
|
||||
|
||||
describe( 'Sharing: Grant & Revoke permissions', ( ) => {
|
||||
|
||||
before( async ( ) => {
|
||||
userTwo.id = await createUser( userTwo )
|
||||
} )
|
||||
@@ -200,7 +198,6 @@ describe( 'Streams @core-streams', ( ) => {
|
||||
su = await getStream( { streamId: s.id } )
|
||||
expect( su.updatedAt ).to.not.equal( s.updatedAt )
|
||||
} )
|
||||
|
||||
} )
|
||||
} )
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
const roles = require( `${appRoot}/modules/core/roles.js` )
|
||||
|
||||
const Users = ( ) => knex( 'users' )
|
||||
|
||||
// tableName, columnName that need migration
|
||||
const migrationTargets = [
|
||||
[ 'api_tokens' , 'owner' ],
|
||||
[ 'authorization_codes' , 'userId' ],
|
||||
[ 'branches' , 'authorId' ],
|
||||
[ 'commits' , 'author' ],
|
||||
[ 'file_uploads' , 'userId' ],
|
||||
[ 'personal_api_tokens' , 'userId' ],
|
||||
[ 'refresh_tokens' , 'userId' ],
|
||||
// [ 'server_acl' , 'userId' ], //userId is a PrimaryKey in this table, act accordingly
|
||||
[ 'server_apps' , 'authorId' ],
|
||||
[ 'server_invites' , 'inviterId' ],
|
||||
// [ 'stream_acl' , 'userId' ],//userId, with resourceId is a PrimaryKey in this table, act accordingly
|
||||
[ 'stream_activity' , 'userId' ],
|
||||
]
|
||||
|
||||
const migrateColumnValue = async( tableName, columnName, oldUser, newUser ) => {
|
||||
try {
|
||||
const query = knex( tableName ).where( { [columnName]: oldUser.id } ).update( { [columnName]: newUser.id } )
|
||||
console.log( `${query}` )
|
||||
await query
|
||||
} catch ( err ) {
|
||||
console.log( err )
|
||||
}
|
||||
}
|
||||
|
||||
const serverAclMigration = async ( { lowerUser, upperUser } ) => {
|
||||
const oldAcl = await knex( 'server_acl' ).where( { userId: upperUser.id } ).first()
|
||||
// if the old user was admin, make the target admin too
|
||||
if ( oldAcl.role === 'server:admin' ) await knex( 'server_acl' ).where( { userId: lowerUser.id } ).update( { role: 'server:admin' } )
|
||||
}
|
||||
|
||||
const _migrateSingleStreamAccess = async ( { lowerUser, upperUser, upperStreamAcl } ) => {
|
||||
const upperRole = roles.filter( r => r.name === upperStreamAcl.role )[0]
|
||||
const lowerAcl = await knex( 'stream_acl' ).where( { userId: lowerUser.id, resourceId: upperStreamAcl.resourceId } ).first()
|
||||
// see if the lowerUser has access to the stream
|
||||
if ( lowerAcl ) {
|
||||
// if the upper user had more access, migrate the lower user up
|
||||
const lowerRole = roles.filter( r => r.name === lowerAcl.role )[0]
|
||||
if ( lowerRole.weight < upperRole.weight )
|
||||
await knex( 'stream_acl' )
|
||||
.where( { userId: lowerUser.id, resourceId: upperStreamAcl.resourceId } )
|
||||
.update( { role: upperRole.name } )
|
||||
} else {
|
||||
// if it didn't have access, just add it
|
||||
let lowerStreamAcl = { ...upperStreamAcl }
|
||||
lowerStreamAcl.userId = lowerUser.id
|
||||
await knex( 'stream_acl' ).insert( lowerStreamAcl )
|
||||
}
|
||||
}
|
||||
|
||||
const streamAclMigration = async ( { lowerUser, upperUser } ) => {
|
||||
const upperAcl = await knex( 'stream_acl' ).where( { userId: upperUser.id } )
|
||||
|
||||
await Promise.all( upperAcl.map( async upperStreamAcl => await _migrateSingleStreamAccess( { lowerUser, upperUser, upperStreamAcl } ) ) )
|
||||
}
|
||||
|
||||
const createMigrations = ( { lowerUser, upperUser } ) => migrationTargets.map( ( [ tableName, columnName ] ) => {
|
||||
migrateColumnValue( tableName, columnName, upperUser,lowerUser ) } )
|
||||
|
||||
|
||||
const userByEmailQuery = email => Users( ).where( { email } )
|
||||
|
||||
const getDuplicateUsers = async ( ) => {
|
||||
let duplicates = await knex.raw( 'select lower(email) as lowered, count(id) as reg_count from users group by lowered having count(id) > 1' )
|
||||
return await Promise.all( duplicates.rows.map( async dup => {
|
||||
let lowerEmail = dup.lowered
|
||||
|
||||
let lowerUser = await userByEmailQuery( lowerEmail ).first( )
|
||||
// if no user found migrate to a random one?
|
||||
// TODO: decide 👆
|
||||
// my idea, take the first one and run with it
|
||||
if ( !lowerUser ) lowerUser = await Users( ).whereRaw( 'lower(email) = lower(?)',[ lowerEmail ] ).first()
|
||||
let upperUser = await Users( ).whereRaw( 'lower(email) = lower(?)',[ lowerEmail ] ).whereNot( { id: lowerUser.id } ).first( )
|
||||
return { lowerUser,upperUser }
|
||||
} ) )
|
||||
}
|
||||
|
||||
const runMigrations = async ( ) => {
|
||||
const duplicateUsers = await getDuplicateUsers( )
|
||||
console.log( duplicateUsers )
|
||||
await Promise.all( duplicateUsers.map( async userDouble => {
|
||||
const migrations = createMigrations( userDouble )
|
||||
await Promise.all( migrations.map( async migrationStep => await migrationStep ) )
|
||||
await serverAclMigration( userDouble )
|
||||
await streamAclMigration( userDouble )
|
||||
|
||||
// remove the now defunct user
|
||||
await userByEmailQuery( userDouble.upperUser.email ).delete( )
|
||||
} ) )
|
||||
}
|
||||
|
||||
( async function () {
|
||||
try {
|
||||
// await createData()
|
||||
await runMigrations()
|
||||
} catch ( err ) {
|
||||
console.log( err )
|
||||
} finally { process.exit() }
|
||||
}() )
|
||||
@@ -15,8 +15,9 @@
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true}],
|
||||
"babel-plugin-add-module-exports",
|
||||
"@babel/plugin-transform-classes"
|
||||
"@babel/plugin-transform-classes",
|
||||
["@babel/plugin-proposal-private-methods", { "loose": true }]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ module.exports = {
|
||||
'space-before-blocks': 'error',
|
||||
'space-infix-ops': 'error',
|
||||
'comma-dangle': [ 'error', 'never' ],
|
||||
'no-console': [ 'error', { allow: [ 'warn', 'error' ] } ],
|
||||
'no-console': [ 'warn', { allow: [ 'warn', 'error' ] } ],
|
||||
'space-unary-ops': 'error',
|
||||
'no-var': 'error',
|
||||
'no-alert': 'error',
|
||||
'no-param-reassign': 'warn',
|
||||
semi: [ 'error', 'never' ],
|
||||
quotes: [ 'error', 'single' ],
|
||||
eqeqeq: 'warn'
|
||||
eqeqeq: 'warn',
|
||||
'no-unused-vars': 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script defer src="demo.js"></script></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p class="text-sm">Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.interactions.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.interactions.toggleSectionBox()">Toggle Section Box</button>
|
||||
<button onclick="v.interactions.rotateCamera(undefined, undefined, false)">Rotate</button>
|
||||
<button onclick="viewerScreenshot()">Screenshot</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://latest.speckle.dev/streams/a6f96ea62c/objects/68d6535632dbf9aba06adae8a263c8e4"/>
|
||||
<button class="" onclick="loadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
View:
|
||||
<button onclick="v.interactions.rotateTo('top')">Top</button>
|
||||
<button onclick="v.interactions.rotateTo('front')">Front</button>
|
||||
<button onclick="v.interactions.rotateTo('back')">Back</button>
|
||||
<button onclick="v.interactions.rotateTo('left')">Left</button>
|
||||
<button onclick="v.interactions.rotateTo('right')">Right</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<html>
|
||||
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
|
||||
integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ=="
|
||||
crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
#renderer {
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script defer src="demo.js"></script></head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p class="text-sm">Click an object to select it. Double click it to focus on it. Press `esc` to clear the
|
||||
selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle
|
||||
its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.interactions.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.interactions.toggleSectionBox()">Toggle Section Box</button>
|
||||
<button onclick="v.interactions.rotateCamera(undefined, undefined, false)">Rotate</button>
|
||||
<button onclick="viewerScreenshot()">Screenshot</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%"
|
||||
value="https://latest.speckle.dev/streams/010b3af4c3/objects/a401baf38fe5809d0eb9d3c902a36e8f" />
|
||||
<button class="" onclick="loadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
View:
|
||||
<button onclick="v.interactions.rotateTo('top')">Top</button>
|
||||
<button onclick="v.interactions.rotateTo('front')">Front</button>
|
||||
<button onclick="v.interactions.rotateTo('back')">Back</button>
|
||||
<button onclick="v.interactions.rotateTo('left')">Left</button>
|
||||
<button onclick="v.interactions.rotateTo('right')">Right</button>
|
||||
</div>
|
||||
|
||||
<div class="twelve columns">
|
||||
Used Memory (MB): <span id="info-mem">-</span> /
|
||||
LoadProgress: <span id="info-progress">-</span> /
|
||||
ViewerBusy: <span id="info-busy">-</span> /
|
||||
Draw Calls: <span id="info-draws">-</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@speckle/viewer",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.3",
|
||||
"description": "A 3d viewer for Speckle, based on threejs.",
|
||||
"homepage": "https://speckle.systems",
|
||||
"repository": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"serve": "webpack serve --env dev --config webpack.config.example.js",
|
||||
"serve": "webpack serve --env dev --config webpack.config.example.js --port 9002",
|
||||
"dev": "webpack --progress --watch --env dev",
|
||||
"build": "webpack --env dev && webpack --env build",
|
||||
"prepublishOnly": "npm run build"
|
||||
@@ -29,32 +29,36 @@
|
||||
"threejs"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.12.10",
|
||||
"@babel/core": "7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/preset-env": "7.12.11",
|
||||
"@babel/preset-react": "7.12.10",
|
||||
"@babel/preset-typescript": "7.12.7",
|
||||
"@speckle/objectloader": "^2.0.0",
|
||||
"babel-jest": "26.6.3",
|
||||
"@babel/cli": "7.15.7",
|
||||
"@babel/core": "7.15.8",
|
||||
"@babel/eslint-parser": "^7.15.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-private-methods": "^7.14.5",
|
||||
"@babel/plugin-transform-classes": "^7.16.0",
|
||||
"@babel/preset-env": "7.15.8",
|
||||
"@babel/preset-react": "7.14.5",
|
||||
"@babel/preset-typescript": "7.15.0",
|
||||
"babel-jest": "27.2.5",
|
||||
"babel-loader": "^8.0.0-beta.4",
|
||||
"babel-plugin-add-module-exports": "1.0.4",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^7.26.0",
|
||||
"html-webpack-plugin": "^5.0.0-beta.4",
|
||||
"jest": "26.6.3",
|
||||
"mocha": "^4.0.1",
|
||||
"webpack": "5.11.0",
|
||||
"webpack-cli": "^4.3.1",
|
||||
"webpack-dev-server": "^3.11.1",
|
||||
"yargs": "^10.0.3"
|
||||
"eslint": "^8.0.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "27.2.5",
|
||||
"mocha": "^9.1.2",
|
||||
"webpack": "5.58.2",
|
||||
"webpack-cli": "^4.9.0",
|
||||
"webpack-dev-server": "^4.3.1",
|
||||
"yargs": "^17.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"camera-controls": "^1.28.0",
|
||||
"@speckle/objectloader": "^2.2.0",
|
||||
"camera-controls": "^1.33.1",
|
||||
"hold-event": "^0.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"three": "0.124.0"
|
||||
"rainbowvis.js": "^1.0.1",
|
||||
"three": "^0.134.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,48 @@ To build the library, you should run:
|
||||
npm run build
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Syntax and examples for supported API methods. The examples assume a Viewer instance named `v`.
|
||||
|
||||
### Load/Unload an object
|
||||
`v.loadObject( objectUrl )` / `v.unloadObject( objectUrl )`
|
||||
|
||||
Example: `v.loadObject( 'https://speckle.xyz/streams/3073b96e86/objects/e05c5834368931c9d9a4e2087b4da670' )`
|
||||
|
||||
### Get properties of loaded objects
|
||||
`v.getObjectsProperties()`
|
||||
|
||||
This returns a dictionary with `{ propertyName: propertyInfo }` elements. The property information provided is:
|
||||
- `type` ( == `'string'` / `'number'` / `'boolean'`): the property type
|
||||
- `objectCount` (int): How many objects in the scene have this property
|
||||
- `allValues` (array of `objectCount` elements): The values for this property of all objects that have this property
|
||||
- `minValue` - the smallest value (using `<` operator, works also on strings)
|
||||
- `maxValue` - the largest value
|
||||
- `uniqueValues` - a dictionary of `{ uniqueValue: occurenceCount }` elements, secifying how many objects have the property set to that specific value
|
||||
|
||||
### Filtering and coloring
|
||||
Those calls filter and color the objects loaded in the scene, and drops the previous applied filters (filtering is not additive).
|
||||
|
||||
Syntax: `await v.applyFilter( { filterBy, colorBy, ghostOthers } )`
|
||||
|
||||
The 3 optional parameters are:
|
||||
- `filterBy`: A dictionary that specify the filter. Elements are in the form `{ propertyName: propertyValueFilter }`. The propertyValueFilter can be one of:
|
||||
- A specific value: (only objects with that property value pass the filter)
|
||||
- An array of values: An object passes the filter if its value is in the array
|
||||
- A range of values, specified by `{ 'gte': value1, 'lte': value2 }` (greater than or equal, lower than or equal)
|
||||
- An exclusion list, specified by `{ 'not': excludedValuesArray }`
|
||||
|
||||
- `colorBy`: A dictionary that makes all objects colored based on a property value. Two types of coloring are supported:
|
||||
- Gradient (from a numeric property): `{ 'type': 'gradient', 'property': propertyName, 'minValue': propertyMinValue, 'maxValue': propertyMaxValue, 'gradientColors': [color1, color2] }`
|
||||
- Category (for coloring each unique value differently): `{ 'type': 'category', 'property': propertyName, 'values': { value1: color1, value2: color2, ... }, 'default': colorForAnyOtherValue }`. The `values` and the `default` parameters are optional: Random colors are generated if they are ommited.
|
||||
|
||||
- `ghostOthers`: A boolean (default `false`). If set to `true`, then the objects that are filtered out are actually shown with very low opacity, so that the remaining objects have a better context.
|
||||
|
||||
|
||||
To remove all filters: `await v.applyFilter( null )`
|
||||
|
||||
|
||||
## Community
|
||||
|
||||
If in trouble, the Speckle Community hangs out on [the forum](https://speckle.community). Do join and introduce yourself! We're happy to help.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/* eslint-disable */
|
||||
import Viewer from './modules/Viewer'
|
||||
|
||||
setInterval(() => {
|
||||
document.getElementById('info-mem').innerText = '' + Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)
|
||||
}, 100 )
|
||||
|
||||
let v = new Viewer( { container: document.getElementById( 'renderer' ), showStats: true } )
|
||||
v.on( 'load-progress', args => console.log( `Load progress ${args.progress} (on object ${args.id})` ) )
|
||||
v.on( 'load-progress', args => {
|
||||
document.getElementById('info-progress').innerText = `${Math.round(1000 * args.progress) / 1000 }`
|
||||
} )
|
||||
|
||||
window.v = v
|
||||
window.addEventListener( 'load', () => {
|
||||
@@ -16,8 +22,10 @@ window.addEventListener( 'load', () => {
|
||||
window.loadData = async function LoadData( url ) {
|
||||
url = url || document.getElementById( 'objectUrlInput' ).value
|
||||
localStorage.setItem( 'prevLoadUrl', url )
|
||||
let t0 = Date.now()
|
||||
await v.loadObject( url )
|
||||
}
|
||||
console.log(`Finished loading in: ${(Date.now() - t0) / 1000}`)
|
||||
}
|
||||
|
||||
v.on( 'select', objects => {
|
||||
console.info( `Selection event. Current selection count: ${objects.length}.` )
|
||||
|
||||
@@ -1,58 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p class="text-sm">Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.interactions.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.interactions.toggleSectionBox()">Toggle Section Box</button>
|
||||
<button onclick="v.interactions.rotateCamera(undefined, undefined, false)">Rotate</button>
|
||||
<button onclick="viewerScreenshot()">Screenshot</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://latest.speckle.dev/streams/a6f96ea62c/objects/68d6535632dbf9aba06adae8a263c8e4"/>
|
||||
<button class="" onclick="loadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
View:
|
||||
<button onclick="v.interactions.rotateTo('top')">Top</button>
|
||||
<button onclick="v.interactions.rotateTo('front')">Front</button>
|
||||
<button onclick="v.interactions.rotateTo('back')">Back</button>
|
||||
<button onclick="v.interactions.rotateTo('left')">Left</button>
|
||||
<button onclick="v.interactions.rotateTo('right')">Right</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<html>
|
||||
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
|
||||
integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ=="
|
||||
crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
#renderer {
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body xxxstyle="background: #2a2a2a">
|
||||
<div class="container" >
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p class="text-sm">Click an object to select it. Double click it to focus on it. Press `esc` to clear the
|
||||
selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle
|
||||
its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.interactions.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.toggleSectionBox()">Toggle Section Box</button>
|
||||
<button onclick="v.interactions.rotateCamera(undefined, undefined, false)">Rotate</button>
|
||||
<button onclick="viewerScreenshot()">Screenshot</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%"
|
||||
value="https://latest.speckle.dev/streams/010b3af4c3/objects/a401baf38fe5809d0eb9d3c902a36e8f" />
|
||||
<button class="" onclick="loadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.unloadAll()">Dispose Everything</button>
|
||||
View:
|
||||
<button onclick="v.toggleCameraProjection()">Ortho/Perspective</button>
|
||||
<button onclick="v.interactions.rotateTo()">3D</button>
|
||||
<button onclick="v.interactions.rotateTo('top')">Top</button>
|
||||
<button onclick="v.interactions.rotateTo('front')">Front</button>
|
||||
<button onclick="v.interactions.rotateTo('back')">Back</button>
|
||||
<button onclick="v.interactions.rotateTo('left')">Left</button>
|
||||
<button onclick="v.interactions.rotateTo('right')">Right</button>
|
||||
</div>
|
||||
|
||||
<div class="twelve columns">
|
||||
Used Memory (MB): <span id="info-mem">-</span> /
|
||||
LoadProgress: <span id="info-progress">-</span> /
|
||||
ViewerBusy: <span id="info-busy">-</span> /
|
||||
Draw Calls: <span id="info-draws">-</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
import Viewer from './modules/Viewer'
|
||||
import Converter from './modules/Converter'
|
||||
import Converter from './modules/converter/Converter'
|
||||
|
||||
export { Viewer, Converter }
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as THREE from 'three'
|
||||
import Rainbow from 'rainbowvis.js'
|
||||
|
||||
export default class FilteringManager {
|
||||
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
this.WireframeMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0x7080A0,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.04,
|
||||
wireframe: true
|
||||
} )
|
||||
// console.log(this.viewer.sectionBox.planes)
|
||||
|
||||
this.ColoredMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0x7080A0,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: false,
|
||||
clippingPlanes: this.viewer.sectionBox.planes
|
||||
} )
|
||||
|
||||
this.colorLegend = {}
|
||||
}
|
||||
|
||||
filterAndColorObject( obj, filter ) {
|
||||
if ( !filter )
|
||||
return obj.clone()
|
||||
|
||||
if ( !this.passesFilter( obj.userData, filter.filterBy ) )
|
||||
{
|
||||
if ( filter.ghostOthers ) {
|
||||
let clone = obj.clone()
|
||||
this.ghostObject( clone )
|
||||
return clone
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
let clone = obj.clone()
|
||||
if ( filter.colorBy ) {
|
||||
if ( filter.colorBy.type === 'category' ) {
|
||||
let newMaterial = this.colorWithCategory( obj, filter.colorBy )
|
||||
this.setMaterial( clone, newMaterial )
|
||||
} else if ( filter.colorBy.type === 'gradient' ) {
|
||||
let newMaterial = this.colorWithGradient( obj, filter.colorBy )
|
||||
this.setMaterial( clone, newMaterial )
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
ghostObject( clone ) {
|
||||
clone.userData = { hidden: true }
|
||||
|
||||
if ( clone.type === 'Group' ) {
|
||||
for ( let child of clone.children ) {
|
||||
this.ghostObject( child )
|
||||
}
|
||||
} else if ( clone.type === 'Mesh' ) {
|
||||
clone.material = clone.material.clone()
|
||||
clone.material.clippingPlanes = null
|
||||
clone.material.transparent = true
|
||||
clone.material.opacity = 0.05
|
||||
} else {
|
||||
clone.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
setMaterial ( clone, material ) {
|
||||
if ( clone.type === 'Group' ) {
|
||||
for ( let child of clone.children ) {
|
||||
this.setMaterial( child, material )
|
||||
}
|
||||
} else if ( clone.material !== undefined ) {
|
||||
clone.material = material
|
||||
clone.material.clippingPlanes = this.viewer.sectionBox.planes
|
||||
}
|
||||
}
|
||||
|
||||
getObjectProperty( obj, property ) {
|
||||
if ( !property ) return
|
||||
let keyParts = property.split( '.' )
|
||||
let crtObj = obj
|
||||
for ( let i = 0; i < keyParts.length - 1; i++ ) {
|
||||
if ( !( keyParts[i] in crtObj ) ) return
|
||||
crtObj = crtObj[ keyParts[i] ]
|
||||
if ( crtObj.constructor !== Object ) return
|
||||
}
|
||||
let attributeName = keyParts[ keyParts.length - 1 ]
|
||||
return crtObj[ attributeName ]
|
||||
}
|
||||
|
||||
colorWithCategory( threejsObj, colors ) {
|
||||
let obj = threejsObj.userData
|
||||
let defaultValue = colors.default
|
||||
let color = defaultValue
|
||||
let objValue = this.getObjectProperty( obj, colors.property )
|
||||
let customPallete = colors.values || {}
|
||||
if ( objValue in customPallete ) {
|
||||
color = customPallete[ objValue ]
|
||||
}
|
||||
|
||||
if ( !color ) {
|
||||
// compute value hash
|
||||
let objValueAsString = '' + objValue
|
||||
let hash = 0
|
||||
for( let i = 0; i < objValueAsString.length; i++ ) {
|
||||
let chr = objValueAsString.charCodeAt( i )
|
||||
hash = ( ( hash << 5 ) - hash ) + chr
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
hash = Math.abs( hash )
|
||||
let colorHue = hash % 360
|
||||
color = `hsl(${colorHue}, 50%, 30%)`
|
||||
}
|
||||
|
||||
if ( objValue !== undefined && objValue !== null )
|
||||
this.colorLegend[ objValue.toString() ] = color
|
||||
|
||||
let material = this.ColoredMaterial.clone()
|
||||
material.color = new THREE.Color( color )
|
||||
return material
|
||||
}
|
||||
|
||||
colorWithGradient( threejsObj, colors ) {
|
||||
let obj = threejsObj.userData
|
||||
let rainbow = new Rainbow( )
|
||||
if ( 'minValue' in colors && 'maxValue' in colors )
|
||||
rainbow.setNumberRange( colors.minValue, colors.maxValue )
|
||||
if ( 'gradientColors' in colors )
|
||||
rainbow.setSpectrum( ...colors.gradientColors )
|
||||
|
||||
let objValue = this.getObjectProperty( obj, colors.property )
|
||||
objValue = Number( objValue )
|
||||
if ( Number.isNaN( objValue ) ) {
|
||||
return this.WireframeMaterial
|
||||
}
|
||||
|
||||
let material = this.ColoredMaterial.clone()
|
||||
material.color = new THREE.Color( `#${rainbow.colourAt( objValue )}` )
|
||||
return material
|
||||
}
|
||||
|
||||
passesFilter( obj, filterBy ) {
|
||||
if ( !filterBy ) return true
|
||||
for ( let filterKey in filterBy ) {
|
||||
let objValue = this.getObjectProperty( obj, filterKey )
|
||||
|
||||
let passesFilter = this.filterValue( objValue, filterBy[ filterKey ] )
|
||||
if ( !passesFilter ) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
filterValue( objValue, valueFilter ) {
|
||||
// Array value filter means it can be any value from the array
|
||||
if ( Array.isArray( valueFilter ) )
|
||||
return valueFilter.includes( objValue )
|
||||
|
||||
// Dictionary value filter can specify ranges with `lte` and `gte` fields (LowerThanOrEqual, GreaterThanOrEqual)
|
||||
if ( valueFilter.constructor === Object ) {
|
||||
if ( 'not' in valueFilter && Array.isArray( valueFilter.not ) ) {
|
||||
if ( valueFilter.not.includes( objValue ) )
|
||||
return false
|
||||
}
|
||||
if ( 'lte' in valueFilter && objValue > valueFilter.lte )
|
||||
return false
|
||||
if ( 'gte' in valueFilter && objValue < valueFilter.gte )
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Can also filter by specific value
|
||||
return objValue === valueFilter
|
||||
}
|
||||
|
||||
initFilterOperation() {
|
||||
this.colorLegend = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import * as THREE from 'three'
|
||||
import SectionBox from './SectionBox'
|
||||
import SelectionHelper from './SelectionHelper'
|
||||
|
||||
export default class InteractionHandler {
|
||||
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
|
||||
this.sectionBox = new SectionBox( this.viewer )
|
||||
this.sectionBox.toggle() // switch off
|
||||
|
||||
this.preventSelection = false
|
||||
|
||||
this.selectionHelper = new SelectionHelper( this.viewer, { subset: this.viewer.sceneManager.userObjects, sectionBox: this.sectionBox } )
|
||||
|
||||
this.selectionHelper = new SelectionHelper( this.viewer, { sectionBox: this.sectionBox, hover: false } )
|
||||
this.selectionMeshMaterial = new THREE.MeshLambertMaterial( { color: 0x0B55D2, emissive: 0x0B55D2, side: THREE.DoubleSide } )
|
||||
this.selectionMeshMaterial.clippingPlanes = this.sectionBox.planes
|
||||
|
||||
this.selectionMeshMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
||||
// console.log(this.viewer.sceneManager.allObjects)
|
||||
this.selectionLineMaterial = new THREE.LineBasicMaterial( { color: 0x0B55D2 } )
|
||||
this.selectionLineMaterial.clippingPlanes = this.sectionBox.planes
|
||||
this.selectionLineMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
||||
|
||||
this.selectionEdgesMaterial = new THREE.LineBasicMaterial( { color: 0x23F3BD } )
|
||||
this.selectionEdgesMaterial.clippingPlanes = this.sectionBox.planes
|
||||
this.selectionEdgesMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
||||
|
||||
this.selectedObjects = new THREE.Group()
|
||||
this.viewer.scene.add( this.selectedObjects )
|
||||
@@ -31,18 +26,29 @@ export default class InteractionHandler {
|
||||
this.selectionHelper.on( 'object-doubleclicked', this._handleDoubleClick.bind( this ) )
|
||||
this.selectionHelper.on( 'object-clicked', this._handleSelect.bind( this ) )
|
||||
|
||||
this.viewer.sceneManager.materials.forEach( mat => mat.clippingPlanes = this.sectionBox.planes )
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if( e.key === 'Escape' && this.viewer.mouseOverRenderer ) {
|
||||
this.deselectObjects()
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
_handleDoubleClick( objs ) {
|
||||
if ( !objs || objs.length === 0 ) this.zoomExtents()
|
||||
if ( !objs || objs.length === 0 ) {
|
||||
if( this.viewer.sectionBox.display.visible ) {
|
||||
this.zoomToObject( this.viewer.sectionBox.cube )
|
||||
} else {
|
||||
this.zoomExtents()
|
||||
}
|
||||
}
|
||||
else this.zoomToObject( objs[0].object )
|
||||
this.viewer.needsRender = true
|
||||
this.viewer.emit( 'object-doubleclicked', objs && objs.length !== 0 ? objs[0].object : null )
|
||||
}
|
||||
|
||||
_handleSelect( objs ) {
|
||||
if ( this.preventSelection ) return
|
||||
if( this.viewer.cameraHandler.orbiting ) return
|
||||
if( this.preventSelection ) return
|
||||
|
||||
if ( objs.length === 0 ) {
|
||||
this.deselectObjects()
|
||||
@@ -50,43 +56,70 @@ export default class InteractionHandler {
|
||||
}
|
||||
|
||||
if ( !this.selectionHelper.multiSelect ) this.deselectObjects()
|
||||
|
||||
|
||||
let selType = objs[0].object.type
|
||||
|
||||
let rootBlock = null
|
||||
if ( objs[0].object.parent?.userData?.speckle_type?.toLowerCase().includes( 'blockinstance' ) ) {
|
||||
selType = 'Block'
|
||||
rootBlock = this.getParentBlock( objs[0].object.parent )
|
||||
}
|
||||
|
||||
switch ( selType ) {
|
||||
case 'Block':
|
||||
// TODO: maybe just leave the bounding box for now
|
||||
break
|
||||
case 'Mesh':
|
||||
this.selectedObjects.add( new THREE.Mesh( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
||||
break
|
||||
case 'Line':
|
||||
this.selectedObjects.add( new THREE.Line( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
||||
break
|
||||
case 'Point':
|
||||
console.warn( 'Point selection not implemented.' )
|
||||
return // exit the whole func here, points cause all sorts of trouble when being selected (ie, bbox stuff)
|
||||
case 'Block': {
|
||||
let blockObjs = this.getBlockObjectsCloned( rootBlock )
|
||||
for( let child of blockObjs ) {
|
||||
child.material = this.selectionMeshMaterial
|
||||
this.selectedObjects.add( child )
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Mesh':
|
||||
this.selectedObjects.add( new THREE.Mesh( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
||||
break
|
||||
case 'Line':
|
||||
this.selectedObjects.add( new THREE.Line( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
||||
break
|
||||
case 'Point':
|
||||
console.warn( 'Point selection not implemented.' )
|
||||
return // exit the whole func here, points cause all sorts of trouble when being selected (ie, bbox stuff)
|
||||
}
|
||||
|
||||
this.selectedObjectsUserData.push( objs[0].object.userData )
|
||||
|
||||
let box
|
||||
if ( selType === 'Block' ) {
|
||||
box = new THREE.BoxHelper( objs[0].object.parent, 0x23F3BD )
|
||||
this.selectedObjectsUserData.push( rootBlock.userData )
|
||||
box = new THREE.BoxHelper( rootBlock, 0x23F3BD )
|
||||
} else {
|
||||
this.selectedObjectsUserData.push( objs[0].object.userData )
|
||||
box = new THREE.BoxHelper( objs[0].object, 0x23F3BD )
|
||||
}
|
||||
|
||||
box.material = this.selectionEdgesMaterial
|
||||
this.selectedObjects.add( box )
|
||||
this.viewer.needsRender = true
|
||||
this.viewer.emit( 'select', this.selectedObjectsUserData )
|
||||
}
|
||||
|
||||
getParentBlock( block ) {
|
||||
if( block.parent?.userData?.speckle_type?.toLowerCase().includes( 'blockinstance' ) ) {
|
||||
return this.getParentBlock( block.parent )
|
||||
}
|
||||
else return block
|
||||
}
|
||||
|
||||
getBlockObjectsCloned( block, objects = [] ) {
|
||||
for( let child of block.children ) {
|
||||
if( child instanceof THREE.Group ) {
|
||||
objects.push( ...this.getBlockObjectsCloned( child ) )
|
||||
} else {
|
||||
objects.push( child.clone() )
|
||||
}
|
||||
}
|
||||
for( let child of objects ) {
|
||||
child.geometry = child.geometry.clone().applyMatrix4( block.matrix )
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
deselectObjects() {
|
||||
this.selectedObjects.clear()
|
||||
this.selectedObjectsUserData = []
|
||||
@@ -94,92 +127,85 @@ export default class InteractionHandler {
|
||||
this.viewer.emit( 'select', this.selectedObjectsUserData )
|
||||
}
|
||||
|
||||
toggleSectionBox() {
|
||||
this.sectionBox.toggle()
|
||||
if ( this.sectionBox.display.visible ) {
|
||||
if ( this.selectedObjects.children.length === 0 ) {
|
||||
this.sectionBox.setBox( this.viewer.sceneManager.getSceneBoundingBox() )
|
||||
this.zoomExtents()
|
||||
}
|
||||
else {
|
||||
let box = new THREE.Box3().setFromObject( this.selectedObjects )
|
||||
this.sectionBox.setBox( box )
|
||||
this.zoomToBox( box )
|
||||
}
|
||||
} else {
|
||||
this.preventSelection = false
|
||||
}
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
hideSectionBox() {
|
||||
if ( !this.sectionBox.display.visible ) return
|
||||
this.toggleSectionBox( )
|
||||
}
|
||||
|
||||
showSectionBox() {
|
||||
if ( this.sectionBox.display.visible ) return
|
||||
this.toggleSectionBox( )
|
||||
}
|
||||
|
||||
zoomToObject( target, fit = 1.2, transition = true ) {
|
||||
const box = new THREE.Box3().setFromObject( target )
|
||||
this.zoomToBox( box, fit, transition )
|
||||
}
|
||||
|
||||
zoomExtents( fit = 1.2, transition = true ) {
|
||||
if ( this.sectionBox.display.visible ) {
|
||||
this.zoomToObject( this.sectionBox.boxMesh )
|
||||
if ( this.viewer.sectionBox.display.visible ) {
|
||||
this.zoomToObject( this.viewer.sectionBox.cube )
|
||||
return
|
||||
}
|
||||
if ( this.viewer.sceneManager.objects.length === 0 ) {
|
||||
if ( this.viewer.sceneManager.sceneObjects.allObjects.length === 0 ) {
|
||||
let box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
this.zoomToBox( box, fit, transition )
|
||||
return
|
||||
}
|
||||
|
||||
let box = new THREE.Box3().setFromObject( this.viewer.sceneManager.userObjects )
|
||||
let box = new THREE.Box3().setFromObject( this.viewer.sceneManager.sceneObjects.allObjects )
|
||||
this.zoomToBox( box, fit, transition )
|
||||
this.viewer.controls.setBoundary( box )
|
||||
// this.viewer.controls.setBoundary( box )
|
||||
}
|
||||
|
||||
zoomToBox( box, fit = 1.2, transition = true ) {
|
||||
if( box.max.x === Infinity || box.max.x === -Infinity ) {
|
||||
box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
}
|
||||
const fitOffset = fit
|
||||
|
||||
const size = box.getSize( new THREE.Vector3() )
|
||||
let target = new THREE.Sphere()
|
||||
box.getBoundingSphere( target )
|
||||
target.radius = target.radius * fitOffset
|
||||
|
||||
this.viewer.controls.fitToSphere( target, transition )
|
||||
|
||||
|
||||
const maxSize = Math.max( size.x, size.y, size.z )
|
||||
const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * this.viewer.camera.fov / 360 ) )
|
||||
const fitWidthDistance = fitHeightDistance / this.viewer.camera.aspect
|
||||
const camFov = this.viewer.cameraHandler.camera.fov ? this.viewer.cameraHandler.camera.fov : 55
|
||||
const camAspect = this.viewer.cameraHandler.camera.aspect ? this.viewer.cameraHandler.camera.aspect : 1.2
|
||||
const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * camFov / 360 ) )
|
||||
const fitWidthDistance = fitHeightDistance / camAspect
|
||||
const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance )
|
||||
|
||||
this.viewer.cameraHandler.controls.fitToSphere( target, transition )
|
||||
|
||||
this.viewer.controls.minDistance = distance / 100
|
||||
this.viewer.controls.maxDistance = distance * 100
|
||||
this.viewer.camera.near = distance / 100
|
||||
this.viewer.camera.far = distance * 100
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows camera to go "underneath" or not. By default, this function will set
|
||||
* the max polar angle to Pi, allowing the camera to look from down upwards.
|
||||
* @param {[type]} angle [description]
|
||||
*/
|
||||
setMaxPolarAngle( angle = Math.PI ) {
|
||||
this.viewer.controls.maxPolarAngle = angle
|
||||
this.viewer.cameraHandler.controls.minDistance = distance / 100
|
||||
this.viewer.cameraHandler.controls.maxDistance = distance * 100
|
||||
this.viewer.cameraHandler.camera.near = distance / 100
|
||||
this.viewer.cameraHandler.camera.far = distance * 100
|
||||
this.viewer.cameraHandler.camera.updateProjectionMatrix()
|
||||
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' ) {
|
||||
this.viewer.cameraHandler.orthoCamera.far = distance * 100
|
||||
this.viewer.cameraHandler.orthoCamera.updateProjectionMatrix()
|
||||
|
||||
// fit the camera inside, so we don't have clipping plane issues.
|
||||
// WIP implementation
|
||||
let camPos = this.viewer.cameraHandler.orthoCamera.position
|
||||
let dist = target.distanceToPoint( camPos )
|
||||
if( dist < 0 ) {
|
||||
dist *= -1
|
||||
this.viewer.cameraHandler.controls.setPosition( camPos.x + dist, camPos.y + dist, camPos.z + dist )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rotateCamera( azimuthAngle = 0.261799, polarAngle = 0, transition = true ) {
|
||||
this.viewer.controls.rotate( azimuthAngle, polarAngle, transition )
|
||||
this.viewer.cameraHandler.controls.rotate( azimuthAngle, polarAngle, transition )
|
||||
}
|
||||
|
||||
screenshot() {
|
||||
return this.viewer.renderer.domElement.toDataURL( 'image/png' )
|
||||
let sectionBoxVisible = this.viewer.sectionBox.display.visible
|
||||
if( sectionBoxVisible ) {
|
||||
this.viewer.sectionBox.displayOff()
|
||||
this.viewer.needsRender = true
|
||||
this.viewer.render()
|
||||
}
|
||||
const screenshot = this.viewer.renderer.domElement.toDataURL( 'image/png' )
|
||||
if( sectionBoxVisible ) {
|
||||
this.viewer.sectionBox.displayOn()
|
||||
}
|
||||
return screenshot
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,36 +220,65 @@ export default class InteractionHandler {
|
||||
const DEG180 = Math.PI
|
||||
|
||||
switch ( side ) {
|
||||
case 'front':
|
||||
this.viewer.controls.rotateTo( 0, DEG90, transition )
|
||||
break
|
||||
case 'front':
|
||||
this.viewer.cameraHandler.controls.rotateTo( 0, DEG90, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case 'back':
|
||||
this.viewer.controls.rotateTo( DEG180, DEG90, transition )
|
||||
break
|
||||
case 'back':
|
||||
this.viewer.cameraHandler.controls.rotateTo( DEG180, DEG90, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case 'up':
|
||||
case 'top':
|
||||
this.viewer.controls.rotateTo( 0, 0, transition )
|
||||
break
|
||||
case 'up':
|
||||
case 'top':
|
||||
this.viewer.cameraHandler.controls.rotateTo( 0, 0, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case 'down':
|
||||
case 'bottom':
|
||||
this.viewer.controls.rotateTo( 0, DEG180, transition )
|
||||
break
|
||||
case 'down':
|
||||
case 'bottom':
|
||||
this.viewer.cameraHandler.controls.rotateTo( 0, DEG180, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case 'right':
|
||||
this.viewer.controls.rotateTo( DEG90, DEG90, transition )
|
||||
break
|
||||
case 'right':
|
||||
this.viewer.cameraHandler.controls.rotateTo( DEG90, DEG90, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case 'left':
|
||||
this.viewer.controls.rotateTo( -DEG90, DEG90, transition )
|
||||
break
|
||||
case 'left':
|
||||
this.viewer.cameraHandler.controls.rotateTo( -DEG90, DEG90, transition )
|
||||
if( this.viewer.cameraHandler.activeCam.name === 'ortho' )
|
||||
this.viewer.cameraHandler.disableRotations()
|
||||
break
|
||||
|
||||
case '3d':
|
||||
case '3D':
|
||||
default: {
|
||||
let box
|
||||
if ( this.viewer.sceneManager.sceneObjects.allObjects.children.length === 0 )
|
||||
box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
else
|
||||
box = new THREE.Box3().setFromObject( this.viewer.sceneManager.sceneObjects.allObjects )
|
||||
if( box.max.x === Infinity || box.max.x === -Infinity ) {
|
||||
box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
}
|
||||
this.viewer.cameraHandler.controls.setPosition( box.max.x, box.max.y, box.max.z, transition )
|
||||
this.zoomExtents()
|
||||
this.viewer.cameraHandler.enableRotations()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getViews() {
|
||||
return this.viewer.sceneManager.views.map( v => { return { name: v.applicationId, id: v.id } } )
|
||||
return this.viewer.sceneManager.views.map( v => { return { name: v.applicationId, id: v.id, view: v } } )
|
||||
}
|
||||
|
||||
setView( id, transition = true ) {
|
||||
@@ -237,11 +292,11 @@ export default class InteractionHandler {
|
||||
let target = view.target
|
||||
let position = view.origin
|
||||
|
||||
this.viewer.controls.setLookAt( position.x, position.y, position.z, target.x, target.y, target.z, transition )
|
||||
this.viewer.cameraHandler.activeCam.controls.setLookAt( position.x, position.y, position.z, target.x, target.y, target.z, transition )
|
||||
}
|
||||
|
||||
setLookAt( position, target, transition = true ) {
|
||||
if ( !position || !target ) return
|
||||
this.viewer.controls.setLookAt( position.x, position.y, position.z, target.x, target.y, target.z, transition )
|
||||
this.viewer.cameraHandler.activeCam.controls.setLookAt( position.x, position.y, position.z, target.x, target.y, target.z, transition )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as THREE from 'three'
|
||||
import debounce from 'lodash.debounce'
|
||||
import SceneObjects from './SceneObjects'
|
||||
|
||||
/**
|
||||
* Manages objects and provides some convenience methods to focus on the entire scene, or one specific object.
|
||||
@@ -9,18 +10,9 @@ export default class SceneObjectManager {
|
||||
constructor( viewer, skipPostLoad = false ) {
|
||||
this.viewer = viewer
|
||||
this.scene = viewer.scene
|
||||
this.userObjects = new THREE.Group()
|
||||
this.solidObjects = new THREE.Group()
|
||||
this.lineObjects = new THREE.Group()
|
||||
this.pointObjects = new THREE.Group()
|
||||
this.transparentObjects = new THREE.Group()
|
||||
this.views = []
|
||||
|
||||
this.userObjects.add( this.solidObjects )
|
||||
this.userObjects.add( this.transparentObjects )
|
||||
this.userObjects.add( this.lineObjects )
|
||||
this.userObjects.add( this.pointObjects )
|
||||
this.scene.add( this.userObjects )
|
||||
this.sceneObjects = new SceneObjects( viewer )
|
||||
|
||||
this.solidMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0x8D9194,
|
||||
@@ -28,7 +20,8 @@ export default class SceneObjectManager {
|
||||
roughness: 1,
|
||||
metalness: 0,
|
||||
side: THREE.DoubleSide,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture,
|
||||
clippingPlanes: this.viewer.sectionBox.planes
|
||||
} )
|
||||
|
||||
this.transparentMaterial = new THREE.MeshStandardMaterial( {
|
||||
@@ -39,34 +32,41 @@ export default class SceneObjectManager {
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture,
|
||||
clippingPlanes: this.viewer.sectionBox.planes
|
||||
} )
|
||||
|
||||
this.solidVertexMaterial = new THREE.MeshBasicMaterial( {
|
||||
color: 0xffffff,
|
||||
vertexColors: THREE.VertexColors,
|
||||
side: THREE.DoubleSide,
|
||||
reflectivity: 0
|
||||
reflectivity: 0,
|
||||
clippingPlanes: this.viewer.sectionBox.planes
|
||||
} )
|
||||
|
||||
this.lineMaterial = new THREE.LineBasicMaterial( { color: 0x7F7F7F } )
|
||||
this.pointMaterial = new THREE.PointsMaterial(
|
||||
{ size: 2, sizeAttenuation: false, color: 0x7F7F7F }
|
||||
)
|
||||
this.lineMaterial = new THREE.LineBasicMaterial( { color: 0x7F7F7F, clippingPlanes: this.viewer.sectionBox.planes } )
|
||||
|
||||
this.pointMaterial = new THREE.PointsMaterial( { size: 2, sizeAttenuation: false, color: 0x7F7F7F, clippingPlanes: this.viewer.sectionBox.planes } )
|
||||
|
||||
this.pointVertexColorsMaterial = new THREE.PointsMaterial( {
|
||||
size: 2, sizeAttenuation: false, vertexColors: true
|
||||
} )
|
||||
this.pointVertexColorsMaterial = new THREE.PointsMaterial( { size: 2, sizeAttenuation: false, vertexColors: true, clippingPlanes: this.viewer.sectionBox.planes } )
|
||||
|
||||
this.objectIds = []
|
||||
this.postLoad = debounce( () => { this._postLoadFunction() }, 200 )
|
||||
this.postLoad = debounce( () => { this._postLoadFunction() }, 20, { maxWait: 5000 } )
|
||||
this.skipPostLoad = skipPostLoad
|
||||
|
||||
this.loaders = []
|
||||
}
|
||||
|
||||
get objects() {
|
||||
return [ ...this.solidObjects.children, ...this.transparentObjects.children, ...this.lineObjects.children, ...this.pointObjects.children ]
|
||||
get allObjects() {
|
||||
return [ ...this.sceneObjects.allSolidObjects.children, ...this.sceneObjects.allTransparentObjects.children, ...this.sceneObjects.allLineObjects.children, ...this.sceneObjects.allPointObjects.children ]
|
||||
}
|
||||
|
||||
get filteredObjects() {
|
||||
let ret = []
|
||||
for ( let objectGroup of this.sceneObjects.objectsInScene.children ) {
|
||||
if ( objectGroup.name === 'GroupedSolidObjects' )
|
||||
continue
|
||||
ret.push( ...objectGroup.children )
|
||||
}
|
||||
return ret.filter( obj => !obj.userData.hidden )
|
||||
}
|
||||
|
||||
get materials() {
|
||||
@@ -84,6 +84,8 @@ export default class SceneObjectManager {
|
||||
addObject( wrapper, addToScene = true ) {
|
||||
if ( !wrapper || !wrapper.bufferGeometry ) return
|
||||
|
||||
// this.postLoad()
|
||||
|
||||
switch ( wrapper.geometryType ) {
|
||||
case 'View':
|
||||
this.views.push( wrapper.meta )
|
||||
@@ -105,10 +107,9 @@ export default class SceneObjectManager {
|
||||
return this.addBlock( wrapper, addToScene )
|
||||
}
|
||||
|
||||
this.postLoad()
|
||||
}
|
||||
|
||||
addSolid( wrapper, addToScene = true ) {
|
||||
addSolid( wrapper, _addToScene = true ) {
|
||||
// Do we have a defined material?
|
||||
if ( wrapper.meta.renderMaterial ) {
|
||||
let renderMat = wrapper.meta.renderMaterial
|
||||
@@ -117,7 +118,7 @@ export default class SceneObjectManager {
|
||||
// Is it a transparent material?
|
||||
if ( renderMat.opacity !== 1 ) {
|
||||
let material = this.transparentMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.interactions.sectionBox.planes
|
||||
material.clippingPlanes = this.viewer.sectionBox.planes
|
||||
|
||||
material.color = color
|
||||
material.opacity = renderMat.opacity !== 0 ? renderMat.opacity : 0.2
|
||||
@@ -126,7 +127,7 @@ export default class SceneObjectManager {
|
||||
// It's not a transparent material!
|
||||
} else {
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.interactions.sectionBox.planes
|
||||
material.clippingPlanes = this.viewer.sectionBox.planes
|
||||
|
||||
material.color = color
|
||||
material.metalness = renderMat.metalness
|
||||
@@ -139,7 +140,7 @@ export default class SceneObjectManager {
|
||||
} else {
|
||||
// If we don't have defined material, just use the default
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.interactions.sectionBox.planes
|
||||
material.clippingPlanes = this.viewer.sectionBox.planes
|
||||
|
||||
return this.addSingleSolid( wrapper, material )
|
||||
}
|
||||
@@ -147,11 +148,12 @@ export default class SceneObjectManager {
|
||||
|
||||
addSingleSolid( wrapper, material, addToScene = true ) {
|
||||
const mesh = new THREE.Mesh( wrapper.bufferGeometry, material ? material : this.solidMaterial )
|
||||
// mesh.matrixAutoUpdate = false
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
if ( addToScene ) {
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.solidObjects.add( mesh )
|
||||
// this.objectIds.push( mesh.uuid )
|
||||
this.sceneObjects.allSolidObjects.add( mesh )
|
||||
}
|
||||
return mesh
|
||||
}
|
||||
@@ -161,8 +163,8 @@ export default class SceneObjectManager {
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
if ( addToScene ) {
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.transparentObjects.add( mesh )
|
||||
// this.objectIds.push( mesh.uuid )
|
||||
this.sceneObjects.allTransparentObjects.add( mesh )
|
||||
}
|
||||
return mesh
|
||||
}
|
||||
@@ -172,8 +174,8 @@ export default class SceneObjectManager {
|
||||
line.userData = wrapper.meta
|
||||
line.uuid = wrapper.meta.id
|
||||
if ( addToScene ) {
|
||||
this.objectIds.push( line.uuid )
|
||||
this.lineObjects.add( line )
|
||||
// this.objectIds.push( line.uuid )
|
||||
this.sceneObjects.allLineObjects.add( line )
|
||||
}
|
||||
return line
|
||||
}
|
||||
@@ -183,8 +185,8 @@ export default class SceneObjectManager {
|
||||
dot.userData = wrapper.meta
|
||||
dot.uuid = wrapper.meta.id
|
||||
if ( addToScene ) {
|
||||
this.objectIds.push( dot.uuid )
|
||||
this.pointObjects.add( dot )
|
||||
// this.objectIds.push( dot.uuid )
|
||||
this.sceneObjects.allPointObjects.add( dot )
|
||||
}
|
||||
return dot
|
||||
}
|
||||
@@ -199,7 +201,8 @@ export default class SceneObjectManager {
|
||||
|
||||
this._normaliseColor( color )
|
||||
let material = this.pointMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.interactions.sectionBox.planes
|
||||
material.clippingPlanes = this.viewer.sectionBox.planes
|
||||
// material.clippingPlanes = this.viewer.interactions.sectionBox.planes
|
||||
|
||||
material.color = color
|
||||
|
||||
@@ -211,8 +214,8 @@ export default class SceneObjectManager {
|
||||
clouds.userData = wrapper.meta
|
||||
clouds.uuid = wrapper.meta.id
|
||||
if ( addToScene ) {
|
||||
this.objectIds.push( clouds.uuid )
|
||||
this.pointObjects.add( clouds )
|
||||
// this.objectIds.push( clouds.uuid )
|
||||
this.sceneObjects.allPointObjects.add( clouds )
|
||||
}
|
||||
return clouds
|
||||
}
|
||||
@@ -232,15 +235,28 @@ export default class SceneObjectManager {
|
||||
if ( addToScene ) {
|
||||
// Note: only apply the scale transform if this block is going to be added to the scene. otherwise it means it's a child of a nested block.
|
||||
group.applyMatrix4( wrapper.extras.scaleMatrix )
|
||||
this.objectIds.push()
|
||||
this.solidObjects.add( group )
|
||||
// this.objectIds.push()
|
||||
this.sceneObjects.allSolidObjects.add( group )
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
removeObject( id ) {
|
||||
// TODO
|
||||
async removeImportedObject( importedUrl ) {
|
||||
for ( let objGroup of this.sceneObjects.allObjects.children ) {
|
||||
let toRemove = objGroup.children.filter( obj => obj.userData?.__importedUrl === importedUrl )
|
||||
toRemove.forEach( obj => {
|
||||
if ( obj.material )
|
||||
obj.material.dispose()
|
||||
if ( obj.geometry )
|
||||
obj.geometry.dispose()
|
||||
objGroup.remove( obj )
|
||||
} )
|
||||
|
||||
}
|
||||
this.views = this.views.filter( v => v.__importedUrl !== importedUrl )
|
||||
|
||||
await this.sceneObjects.applyFilter( undefined, true )
|
||||
}
|
||||
|
||||
removeAllObjects() {
|
||||
@@ -249,24 +265,25 @@ export default class SceneObjectManager {
|
||||
obj.geometry.dispose()
|
||||
}
|
||||
}
|
||||
this.solidObjects.clear()
|
||||
this.transparentObjects.clear()
|
||||
this.lineObjects.clear()
|
||||
this.pointObjects.clear()
|
||||
this.sceneObjects.allSolidObjects.clear()
|
||||
this.sceneObjects.allTransparentObjects.clear()
|
||||
this.sceneObjects.allLineObjects.clear()
|
||||
this.sceneObjects.allPointObjects.clear()
|
||||
|
||||
this.viewer.interactions.deselectObjects()
|
||||
this.viewer.interactions.hideSectionBox()
|
||||
this.objectIds = []
|
||||
//this.objectIds = []
|
||||
this.views = []
|
||||
|
||||
this._postLoadFunction()
|
||||
}
|
||||
|
||||
_postLoadFunction() {
|
||||
async _postLoadFunction() {
|
||||
if ( this.skipPostLoad ) return
|
||||
this.viewer.interactions.zoomExtents()
|
||||
this.viewer.interactions.hideSectionBox()
|
||||
this.viewer.reflectionsNeedUpdate = true
|
||||
this.viewer.sectionBox.off()
|
||||
await this.sceneObjects.applyFilter()
|
||||
this.viewer.interactions.zoomExtents( undefined, false )
|
||||
this.viewer.reflectionsNeedUpdate = false
|
||||
}
|
||||
|
||||
getSceneBoundingBox() {
|
||||
@@ -279,7 +296,7 @@ export default class SceneObjectManager {
|
||||
}
|
||||
|
||||
_argbToRGB( argb ) {
|
||||
return '#'+ ( '000000' + ( argb & 0xFFFFFF ).toString( 16 ) ).slice( -6 )
|
||||
return '#' + ( '000000' + ( argb & 0xFFFFFF ).toString( 16 ) ).slice( -6 )
|
||||
}
|
||||
|
||||
_normaliseColor( color ) {
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import * as THREE from 'three'
|
||||
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
import FilteringManager from './FilteringManager'
|
||||
|
||||
/**
|
||||
* Container for the scene objects, to allow loading/unloading/filtering/coloring/grouping
|
||||
*/
|
||||
export default class SceneObjects {
|
||||
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
this.scene = viewer.scene
|
||||
|
||||
this.allObjects = new THREE.Group()
|
||||
this.allObjects.name = 'allObjects'
|
||||
|
||||
this.allSolidObjects = new THREE.Group()
|
||||
this.allSolidObjects.name = 'allSolidObjects'
|
||||
this.allSolidObjects.visible = false // these are grouped later, we never want to display them individually
|
||||
this.allObjects.add( this.allSolidObjects )
|
||||
|
||||
this.allTransparentObjects = new THREE.Group()
|
||||
this.allTransparentObjects.name = 'allTransparentObjects'
|
||||
this.allObjects.add( this.allTransparentObjects )
|
||||
|
||||
this.allLineObjects = new THREE.Group()
|
||||
this.allLineObjects.name = 'allLineObjects'
|
||||
this.allObjects.add( this.allLineObjects )
|
||||
|
||||
this.allPointObjects = new THREE.Group()
|
||||
this.allPointObjects.name = 'allPointObjects'
|
||||
this.allObjects.add( this.allPointObjects )
|
||||
|
||||
// Grouped solid objects, generated from `allSolidObjects`
|
||||
this.groupedSolidObjects = new THREE.Group()
|
||||
this.groupedSolidObjects.name = 'groupedSolidObjects'
|
||||
this.allObjects.add( this.groupedSolidObjects )
|
||||
|
||||
this.filteringManager = new FilteringManager( this.viewer )
|
||||
this.filteredObjects = null
|
||||
|
||||
this.appliedFilter = null
|
||||
|
||||
// When the `appliedFilter` is null, scene will contain `allObjects`. Otherwise, `filteredObjects`
|
||||
// This is to optimize the no-filter usecase, so we don't make an unnecessary clone of all the objects
|
||||
this.objectsInScene = this.allObjects
|
||||
this.scene.add( this.allObjects )
|
||||
|
||||
this.isBusy = true
|
||||
this.lastAsyncPause = Date.now()
|
||||
}
|
||||
|
||||
async asyncPause() {
|
||||
// Don't freeze the UI when doing all those traversals
|
||||
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
||||
// if (Date.now() - this.lastAsyncPause > 200 ) console.log("FREEZED for ", Date.now() - this.lastAsyncPause)
|
||||
await new Promise( resolve => setTimeout( resolve, 0 ) )
|
||||
this.lastAsyncPause = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
getObjectsProperties() {
|
||||
let flattenObject = function( obj ) {
|
||||
let flatten = {}
|
||||
for ( let k in obj ) {
|
||||
if ( [ 'id', '__closure', 'bbox', 'totalChildrenCount' ].includes( k ) )
|
||||
continue
|
||||
let v = obj[ k ]
|
||||
if ( Array.isArray( v ) )
|
||||
continue
|
||||
if ( v.constructor === Object ) {
|
||||
let flattenProp = flattenObject( v )
|
||||
for ( let pk in flattenProp ) {
|
||||
flatten[ `${k}.${pk}` ] = flattenProp[ pk ]
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ( [ 'string', 'number', 'boolean' ].includes( typeof v ) )
|
||||
flatten[ k ] = v
|
||||
}
|
||||
return flatten
|
||||
}
|
||||
|
||||
let propValues = {}
|
||||
for ( let objGroup of this.objectsInScene.children ) {
|
||||
for ( let threeObj of objGroup.children ) {
|
||||
let obj = flattenObject( threeObj.userData )
|
||||
for ( let prop of Object.keys( obj ) ) {
|
||||
if ( !( prop in propValues ) ) {
|
||||
propValues[ prop ] = []
|
||||
}
|
||||
propValues[ prop ].push( obj[ prop ] )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let propInfo = {}
|
||||
for ( let prop in propValues ) {
|
||||
let pinfo = {
|
||||
type: typeof propValues[ prop ][ 0 ],
|
||||
objectCount: propValues[ prop ].length,
|
||||
allValues: propValues[ prop ],
|
||||
uniqueValues: {},
|
||||
minValue: propValues[ prop ][ 0 ],
|
||||
maxValue: propValues[ prop ][ 0 ]
|
||||
}
|
||||
for ( let v of propValues[ prop ] ) {
|
||||
if ( v < pinfo.minValue ) pinfo.minValue = v
|
||||
if ( v > pinfo.maxValue ) pinfo.maxValue = v
|
||||
if ( !( v in pinfo.uniqueValues ) ) {
|
||||
pinfo.uniqueValues[ v ] = 0
|
||||
}
|
||||
pinfo.uniqueValues[ v ] += 1
|
||||
}
|
||||
|
||||
propInfo[ prop ] = pinfo
|
||||
}
|
||||
return propInfo
|
||||
}
|
||||
|
||||
async applyFilterToGroup( threejsGroup, filter ) {
|
||||
let ret = new THREE.Group()
|
||||
ret.name = 'filtered_' + threejsGroup.name
|
||||
|
||||
for ( let obj of threejsGroup.children ) {
|
||||
await this.asyncPause()
|
||||
let filteredObj = this.filteringManager.filterAndColorObject( obj, filter )
|
||||
if ( filteredObj )
|
||||
ret.add( filteredObj )
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
disposeAndClearGroup( threejsGroup, disposeGeometry = true ) {
|
||||
let t0 = Date.now()
|
||||
for ( let child of threejsGroup.children ) {
|
||||
if ( child.type === 'Group' ) {
|
||||
this.disposeAndClearGroup( child, disposeGeometry )
|
||||
}
|
||||
if ( child.material )
|
||||
child.material.dispose()
|
||||
if ( disposeGeometry && child.geometry )
|
||||
child.geometry.dispose()
|
||||
}
|
||||
threejsGroup.clear()
|
||||
// console.log( 'Dispose in: ', Date.now() - t0 )
|
||||
}
|
||||
|
||||
async applyFilter( filter ) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if ( filter === undefined ) filter = this.appliedFilter
|
||||
|
||||
if ( filter === null ) {
|
||||
// Remove filters, use allObjects
|
||||
let newGoupedSolidObjects = await this.groupSolidObjects( this.allSolidObjects )
|
||||
if ( this.groupedSolidObjects !== null ) {
|
||||
this.disposeAndClearGroup( this.groupedSolidObjects )
|
||||
this.allObjects.remove( this.groupedSolidObjects )
|
||||
}
|
||||
this.groupedSolidObjects = newGoupedSolidObjects
|
||||
this.allObjects.add( this.groupedSolidObjects )
|
||||
|
||||
if ( this.filteredObjects !== null ) {
|
||||
this.disposeAndClearGroup( this.filteredObjects )
|
||||
this.filteredObjects = null
|
||||
}
|
||||
this.scene.remove( this.objectsInScene )
|
||||
this.scene.add( this.allObjects )
|
||||
this.objectsInScene = this.allObjects
|
||||
} else {
|
||||
// A filter is to be applied
|
||||
this.filteringManager.initFilterOperation()
|
||||
|
||||
let newFilteredObjects = new THREE.Group()
|
||||
newFilteredObjects.name = 'FilteredObjects'
|
||||
|
||||
let filteredSolidObjects = await this.applyFilterToGroup( this.allSolidObjects, filter )
|
||||
filteredSolidObjects.visible = false
|
||||
newFilteredObjects.add( filteredSolidObjects )
|
||||
|
||||
let filteredLineObjects = await this.applyFilterToGroup( this.allLineObjects, filter )
|
||||
newFilteredObjects.add( filteredLineObjects )
|
||||
|
||||
let filteredTransparentObjects = await this.applyFilterToGroup( this.allTransparentObjects, filter )
|
||||
newFilteredObjects.add( filteredTransparentObjects )
|
||||
|
||||
let filteredPointObjects = await this.applyFilterToGroup( this.allPointObjects, filter )
|
||||
newFilteredObjects.add( filteredPointObjects )
|
||||
|
||||
// group solid objects
|
||||
let groupedFilteredSolidObjects = await this.groupSolidObjects( filteredSolidObjects )
|
||||
newFilteredObjects.add( groupedFilteredSolidObjects )
|
||||
|
||||
// Sync update scene
|
||||
if ( this.filteredObjects !== null ) {
|
||||
this.disposeAndClearGroup( this.filteredObjects )
|
||||
}
|
||||
this.filteredObjects = newFilteredObjects
|
||||
|
||||
this.scene.remove( this.objectsInScene )
|
||||
this.scene.add( this.filteredObjects )
|
||||
this.objectsInScene = this.filteredObjects
|
||||
}
|
||||
|
||||
this.appliedFilter = filter
|
||||
this.viewer.needsRender = true
|
||||
|
||||
|
||||
return { colorLegend: this.filteringManager.colorLegend }
|
||||
}
|
||||
|
||||
flattenGroup( group ) {
|
||||
let acc = []
|
||||
for( let child of group.children ) {
|
||||
if( child instanceof THREE.Group ) {
|
||||
acc.push( ...this.flattenGroup( child ) )
|
||||
|
||||
} else {
|
||||
acc.push( child.clone() )
|
||||
}
|
||||
}
|
||||
for( let element of acc ) {
|
||||
element.geometry = element.geometry.clone()
|
||||
element.geometry.applyMatrix4( group.matrix )
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
async groupSolidObjects( threejsGroup ) {
|
||||
let materialIdToBufferGeometry = {}
|
||||
let materialIdToMaterial = {}
|
||||
let materialIdToMeshes = {}
|
||||
|
||||
for ( let obj of threejsGroup.children ) {
|
||||
let meshes = []
|
||||
if( obj instanceof THREE.Group ) {
|
||||
meshes = this.flattenGroup( obj )
|
||||
|
||||
} else {
|
||||
meshes = [ obj ]
|
||||
}
|
||||
|
||||
for( let mesh of meshes ) {
|
||||
let m = mesh.material
|
||||
let materialId = `${m.type}/${m.vertexColors}/${m.color.toJSON()}/${m.side}/${m.transparent}/${m.opactiy}/${m.emissive}/${m.metalness}/${m.roughness}`
|
||||
|
||||
if ( !( materialId in materialIdToBufferGeometry ) ) {
|
||||
materialIdToBufferGeometry[ materialId ] = []
|
||||
materialIdToMaterial[ materialId ] = m
|
||||
materialIdToMeshes[ materialId ] = []
|
||||
}
|
||||
|
||||
materialIdToBufferGeometry[ materialId ].push( mesh.geometry )
|
||||
materialIdToMeshes[ materialId ].push( mesh )
|
||||
|
||||
// Max 1024 objects per group (mergeBufferGeometries is sync and can freeze for large data)
|
||||
if ( materialIdToBufferGeometry[ materialId ].length >= 1024 ) {
|
||||
let archivedMaterialId = `arch//${materialId}//${mesh.id}`
|
||||
materialIdToBufferGeometry[ archivedMaterialId ] = materialIdToBufferGeometry[ materialId ]
|
||||
materialIdToMaterial[ archivedMaterialId ] = materialIdToMaterial[ materialId ]
|
||||
materialIdToMeshes[ archivedMaterialId ] = materialIdToMeshes[ materialId ]
|
||||
delete materialIdToBufferGeometry[ materialId ]
|
||||
delete materialIdToMaterial[ materialId ]
|
||||
delete materialIdToMeshes[ materialId ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let groupedObjects = new THREE.Group()
|
||||
groupedObjects.name = 'GroupedSolidObjects'
|
||||
|
||||
await this.asyncPause()
|
||||
|
||||
for ( let materialId in materialIdToBufferGeometry ) {
|
||||
await this.asyncPause()
|
||||
// TODO: does this handle transforms well ?
|
||||
let groupGeometry = BufferGeometryUtils.mergeBufferGeometries( materialIdToBufferGeometry[ materialId ] )
|
||||
await this.asyncPause()
|
||||
let groupMaterial = materialIdToMaterial[ materialId ]
|
||||
let groupMesh = new THREE.Mesh( groupGeometry, groupMaterial )
|
||||
groupMesh.userData = null
|
||||
groupedObjects.add( groupMesh )
|
||||
}
|
||||
|
||||
return groupedObjects
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,246 +1,382 @@
|
||||
import * as THREE from 'three'
|
||||
import SelectionHelper from './SelectionHelper'
|
||||
import { TransformControls } from './external/TransformControls.js'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
|
||||
import { Box3 } from 'three'
|
||||
|
||||
export default class SectionBox {
|
||||
|
||||
constructor( viewer, bbox ) {
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
|
||||
this.orbiting = false
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
|
||||
this.dragging = false
|
||||
this.display = new THREE.Group()
|
||||
this.viewer.controls.addEventListener( 'wake', () => { this.orbiting = true } )
|
||||
this.viewer.controls.addEventListener( 'controlend', () => { this.orbiting = false } )
|
||||
|
||||
this.box = bbox || this.viewer.sceneManager.getSceneBoundingBox()
|
||||
const dimensions = new THREE.Vector3().subVectors( this.box.max, this.box.min )
|
||||
this.boxGeo = new THREE.BoxGeometry( dimensions.x, dimensions.y, dimensions.z )
|
||||
|
||||
const matrix = new THREE.Matrix4().setPosition( dimensions.addVectors( this.box.min, this.box.max ).multiplyScalar( 0.5 ) )
|
||||
this.boxGeo.applyMatrix4( matrix )
|
||||
this.boxMesh = new THREE.Mesh( this.boxGeo, new THREE.MeshBasicMaterial() )
|
||||
|
||||
this.boxHelper = new THREE.BoxHelper( this.boxMesh, 0x0A66FF )
|
||||
|
||||
const plane = new THREE.PlaneGeometry( 1, 1 )
|
||||
this.hoverPlane = new THREE.Mesh( plane, new THREE.MeshStandardMaterial( {
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
opacity: 0.02,
|
||||
color: 0x0A66FF,
|
||||
metalness: 0.1,
|
||||
roughness: 0.75
|
||||
} ) )
|
||||
|
||||
this.display.add( this.boxHelper )
|
||||
this.display.add( this.hoverPlane )
|
||||
|
||||
this.display.name = 'SectionBox'
|
||||
this.viewer.scene.add( this.display )
|
||||
|
||||
this.boxMesh.userData.planes = []
|
||||
this.boxMesh.userData.indices = []
|
||||
this.planes = []
|
||||
// box
|
||||
this.boxGeometry = this._generateSimpleCube( 5, 5, 5 )
|
||||
this.material = new THREE.MeshStandardMaterial( { color: 0x00ffff, opacity:0, wireframe: false, side: THREE.DoubleSide } )
|
||||
this.cube = new THREE.Mesh( this.boxGeometry, this.material )
|
||||
this.cube.visible = false
|
||||
|
||||
// Gen box and planes
|
||||
this._generatePlanes()
|
||||
this.display.add( this.cube )
|
||||
|
||||
// Box face selection controls
|
||||
this.selectionHelper = new SelectionHelper( this.viewer, { subset: this.boxMesh, hover: true } )
|
||||
let targetFaceIndex = -1
|
||||
this.boxHelper = new THREE.BoxHelper( this.cube, 0x0A66FF )
|
||||
this.boxHelper.material.opacity = 0.4
|
||||
this.display.add( this.boxHelper )
|
||||
|
||||
this.selectionHelper.on( 'hovered', ( obj ) => {
|
||||
if ( obj.length === 0 && !this.dragging ) {
|
||||
this.hoverPlane.visible = false
|
||||
this.controls.visible = true
|
||||
this.planeControls.detach()
|
||||
this.viewer.controls.enabled = true
|
||||
this.viewer.interactions.preventSelection = false
|
||||
this.viewer.needsRender = true
|
||||
targetFaceIndex = -1
|
||||
return
|
||||
}
|
||||
if ( this.orbiting || this.dragging ) return
|
||||
// we're attaching the gizmo mover to this sphere in the box centre
|
||||
let sphere = new THREE.SphereGeometry( 0.01, 10, 10 )
|
||||
this.sphere = new THREE.Mesh( sphere, new THREE.MeshStandardMaterial( { color:0x00ffff } ) )
|
||||
this.sphere.visible = false
|
||||
this.display.add( this.sphere )
|
||||
|
||||
this.controls.visible = false
|
||||
this.hoverPlane.visible = true
|
||||
// plane
|
||||
this.plane = new THREE.PlaneGeometry( 1, 1 )
|
||||
this.hoverPlane = new THREE.Mesh( this.plane, new THREE.MeshStandardMaterial( { transparent: true, side: THREE.DoubleSide, opacity: 0.1, wireframe: false, color: 0x0A66FF, metalness: 0.1, roughness: 0.75 } ) )
|
||||
this.hoverPlane.visible = false
|
||||
this.display.add( this.hoverPlane )
|
||||
|
||||
this.dragging = false
|
||||
this._setupControls()
|
||||
|
||||
this.sidesSimple = {
|
||||
'256': { verts: [ 1, 2, 5, 6 ], axis:'x' },
|
||||
'152': { verts: [ 1, 2, 5, 6 ], axis:'x' },
|
||||
'407': { verts: [ 0, 3, 4, 7 ], axis:'x' },
|
||||
'703': { verts: [ 0, 3, 4, 7 ], axis:'x' },
|
||||
'327': { verts: [ 2, 3, 6, 7 ], axis:'y' },
|
||||
'726': { verts: [ 2, 3, 6, 7 ], axis:'y' },
|
||||
'450': { verts: [ 0, 1, 4, 5 ], axis:'y' },
|
||||
'051': { verts: [ 0, 1, 4, 5 ], axis:'y' },
|
||||
'312': { verts: [ 0, 1, 3, 2 ], axis:'z' },
|
||||
'013': { verts: [ 0, 1, 3, 2 ], axis:'z' },
|
||||
'546': { verts: [ 4, 5, 7, 6 ], axis:'z' },
|
||||
'647': { verts: [ 4, 5, 7, 6 ], axis:'z' }
|
||||
}
|
||||
|
||||
let centre = new THREE.Vector3()
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
centre.add( this.boxGeo.vertices[ obj[0].object.userData.indices[ obj[0].faceIndex ][i] ].clone().applyMatrix4( this.boxMesh.matrixWorld ) )
|
||||
}
|
||||
centre.multiplyScalar( 0.25 )
|
||||
this.hoverPlane.position.copy( centre )
|
||||
this._generateOrUpdatePlanes()
|
||||
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
let vertex = this.boxGeo.vertices[ obj[0].object.userData.indices[ obj[0].faceIndex ][i] ].clone().applyMatrix4( this.boxMesh.matrixWorld )
|
||||
this.hoverPlane.geometry.vertices[i].set( vertex.x - centre.x, vertex.y - centre.y , vertex.z - centre.z )
|
||||
}
|
||||
this.currentRange = null
|
||||
this.prevPosition = null
|
||||
this.attachedToBox = true
|
||||
|
||||
this.hoverPlane.geometry.verticesNeedUpdate = true
|
||||
this.selectionHelper = new SelectionHelper( this.viewer, { subset: [ this.cube ], hover: false, checkForSectionBoxInclusion: false } )
|
||||
this.selectionHelper.on( 'object-clicked', this._clickHandler.bind( this ) )
|
||||
this.selectionHelper.on( 'hovered', ( objs ) =>{
|
||||
// TODO: cannot get this to work reliably
|
||||
// if( !this.attachedToBox ) return
|
||||
// if( objs.length === 0 ) {
|
||||
// this.controls.visible = false
|
||||
// this.viewer.needsRender = true
|
||||
// }
|
||||
// else if( objs.length !== 0 ) {
|
||||
// this.controls.visible = true
|
||||
// this.viewer.needsRender = true
|
||||
// }
|
||||
} )
|
||||
|
||||
let normal = obj[0].face.normal
|
||||
this.planeControls.showX = normal.x !== 0
|
||||
this.planeControls.showY = normal.y !== 0
|
||||
this.planeControls.showZ = normal.z !== 0
|
||||
|
||||
this.planeControls.attach( this.hoverPlane )
|
||||
|
||||
if ( obj[0].faceIndex !== targetFaceIndex ) {
|
||||
this.viewer.needsRender = true
|
||||
targetFaceIndex = obj[0].faceIndex
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if( e.key === 'Escape' && this.viewer.mouseOverRenderer ) {
|
||||
this._attachControlsToBox()
|
||||
}
|
||||
} )
|
||||
|
||||
// Whole box controls
|
||||
this._globalControlsTarget = new THREE.Mesh( new THREE.SphereGeometry( 0.0001 ), new THREE.MeshBasicMaterial( ) )
|
||||
this._globalControlsTarget.position.copy( this.boxGeo.vertices[ 5 ].clone().multiplyScalar( 1.1 ) )
|
||||
this.display.add( this._globalControlsTarget )
|
||||
this._attachControlsToBox()
|
||||
|
||||
this.controls = new TransformControls( this.viewer.camera, this.viewer.renderer.domElement )
|
||||
this.controls.setSize( 0.5 )
|
||||
this.controls.attach( this._globalControlsTarget )
|
||||
this.viewer.on( 'projection-change', function() { this._setupControls(); this._attachControlsToBox() }.bind( this ) )
|
||||
}
|
||||
|
||||
_setupControls() {
|
||||
this.controls?.dispose()
|
||||
this.controls?.detach()
|
||||
this.controls = new TransformControls( this.viewer.cameraHandler.activeCam.camera, this.viewer.renderer.domElement )
|
||||
this.controls.setSize( 0.75 )
|
||||
this.display.add( this.controls )
|
||||
|
||||
// Section plane controls
|
||||
this.planeControls = new TransformControls( this.viewer.camera, this.viewer.renderer.domElement, true )
|
||||
this.display.add( this.planeControls )
|
||||
|
||||
this.prevGizmoPos = this._globalControlsTarget.position.clone()
|
||||
this.controls.addEventListener( 'change', ( ) => {
|
||||
this.prevGizmoPos.sub( this._globalControlsTarget.position )
|
||||
this.boxMesh.translateX( -this.prevGizmoPos.x )
|
||||
this.boxMesh.translateY( -this.prevGizmoPos.y )
|
||||
this.boxMesh.translateZ( -this.prevGizmoPos.z )
|
||||
|
||||
this.prevGizmoPos = this._globalControlsTarget.position.clone()
|
||||
this.setPlanesFromBox( new THREE.Box3().setFromObject( this.boxMesh ) )
|
||||
this.boxHelper.update()
|
||||
this.viewer.needsRender = true
|
||||
} )
|
||||
|
||||
this.controls.addEventListener( 'change', this._draggingChangeHandler.bind( this ) )
|
||||
this.controls.addEventListener( 'dragging-changed', ( event ) => {
|
||||
this.viewer.controls.enabled = !event.value
|
||||
this.viewer.interactions.preventSelection = !event.value
|
||||
if ( !event.value )
|
||||
this.viewer.interactions.zoomToObject( this.boxMesh )
|
||||
} )
|
||||
|
||||
let prevPlaneGizmoPos = null
|
||||
this.planeControls.addEventListener( 'change', ( ) => {
|
||||
if ( !this.dragging ) return
|
||||
if ( targetFaceIndex === -1 ) return
|
||||
if ( prevPlaneGizmoPos === null ) prevPlaneGizmoPos = this.hoverPlane.position.clone()
|
||||
prevPlaneGizmoPos.sub( this.hoverPlane.position )
|
||||
let plane = this.boxMesh.userData.planes[ targetFaceIndex ]
|
||||
|
||||
prevPlaneGizmoPos.negate()
|
||||
plane.translate( prevPlaneGizmoPos )
|
||||
let indices = this.boxMesh.userData.indices[ targetFaceIndex ]
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
let index = indices[i]
|
||||
this.boxGeo.vertices[index].add( prevPlaneGizmoPos )
|
||||
let val = !!event.value
|
||||
if( val ) {
|
||||
this.dragging = val
|
||||
this.viewer.interactions.preventSelection = val
|
||||
this.viewer.cameraHandler.enabled = !val
|
||||
} else {
|
||||
setTimeout( ()=> {
|
||||
this.dragging = val
|
||||
this.viewer.interactions.preventSelection = val
|
||||
this.viewer.cameraHandler.enabled = !val
|
||||
}, 100 )
|
||||
}
|
||||
this.boxGeo.verticesNeedUpdate = true
|
||||
this.boxMesh.geometry.computeBoundingBox()
|
||||
this.boxMesh.geometry.computeBoundingSphere()
|
||||
|
||||
let gizmoPos = this.boxGeo.vertices[ 5 ].clone()
|
||||
gizmoPos.multiplyScalar( 1.1 )
|
||||
gizmoPos.applyMatrix4( this.boxMesh.matrixWorld )
|
||||
this._globalControlsTarget.position.copy( gizmoPos )
|
||||
this.prevGizmoPos = gizmoPos
|
||||
|
||||
prevPlaneGizmoPos = this.hoverPlane.position.clone()
|
||||
this.boxHelper.update()
|
||||
this.viewer.needsRender = true
|
||||
} )
|
||||
|
||||
this.planeControls.addEventListener( 'dragging-changed', ( event ) => {
|
||||
this.viewer.controls.enabled = !event.value
|
||||
this.viewer.interactions.preventSelection = !event.value
|
||||
this.dragging = !!event.value
|
||||
if ( !this.dragging ) {
|
||||
prevPlaneGizmoPos = null
|
||||
this.viewer.interactions.zoomToObject( this.boxMesh )
|
||||
targetFaceIndex = -1
|
||||
|
||||
}
|
||||
this.viewer.needsRender = true
|
||||
} )
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
_generatePlanes() {
|
||||
for ( let i = 0; i < this.boxGeo.faces.length; i += 2 ) {
|
||||
let face = this.boxGeo.faces[i]
|
||||
let pairFace = this.boxGeo.faces[i+1]
|
||||
let plane = new THREE.Plane()
|
||||
// inverting points so plane
|
||||
plane.setFromCoplanarPoints( this.boxGeo.vertices[face.c], this.boxGeo.vertices[face.b], this.boxGeo.vertices[face.a] )
|
||||
// adding it twice for ease of use
|
||||
this.boxMesh.userData.planes.push( plane )
|
||||
this.boxMesh.userData.planes.push( plane )
|
||||
|
||||
this.boxMesh.userData.indices.push( [ face.a, face.b, face.c, pairFace.b ] )
|
||||
this.boxMesh.userData.indices.push( [ face.a, face.b, face.c, pairFace.b ] )
|
||||
|
||||
this.planes.push( plane )
|
||||
}
|
||||
}
|
||||
|
||||
setPlanesFromBox( box ) {
|
||||
const dimensions = new THREE.Vector3().subVectors( box.max, box.min )
|
||||
let boxGeo = new THREE.BoxGeometry( dimensions.x, dimensions.y, dimensions.z )
|
||||
|
||||
const matrix = new THREE.Matrix4().setPosition( dimensions.addVectors( box.min, box.max ).multiplyScalar( 0.5 ) )
|
||||
boxGeo.applyMatrix4( matrix )
|
||||
|
||||
for ( let i = 0; i < this.boxGeo.faces.length; i += 2 ) {
|
||||
let face = boxGeo.faces[i]
|
||||
let plane = this.boxMesh.userData.planes[i]
|
||||
plane.setFromCoplanarPoints( boxGeo.vertices[face.c], boxGeo.vertices[face.b], boxGeo.vertices[face.a] ) // invert pts
|
||||
}
|
||||
}
|
||||
|
||||
setBox( box ) {
|
||||
box = box.clone().expandByScalar( 0.5 )
|
||||
const dimensions = new THREE.Vector3().subVectors( box.max, box.min )
|
||||
let boxGeo = new THREE.BoxGeometry( dimensions.x, dimensions.y, dimensions.z )
|
||||
|
||||
const matrix = new THREE.Matrix4().setPosition( dimensions.addVectors( box.min, box.max ).multiplyScalar( 0.5 ) )
|
||||
boxGeo.applyMatrix4( matrix )
|
||||
|
||||
for ( let i = 0; i < this.boxGeo.vertices.length; i++ ) {
|
||||
this.boxGeo.vertices[i].copy( boxGeo.vertices[i] )
|
||||
}
|
||||
|
||||
this._globalControlsTarget.position.copy( this.boxGeo.vertices[ 5 ].clone().multiplyScalar( 1.1 ) )
|
||||
this.prevGizmoPos = this._globalControlsTarget.position.clone()
|
||||
this.boxMesh.position.copy( new THREE.Vector3() )
|
||||
this.boxMesh.geometry.verticesNeedUpdate = true
|
||||
this.boxMesh.geometry.computeBoundingBox()
|
||||
this.boxMesh.geometry.computeBoundingSphere()
|
||||
_draggingChangeHandler( ) {
|
||||
this.boxHelper.update()
|
||||
this._generateOrUpdatePlanes()
|
||||
|
||||
// Dragging a side / plane
|
||||
if( this.dragging && this.currentRange ) {
|
||||
if( this.prevPosition === null ) this.prevPosition = this.hoverPlane.position.clone()
|
||||
this.prevPosition.sub( this.hoverPlane.position )
|
||||
this.prevPosition.negate()
|
||||
let boxArr = this.boxGeometry.attributes.position.array
|
||||
for( let i = 0; i < this.currentRange.length; i++ ) {
|
||||
let index = this.currentRange[i]
|
||||
boxArr[3 * index] += this.prevPosition.x
|
||||
boxArr[3 * index + 1] += this.prevPosition.y
|
||||
boxArr[3 * index + 2] += this.prevPosition.z
|
||||
}
|
||||
|
||||
this.prevPosition = this.hoverPlane.position.clone()
|
||||
this.boxGeometry.attributes.position.needsUpdate = true
|
||||
this.boxGeometry.computeVertexNormals()
|
||||
this.boxGeometry.computeBoundingBox()
|
||||
this.boxGeometry.computeBoundingSphere()
|
||||
}
|
||||
|
||||
// Dragging the whole section box
|
||||
if( this.dragging && !this.currentRange ) {
|
||||
if( this.prevPosition === null ) this.prevPosition = this.sphere.position.clone()
|
||||
this.prevPosition.sub( this.sphere.position )
|
||||
this.prevPosition.negate()
|
||||
|
||||
for( let i = 0; i < this.boxGeometry.attributes.position.array.length; i += 3 ) {
|
||||
this.boxGeometry.attributes.position.array[i] += this.prevPosition.x
|
||||
this.boxGeometry.attributes.position.array[i + 1] += this.prevPosition.y
|
||||
this.boxGeometry.attributes.position.array[i + 2] += this.prevPosition.z
|
||||
}
|
||||
this.boxGeometry.attributes.position.needsUpdate = true
|
||||
this.boxGeometry.computeVertexNormals()
|
||||
this.boxGeometry.computeBoundingBox()
|
||||
this.boxGeometry.computeBoundingSphere()
|
||||
|
||||
this.prevPosition = this.sphere.position.clone()
|
||||
}
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
_clickHandler( args ) {
|
||||
if( this.viewer.cameraHandler.orbiting || this.dragging ) return
|
||||
if( args.length === 0 && !this.dragging ) {
|
||||
this._attachControlsToBox()
|
||||
this.boxHelper.material.opacity = 0.5
|
||||
this.attachedToBox = true
|
||||
return
|
||||
}
|
||||
this.attachedToBox = false
|
||||
this.boxHelper.material.opacity = 0.3
|
||||
this.hoverPlane.visible = true
|
||||
let side = this.sidesSimple[`${args[0].face.a}${args[0].face.b}${args[0].face.c}`]
|
||||
this.controls.showX = side.axis === 'x'
|
||||
this.controls.showY = side.axis === 'y'
|
||||
this.controls.showZ = side.axis === 'z'
|
||||
|
||||
this.currentRange = side.verts
|
||||
|
||||
let boxArr = this.boxGeometry.attributes.position
|
||||
let index = 0
|
||||
let planeArr = this.plane.attributes.position.array
|
||||
let centre = new THREE.Vector3()
|
||||
|
||||
let tempArr = []
|
||||
for( let i = 0; i < planeArr.length; i++ ) {
|
||||
if( i % 3 === 0 ) {
|
||||
tempArr.push( boxArr.getX( this.currentRange[index] ) )
|
||||
}
|
||||
else if( i % 3 === 1 ) {
|
||||
tempArr.push( boxArr.getY( this.currentRange[index] ) )
|
||||
}
|
||||
else if( i % 3 === 2 ) {
|
||||
tempArr.push( boxArr.getZ( this.currentRange[index] ) )
|
||||
centre.add( new THREE.Vector3( tempArr[i - 2], tempArr[i - 1], tempArr[i] ) )
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
centre.multiplyScalar( 0.25 )
|
||||
this.hoverPlane.position.copy( centre.applyMatrix4( this.cube.matrixWorld ) )
|
||||
this.prevPosition = this.hoverPlane.position.clone()
|
||||
index = 0
|
||||
for( let i = 0; i < planeArr.length; i++ ) {
|
||||
if( i % 3 === 0 ) {
|
||||
planeArr[i] = boxArr.getX( this.currentRange[index] ) - centre.x
|
||||
}
|
||||
else if( i % 3 === 1 ) {
|
||||
planeArr[i] = boxArr.getY( this.currentRange[index] ) - centre.y
|
||||
}
|
||||
else if( i % 3 === 2 ) {
|
||||
planeArr[i] = boxArr.getZ( this.currentRange[index] ) - centre.z
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
this.plane.applyMatrix4( this.cube.matrixWorld )
|
||||
this.plane.attributes.position.needsUpdate = true
|
||||
this.plane.computeBoundingSphere()
|
||||
this.plane.computeBoundingBox()
|
||||
this.controls.detach()
|
||||
this.controls.attach( this.hoverPlane )
|
||||
this.controls.updateMatrixWorld()
|
||||
}
|
||||
|
||||
_generateSimpleCube( width = 0.5, depth = 0.5, height = 0.5 ) {
|
||||
const vertices = [
|
||||
[ -1 * width, -1 * depth, -1 * height ],
|
||||
[ 1 * width, -1 * depth, -1 * height ],
|
||||
[ 1 * width, 1 * depth, -1 * height ],
|
||||
[ -1 * width, 1 * depth, -1 * height ],
|
||||
[ -1 * width, -1 * depth, 1 * height ],
|
||||
[ 1 * width, -1 * depth, 1 * height ],
|
||||
[ 1 * width, 1 * depth, 1 * height ],
|
||||
[ -1 * width, 1 * depth, 1 * height ]
|
||||
]
|
||||
|
||||
const indexes = [
|
||||
0, 1, 3, 3, 1, 2,
|
||||
1, 5, 2, 2, 5, 6,
|
||||
5, 4, 6, 6, 4, 7,
|
||||
4, 0, 7, 7, 0, 3,
|
||||
3, 2, 7, 7, 2, 6,
|
||||
4, 5, 0, 0, 5, 1
|
||||
]
|
||||
|
||||
let positions = []
|
||||
for( let vert of vertices ) {
|
||||
positions.push( ...vert )
|
||||
}
|
||||
|
||||
let g = new THREE.BufferGeometry()
|
||||
g.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array( positions ), 3 ) )
|
||||
g.setIndex( indexes )
|
||||
g.computeVertexNormals()
|
||||
return g
|
||||
}
|
||||
|
||||
_generateOrUpdatePlanes() {
|
||||
this.planes = this.planes || [ new THREE.Plane(), new THREE.Plane(), new THREE.Plane(), new THREE.Plane(), new THREE.Plane(), new THREE.Plane() ]
|
||||
|
||||
let index = 0
|
||||
let boxArr = this.boxGeometry.attributes.position
|
||||
const indexes = [
|
||||
0, 1, 3, 3, 1, 2,
|
||||
1, 5, 2, 2, 5, 6,
|
||||
5, 4, 6, 6, 4, 7,
|
||||
4, 0, 7, 7, 0, 3,
|
||||
3, 2, 7, 7, 2, 6,
|
||||
4, 5, 0, 0, 5, 1
|
||||
]
|
||||
|
||||
for( let i = 0; i < indexes.length; i += 6 ) {
|
||||
let a = new THREE.Vector3( boxArr.getX( indexes[i] ), boxArr.getY( indexes[i] ), boxArr.getZ( indexes[i] ) )
|
||||
let b = new THREE.Vector3( boxArr.getX( indexes[i + 1] ), boxArr.getY( indexes[i + 1] ), boxArr.getZ( indexes[i + 1] ) )
|
||||
let c = new THREE.Vector3( boxArr.getX( indexes[i + 2] ), boxArr.getY( indexes[i + 2] ), boxArr.getZ( indexes[i + 2] ) )
|
||||
let plane = this.planes[index]
|
||||
plane.setFromCoplanarPoints( a, b, c )
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
_attachControlsToBox() {
|
||||
this.controls.detach()
|
||||
|
||||
let centre = new THREE.Vector3()
|
||||
let boxArr = this.boxGeometry.attributes.position.array
|
||||
for( let i = 0; i < boxArr.length; i += 3 ) {
|
||||
centre.add( new THREE.Vector3( boxArr[i], boxArr[i + 1], boxArr[i + 2] ) )
|
||||
}
|
||||
centre.multiplyScalar( 1 / 8 )
|
||||
this.sphere.position.copy( centre )
|
||||
|
||||
this.cube.geometry.computeBoundingSphere()
|
||||
this.cube.geometry.computeBoundingBox()
|
||||
this.controls.attach( this.sphere )
|
||||
this.currentRange = null
|
||||
this.prevPosition = null
|
||||
this.hoverPlane.visible = false
|
||||
this.controls.showX = true
|
||||
this.controls.showY = true
|
||||
this.controls.showZ = true
|
||||
}
|
||||
|
||||
setBox( targetBox, offset = 0.05 ) {
|
||||
let box
|
||||
|
||||
if( targetBox ) box = targetBox
|
||||
else {
|
||||
if( this.viewer.interactions.selectedObjects.children.length !== 0 ) {
|
||||
box = new THREE.Box3( ).setFromObject( this.viewer.interactions.selectedObjects )
|
||||
} else if( this.viewer.sceneManager.sceneObjects.allObjects.children.length !== 0 ) {
|
||||
box = new THREE.Box3( ).setFromObject( this.viewer.sceneManager.sceneObjects.allObjects )
|
||||
} else {
|
||||
box = new Box3( new THREE.Vector3( -1, -1, -1 ), new THREE.Vector3( 1, 1, 1 ) )
|
||||
}
|
||||
}
|
||||
|
||||
if( box.min.x === Infinity ) {
|
||||
box = new Box3( new THREE.Vector3( -1, -1, -1 ), new THREE.Vector3( 1, 1, 1 ) )
|
||||
}
|
||||
|
||||
const x1 = box.min.x - ( box.max.x - box.min.x ) * offset
|
||||
const y1 = box.min.y - ( box.max.y - box.min.y ) * offset
|
||||
const z1 = box.min.z - ( box.max.z - box.min.z ) * offset
|
||||
const x2 = box.max.x + ( box.max.x - box.min.x ) * offset
|
||||
const y2 = box.max.y + ( box.max.y - box.min.y ) * offset
|
||||
const z2 = box.max.z + ( box.max.z - box.min.z ) * offset
|
||||
|
||||
const newVertices = [
|
||||
x1, y1, z1,
|
||||
x2, y1, z1,
|
||||
x2, y2, z1,
|
||||
x1, y2, z1,
|
||||
x1, y1, z2,
|
||||
x2, y1, z2,
|
||||
x2, y2, z2,
|
||||
x1, y2, z2
|
||||
]
|
||||
|
||||
let boxVerts = this.boxGeometry.attributes.position.array
|
||||
for( let i = 0; i < newVertices.length; i++ ) {
|
||||
boxVerts[i] = newVertices[i]
|
||||
}
|
||||
|
||||
this.boxGeometry.attributes.position.needsUpdate = true
|
||||
this.boxGeometry.computeVertexNormals()
|
||||
this.boxGeometry.computeBoundingBox()
|
||||
this.boxGeometry.computeBoundingSphere()
|
||||
this._generateOrUpdatePlanes()
|
||||
this._attachControlsToBox()
|
||||
this.boxHelper.update()
|
||||
this.setPlanesFromBox( box )
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if ( this.display.visible ) {
|
||||
this.viewer.renderer.localClippingEnabled = false
|
||||
this.display.visible = false
|
||||
this.viewer.emit( 'section-box', false )
|
||||
} else {
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
this.display.visible = true
|
||||
this.viewer.emit( 'section-box', true )
|
||||
}
|
||||
this.setBox()
|
||||
this.display.visible = !this.display.visible
|
||||
this.viewer.renderer.localClippingEnabled = this.display.visible
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.selectionHelper.dispose()
|
||||
this.controls.dispose()
|
||||
this.planeControls.dispose()
|
||||
this.display.clear()
|
||||
off() {
|
||||
this.display.visible = false
|
||||
this.viewer.renderer.localClippingEnabled = false
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
on() {
|
||||
this.display.visible = true
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
displayOff() {
|
||||
this.display.visible = false
|
||||
}
|
||||
|
||||
displayOn() {
|
||||
this.display.visible = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as THREE from 'three'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
|
||||
|
||||
/**
|
||||
* WIP: A utility class for adding section planes to the scene.
|
||||
* - 'S' shows/hides section planes
|
||||
* - 's' toggles controls from translate to rotate
|
||||
*/
|
||||
export default class SectionPlaneHelper {
|
||||
|
||||
constructor( parent ) {
|
||||
this.viewer = parent
|
||||
this.cutters = []
|
||||
this.visible = false
|
||||
|
||||
window.addEventListener( 'keydown', ( event ) => {
|
||||
if ( event.key === 's' ) {
|
||||
this.toggleTransformControls()
|
||||
}
|
||||
if ( event.key === 'S' ) {
|
||||
this.toggleSectionPlanes()
|
||||
}
|
||||
}, false )
|
||||
}
|
||||
|
||||
get planes() {
|
||||
return this.cutters.map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
get activePlanes() {
|
||||
return this.cutters.filter( cutter => cutter.visible ).map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
toggleTransformControls() {
|
||||
this.cutters.forEach( cutter => {
|
||||
if ( cutter.control.mode === 'rotate' ) {
|
||||
cutter.control.setMode( 'translate' )
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.showZ = true
|
||||
return
|
||||
}
|
||||
cutter.control.setMode( 'rotate' )
|
||||
cutter.control.showX = true
|
||||
cutter.control.showY = true
|
||||
cutter.control.showZ = false
|
||||
} )
|
||||
}
|
||||
|
||||
createSectionPlane() {
|
||||
let cutter = { }
|
||||
|
||||
cutter.id = this.cutters.length
|
||||
cutter.visible = false
|
||||
cutter.plane = new THREE.Plane( new THREE.Vector3( 0, 0, -1 ), 1 )
|
||||
|
||||
cutter.helper = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1, 1 ), new THREE.MeshBasicMaterial( { color: 0xAFAFAF, transparent: true, opacity: 0.1, side: THREE.DoubleSide } ) )
|
||||
cutter.helper.visible = false
|
||||
this.viewer.scene.add( cutter.helper )
|
||||
|
||||
cutter.control = new TransformControls( this.viewer.camera, this.viewer.renderer.domElement )
|
||||
cutter.control.setSize( 0.5 )
|
||||
cutter.control.space = 'local'
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.setRotationSnap( THREE.MathUtils.degToRad( 15 ) )
|
||||
|
||||
cutter.control.addEventListener( 'change', () => this.viewer.render )
|
||||
cutter.control.addEventListener( 'dragging-changed', ( event ) => {
|
||||
if ( !cutter.visible ) return
|
||||
this.viewer.controls.enabled = !event.value
|
||||
|
||||
// Reference: https://stackoverflow.com/a/52124409
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
cutter.control.attach( cutter.helper )
|
||||
cutter.control.visible = false
|
||||
this.viewer.scene.add( cutter.control )
|
||||
|
||||
this.cutters.push( cutter )
|
||||
|
||||
// adds local clipping planes to all materials
|
||||
let objs = this.viewer.sceneManager.objects
|
||||
objs.forEach( obj => {
|
||||
obj.material.clippingPlanes = this.cutters.map( c => c.plane )
|
||||
} )
|
||||
}
|
||||
|
||||
toggleSectionPlanes() {
|
||||
if ( this.visible ) this.hideSectionPlanes()
|
||||
else this.showSectionPlanes()
|
||||
|
||||
this.visible = !this.visible
|
||||
}
|
||||
|
||||
showSectionPlanes() {
|
||||
this._matchSceneSize()
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = true
|
||||
cutter.helper.visible = true
|
||||
cutter.control.visible = true
|
||||
} )
|
||||
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
}
|
||||
|
||||
hideSectionPlanes() {
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = false
|
||||
cutter.helper.visible = false
|
||||
cutter.control.visible = false
|
||||
} )
|
||||
this.viewer.renderer.localClippingEnabled = false
|
||||
}
|
||||
|
||||
_matchSceneSize() {
|
||||
// Scales and translate helper to scene bbox center and origin
|
||||
const sceneBox = new THREE.Box3().setFromObject( this.viewer.sceneManager.userObjects )
|
||||
const sceneSize = new THREE.Vector3()
|
||||
sceneBox.getSize( sceneSize )
|
||||
const sceneCenter = new THREE.Vector3()
|
||||
sceneBox.getCenter( sceneCenter )
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.helper.scale.set( sceneSize.x > 0 ? sceneSize.x : 1, sceneSize.y > 0 ? sceneSize.y : 1, sceneSize.z >0 ? sceneSize.z : 1 )
|
||||
cutter.helper.position.set( sceneCenter.x, sceneCenter.y, sceneCenter.z )
|
||||
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,12 +19,6 @@ export default class SelectionHelper extends EventEmitter {
|
||||
this.viewer = parent
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
|
||||
// Handle clicks during camera moves
|
||||
this.orbiting = false
|
||||
|
||||
this.viewer.controls.addEventListener( 'wake', () => { this.orbiting = true } )
|
||||
this.viewer.controls.addEventListener( 'sleep', () => { this.orbiting = false } )
|
||||
|
||||
// optional param allows for raycasting against a subset of objects
|
||||
// this.subset = typeof _options !== 'undefined' && typeof _options.subset !== 'undefined' ? _options.subset : null;
|
||||
this.subset = typeof _options !== 'undefined' && typeof _options.subset !== 'undefined' ? _options.subset : null
|
||||
@@ -37,7 +31,6 @@ export default class SelectionHelper extends EventEmitter {
|
||||
// doesn't feel good when debounced, might be necessary tho
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointermove', debounce( ( e ) => {
|
||||
let hovered = this.getClickedObjects( e )
|
||||
|
||||
// dragging event, this shouldn't be under the "hover option"
|
||||
if ( this.pointerDown ) {
|
||||
this.emit( 'object-drag', hovered, this._getNormalisedClickPosition( e ) )
|
||||
@@ -53,28 +46,30 @@ export default class SelectionHelper extends EventEmitter {
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointerdown', debounce( ( e ) => {
|
||||
this.pointerDown = true
|
||||
|
||||
if ( this.orbiting ) return
|
||||
if ( this.viewer.cameraHandler.orbiting ) return
|
||||
|
||||
this.emit( 'mouse-down', this.getClickedObjects( e ) )
|
||||
}, 100 ) )
|
||||
}
|
||||
|
||||
this.sectionBox = null
|
||||
if ( typeof _options !== 'undefined' && _options.sectionBox ) {
|
||||
this.sectionBox = _options.sectionBox
|
||||
this.checkForSectionBoxInclusion = true
|
||||
if ( typeof _options !== 'undefined' && _options.checkForSectionBoxInclusion ) {
|
||||
this.sectionBox = _options.checkForSectionBoxInclusion
|
||||
}
|
||||
|
||||
// Handle mouseclicks
|
||||
|
||||
let mdTime
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointerdown', ( ) => {
|
||||
mdTime = new Date().getTime()
|
||||
} )
|
||||
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointerup', ( e ) => {
|
||||
if( this.viewer.cameraHandler.orbiting ) return
|
||||
|
||||
let delta = new Date().getTime() - mdTime
|
||||
this.pointerDown = false
|
||||
if ( this.orbiting && delta > 250 ) return
|
||||
|
||||
if ( delta > 250 ) return
|
||||
|
||||
let selectionObjects = this.getClickedObjects( e )
|
||||
|
||||
@@ -132,18 +127,18 @@ export default class SelectionHelper extends EventEmitter {
|
||||
|
||||
getClickedObjects( e ) {
|
||||
const normalizedPosition = this._getNormalisedClickPosition( e )
|
||||
this.raycaster.setFromCamera( normalizedPosition, this.viewer.camera )
|
||||
|
||||
let intersectedObjects = this.raycaster.intersectObjects( this.subset ? this._getGroupChildren( this.subset ) : this.viewer.sceneManager.objects )
|
||||
|
||||
|
||||
if ( this.sectionBox && this.sectionBox.display.visible ) {
|
||||
let box = new THREE.Box3().setFromObject( this.sectionBox.boxMesh )
|
||||
this.raycaster.setFromCamera( normalizedPosition, this.viewer.cameraHandler.activeCam.camera )
|
||||
let targetObjects = this.subset ? this.subset : this.viewer.sceneManager.filteredObjects
|
||||
|
||||
let intersectedObjects = this.raycaster.intersectObjects( targetObjects )
|
||||
|
||||
// filters objects in section box mode
|
||||
if ( this.viewer.sectionBox.display.visible && this.checkForSectionBoxInclusion ) {
|
||||
let box = new THREE.Box3().setFromObject( this.viewer.sectionBox.cube )
|
||||
intersectedObjects = intersectedObjects.filter( obj => {
|
||||
return box.containsPoint( obj.point )
|
||||
} )
|
||||
}
|
||||
|
||||
return intersectedObjects
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import CameraControls from 'camera-controls'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js'
|
||||
import Stats from 'three/examples/jsm/libs/stats.module.js'
|
||||
|
||||
import ObjectManager from './SceneObjectManager'
|
||||
import ViewerObjectLoader from './ViewerObjectLoader'
|
||||
import EventEmitter from './EventEmitter'
|
||||
import InteractionHandler from './InteractionHandler'
|
||||
import CameraHandler from './context/CameraHanlder'
|
||||
|
||||
import SectionBox from './SectionBox'
|
||||
|
||||
export default class Viewer extends EventEmitter {
|
||||
|
||||
constructor( { container, postprocessing = true, reflections = true, showStats = false } ) {
|
||||
constructor( { container, postprocessing = false, reflections = true, showStats = false } ) {
|
||||
super()
|
||||
|
||||
this.clock = new THREE.Clock()
|
||||
window.THREE = THREE
|
||||
|
||||
this.clock = new THREE.Clock()
|
||||
|
||||
this.container = container || document.getElementById( 'renderer' )
|
||||
this.postprocessing = postprocessing
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight )
|
||||
this.camera.up.set( 0, 0, 1 )
|
||||
this.camera.position.set( 1, 1, 1 )
|
||||
this.camera.updateProjectionMatrix()
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true, preserveDrawingBuffer: true } )
|
||||
this.renderer.setClearColor( 0xcccccc, 0 )
|
||||
this.renderer.setPixelRatio( window.devicePixelRatio )
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.container.appendChild( this.renderer.domElement )
|
||||
|
||||
// commented out because the ssao flash is annoying
|
||||
// this.renderer.gammaFactor = 2.2
|
||||
// this.renderer.outputEncoding = THREE.sRGBEncoding
|
||||
|
||||
this.cameraHandler = new CameraHandler( this )
|
||||
|
||||
this.reflections = reflections
|
||||
this.reflectionsNeedUpdate = true
|
||||
@@ -42,27 +38,6 @@ export default class Viewer extends EventEmitter {
|
||||
this.cubeCamera = new THREE.CubeCamera( 0.1, 10_000, cubeRenderTarget )
|
||||
this.scene.add( this.cubeCamera )
|
||||
|
||||
CameraControls.install( { THREE: THREE } )
|
||||
this.controls = new CameraControls( this.camera, this.renderer.domElement )
|
||||
this.controls.maxPolarAngle = Math.PI / 2
|
||||
|
||||
this.composer = new EffectComposer( this.renderer )
|
||||
|
||||
this.ssaoPass = new SSAOPass( this.scene, this.camera, this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.ssaoPass.kernelRadius = 0.03
|
||||
this.ssaoPass.kernelSize = 16
|
||||
this.ssaoPass.minDistance = 0.0002
|
||||
this.ssaoPass.maxDistance = 10
|
||||
this.ssaoPass.output = SSAOPass.OUTPUT.Default
|
||||
this.composer.addPass( this.ssaoPass )
|
||||
|
||||
this.pauseSSAO = false
|
||||
this.controls.addEventListener( 'wake', () => { this.pauseSSAO = true } )
|
||||
this.controls.addEventListener( 'sleep', () => { this.pauseSSAO = false; this.needsRender = true } )
|
||||
|
||||
// Keeps track of loaded objects
|
||||
this.sceneManager = new ObjectManager( this )
|
||||
|
||||
if ( showStats ) {
|
||||
this.stats = new Stats()
|
||||
this.container.appendChild( this.stats.dom )
|
||||
@@ -70,16 +45,51 @@ export default class Viewer extends EventEmitter {
|
||||
|
||||
window.addEventListener( 'resize', this.onWindowResize.bind( this ), false )
|
||||
|
||||
this.mouseOverRenderer = false
|
||||
this.renderer.domElement.addEventListener( 'mouseover', () => { this.mouseOverRenderer = true } )
|
||||
this.renderer.domElement.addEventListener( 'mouseout', () => { this.mouseOverRenderer = false } )
|
||||
|
||||
this.loaders = {}
|
||||
|
||||
this.sectionBox = new SectionBox( this )
|
||||
this.sectionBox.off()
|
||||
|
||||
this.sceneManager = new ObjectManager( this )
|
||||
this.interactions = new InteractionHandler( this )
|
||||
|
||||
this.needsRender = true
|
||||
this.sceneLights()
|
||||
this.animate()
|
||||
|
||||
this.loaders = []
|
||||
this.onWindowResize()
|
||||
this.interactions.zoomExtents()
|
||||
this.needsRender = true
|
||||
}
|
||||
|
||||
sceneLights() {
|
||||
|
||||
// const dirLight = new THREE.DirectionalLight( 0xffffff, 0.1 )
|
||||
// dirLight.color.setHSL( 0.1, 1, 0.95 )
|
||||
// dirLight.position.set( -1, 1.75, 1 )
|
||||
// dirLight.position.multiplyScalar( 1000 )
|
||||
// this.scene.add( dirLight )
|
||||
|
||||
// const dirLight2 = new THREE.DirectionalLight( 0xffffff, 0.9 )
|
||||
// dirLight2.color.setHSL( 0.1, 1, 0.95 )
|
||||
// dirLight2.position.set( 0, -1.75, 1 )
|
||||
// dirLight2.position.multiplyScalar( 1000 )
|
||||
// this.scene.add( dirLight2 )
|
||||
|
||||
// const hemiLight2 = new THREE.HemisphereLight( 0xffffff, new THREE.Color( '#232323' ), 1.9 )
|
||||
// hemiLight2.color.setHSL( 1, 1, 1 )
|
||||
// // hemiLight2.groundColor = new THREE.Color( '#232323' )
|
||||
// hemiLight2.up.set( 0, 0, 1 )
|
||||
// this.scene.add( hemiLight2 )
|
||||
|
||||
// let axesHelper = new THREE.AxesHelper( 1 )
|
||||
// this.scene.add( axesHelper )
|
||||
|
||||
// return
|
||||
|
||||
|
||||
let ambientLight = new THREE.AmbientLight( 0xffffff )
|
||||
this.scene.add( ambientLight )
|
||||
|
||||
@@ -106,33 +116,28 @@ export default class Viewer extends EventEmitter {
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 2 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 3 ], sphereSize ) )
|
||||
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x0, 0.2 )
|
||||
hemiLight.color.setHSL( 1, 1, 1 )
|
||||
hemiLight.groundColor.setHSL( 0.095, 1, 0.75 )
|
||||
hemiLight.up.set( 0, 0, 1 )
|
||||
this.scene.add( hemiLight )
|
||||
|
||||
let axesHelper = new THREE.AxesHelper( 1 )
|
||||
this.scene.add( axesHelper )
|
||||
|
||||
|
||||
let group = new THREE.Group()
|
||||
this.scene.add( group )
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.composer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
// this.composer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.needsRender = true
|
||||
}
|
||||
|
||||
animate() {
|
||||
// requestAnimationFrame( this.animate.bind( this ) )
|
||||
// this.controls.update()
|
||||
//
|
||||
const delta = this.clock.getDelta()
|
||||
const hasControlsUpdated = this.controls.update( delta )
|
||||
|
||||
const hasControlsUpdated = this.cameraHandler.controls.update( delta )
|
||||
// const hasOrthoControlsUpdated = this.cameraHandler.cameras[1].controls.update( delta )
|
||||
|
||||
requestAnimationFrame( this.animate.bind( this ) )
|
||||
|
||||
@@ -141,6 +146,8 @@ export default class Viewer extends EventEmitter {
|
||||
this.needsRender = false
|
||||
if ( this.stats ) this.stats.begin()
|
||||
this.render()
|
||||
if( this.stats && document.getElementById( 'info-draws' ) )
|
||||
document.getElementById( 'info-draws' ).textContent = '' + this.renderer.info.render.calls
|
||||
if ( this.stats ) this.stats.end()
|
||||
}
|
||||
|
||||
@@ -151,7 +158,7 @@ export default class Viewer extends EventEmitter {
|
||||
// Note: scene based "dynamic" reflections need to be handled a bit more carefully, or else:
|
||||
// GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.
|
||||
// First remove the env map from all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
for ( let obj of this.sceneManager.filteredObjects ) {
|
||||
obj.material.envMap = null
|
||||
}
|
||||
|
||||
@@ -162,32 +169,79 @@ export default class Viewer extends EventEmitter {
|
||||
this.scene.background = null
|
||||
|
||||
// Finally, re-set the env maps of all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
for ( let obj of this.sceneManager.filteredObjects ) {
|
||||
obj.material.envMap = this.cubeCamera.renderTarget.texture
|
||||
}
|
||||
this.reflectionsNeedUpdate = false
|
||||
}
|
||||
|
||||
// Render as usual
|
||||
// TODO: post processing SSAO sucks so much currently it's off by default
|
||||
// if ( this.postprocessing && !this.pauseSSAO && !this.renderer.localClippingEnabled ){
|
||||
if ( this.postprocessing && !this.pauseSSAO ) {
|
||||
// console.log('composer')
|
||||
this.composer.render( this.scene, this.camera )
|
||||
}
|
||||
else {
|
||||
// console.log('renderer')
|
||||
this.renderer.render( this.scene, this.camera )
|
||||
}
|
||||
this.renderer.render( this.scene, this.cameraHandler.activeCam.camera )
|
||||
|
||||
}
|
||||
|
||||
async loadObject( url, token ) {
|
||||
let loader = new ViewerObjectLoader( this, url, token )
|
||||
this.loaders.push( loader )
|
||||
toggleSectionBox() {
|
||||
this.sectionBox.toggle()
|
||||
}
|
||||
|
||||
sectionBoxOff() {
|
||||
this.sectionBox.off()
|
||||
}
|
||||
|
||||
sectionBoxOn() {
|
||||
this.sectionBox.on()
|
||||
}
|
||||
|
||||
zoomExtents( fit, transition ) {
|
||||
this.interactions.zoomExtents( fit, transition )
|
||||
}
|
||||
|
||||
setProjectionMode( mode ) {
|
||||
this.cameraHandler.activeCam = mode
|
||||
}
|
||||
|
||||
toggleCameraProjection() {
|
||||
this.cameraHandler.toggleCameras()
|
||||
}
|
||||
|
||||
async loadObject( url, token, enableCaching = true ) {
|
||||
let loader = new ViewerObjectLoader( this, url, token, enableCaching )
|
||||
this.loaders[ url ] = loader
|
||||
await loader.load()
|
||||
return
|
||||
}
|
||||
|
||||
async cancelLoad( url, unload = false ) {
|
||||
this.loaders[url].cancelLoad()
|
||||
if( unload ) {
|
||||
await this.unloadObject( url )
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
async unloadObject( url ) {
|
||||
await this.loaders[ url ].unload()
|
||||
delete this.loaders[ url ]
|
||||
return
|
||||
}
|
||||
|
||||
async unloadAll() {
|
||||
for( let key of Object.keys( this.loaders ) ) {
|
||||
await this.loaders[key].unload()
|
||||
delete this.loaders[key]
|
||||
}
|
||||
await this.applyFilter( null )
|
||||
return
|
||||
}
|
||||
|
||||
async applyFilter( filter ) {
|
||||
return await this.sceneManager.sceneObjects.applyFilter( filter )
|
||||
}
|
||||
|
||||
getObjectsProperties() {
|
||||
return this.sceneManager.sceneObjects.getObjectsProperties()
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// TODO
|
||||
// TODO: currently it's easier to simply refresh the page :)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ObjectLoader from '@speckle/objectloader'
|
||||
import Converter from './Converter'
|
||||
import Converter from './converter/Converter'
|
||||
|
||||
/**
|
||||
* Helper wrapper around the ObjectLoader class, with some built in assumptions.
|
||||
@@ -8,7 +8,8 @@ import Converter from './Converter'
|
||||
export default class ViewerObjectLoader {
|
||||
|
||||
|
||||
constructor( parent, objectUrl, authToken ) {
|
||||
constructor( parent, objectUrl, authToken, enableCaching ) {
|
||||
this.objectUrl = objectUrl
|
||||
this.viewer = parent
|
||||
this.token = null
|
||||
try {
|
||||
@@ -37,10 +38,29 @@ export default class ViewerObjectLoader {
|
||||
serverUrl: this.serverUrl,
|
||||
token: this.token,
|
||||
streamId: this.streamId,
|
||||
objectId: this.objectId
|
||||
objectId: this.objectId,
|
||||
options: { enableCaching: enableCaching }
|
||||
} )
|
||||
|
||||
this.converter = new Converter( this.loader )
|
||||
|
||||
this.lastAsyncPause = Date.now()
|
||||
this.existingAsyncPause = null
|
||||
this.cancel = false
|
||||
}
|
||||
|
||||
async asyncPause() {
|
||||
// while ( this.existingAsyncPause ) {
|
||||
// await this.existingAsyncPause
|
||||
// }
|
||||
// Don't freeze the UI
|
||||
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
||||
this.lastAsyncPause = Date.now()
|
||||
this.existingAsyncPause = new Promise( resolve => setTimeout( resolve, 0 ) )
|
||||
await this.existingAsyncPause
|
||||
this.existingAsyncPause = null
|
||||
if ( Date.now() - this.lastAsyncPause > 500 ) console.log( 'VObjLoader Event loop lag: ', Date.now() - this.lastAsyncPause )
|
||||
}
|
||||
}
|
||||
|
||||
async load( ) {
|
||||
@@ -50,9 +70,17 @@ export default class ViewerObjectLoader {
|
||||
let viewerLoads = 0
|
||||
let firstObjectPromise = null
|
||||
for await ( let obj of this.loader.getObjectIterator() ) {
|
||||
if( this.cancel ) {
|
||||
this.viewer.emit( 'load-progress', { progress: 1, id: this.objectId, url: this.objectUrl } ) // to hide progress bar, easier on the frontend
|
||||
this.viewer.emit( 'load-cancelled', { id: this.objectId, url: this.objectUrl } )
|
||||
return
|
||||
}
|
||||
await this.converter.asyncPause()
|
||||
if ( first ) {
|
||||
firstObjectPromise = this.converter.traverseAndConvert( obj, ( o ) => {
|
||||
this.viewer.sceneManager.addObject( o )
|
||||
firstObjectPromise = this.converter.traverseAndConvert( obj, async ( objectWrapper ) => {
|
||||
await this.converter.asyncPause()
|
||||
objectWrapper.meta.__importedUrl = this.objectUrl
|
||||
this.viewer.sceneManager.addObject( objectWrapper )
|
||||
viewerLoads++
|
||||
} )
|
||||
first = false
|
||||
@@ -66,9 +94,20 @@ export default class ViewerObjectLoader {
|
||||
await firstObjectPromise
|
||||
}
|
||||
|
||||
await this.viewer.sceneManager.postLoad()
|
||||
|
||||
if ( viewerLoads === 0 ) {
|
||||
console.warn( `Viewer: no 3d objects found in object ${this.objectId}` )
|
||||
this.viewer.emit( 'load-warning', { message: `No displayable objects found in object ${this.objectId}.` } )
|
||||
}
|
||||
}
|
||||
|
||||
async unload( ) {
|
||||
this.cancel = true
|
||||
await this.viewer.sceneManager.removeImportedObject( this.objectUrl )
|
||||
}
|
||||
|
||||
cancelLoad() {
|
||||
this.cancel = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as THREE from 'three'
|
||||
import CameraControls from 'camera-controls'
|
||||
import { KeyboardKeyHold } from 'hold-event'
|
||||
|
||||
export default class CameraHandler {
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight )
|
||||
this.camera.up.set( 0, 0, 1 )
|
||||
this.camera.position.set( 1, 1, 1 )
|
||||
this.camera.updateProjectionMatrix()
|
||||
|
||||
let aspect = this.viewer.container.offsetWidth / this.viewer.container.offsetHeight
|
||||
let fustrumSize = 50
|
||||
this.orthoCamera = new THREE.OrthographicCamera( ( -fustrumSize * aspect ) / 2, ( fustrumSize * aspect ) / 2, fustrumSize / 2, -fustrumSize / 2, 0.001, 10000 )
|
||||
this.orthoCamera.up.set( 0, 0, 1 )
|
||||
this.orthoCamera.position.set( 100, 100, 100 )
|
||||
this.orthoCamera.updateProjectionMatrix()
|
||||
|
||||
CameraControls.install( { THREE: THREE } )
|
||||
this.controls = new CameraControls( this.camera, this.viewer.renderer.domElement )
|
||||
this.controls.maxPolarAngle = Math.PI / 1.5
|
||||
this.setupWASDControls()
|
||||
|
||||
this.cameras = [
|
||||
{
|
||||
camera: this.camera,
|
||||
controls: this.controls,
|
||||
name: 'perspective',
|
||||
active: true
|
||||
},
|
||||
{
|
||||
camera: this.orthoCamera,
|
||||
controls: this.controls,
|
||||
name: 'ortho',
|
||||
active: false
|
||||
}
|
||||
]
|
||||
|
||||
this.orbiting = false
|
||||
this.controls.addEventListener( 'wake', () => { this.orbiting = true } )
|
||||
// note: moved to new controls event called "rest"
|
||||
this.controls.addEventListener( 'controlend', () => { } )
|
||||
this.controls.addEventListener( 'rest', () => { setTimeout( () => { this.orbiting = false }, 400 ) } )
|
||||
|
||||
window.addEventListener( 'resize', this.onWindowResize.bind( this ), false )
|
||||
this.onWindowResize()
|
||||
}
|
||||
|
||||
get activeCam() {
|
||||
return this.cameras[0].active ? this.cameras[0] : this.cameras[1]
|
||||
}
|
||||
|
||||
set activeCam( val ) {
|
||||
if( val === 'perspective' )
|
||||
this.setPerspectiveCameraOn()
|
||||
else if( val === 'ortho' )
|
||||
this.setOrthoCameraOn()
|
||||
else throw new Error( `'${val}' projection mode is invalid. Try with 'perspective' or 'ortho'.` )
|
||||
}
|
||||
|
||||
set enabled( val ) {
|
||||
this.controls.enabled = val
|
||||
}
|
||||
|
||||
setPerspectiveCameraOn() {
|
||||
if( this.cameras[0].active ) return
|
||||
this.cameras[0].active = true
|
||||
this.cameras[1].active = false
|
||||
|
||||
this.setupPerspectiveCamera()
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
setOrthoCameraOn() {
|
||||
if( this.cameras[1].active ) return
|
||||
this.cameras[0].active = false
|
||||
this.cameras[1].active = true
|
||||
|
||||
this.setupOrthoCamera()
|
||||
this.viewer.needsRender = true
|
||||
}
|
||||
|
||||
toggleCameras() {
|
||||
if( this.cameras[0].active ) this.setOrthoCameraOn()
|
||||
else this.setPerspectiveCameraOn()
|
||||
}
|
||||
|
||||
setupOrthoCamera() {
|
||||
this.previousDistance = this.controls.distance
|
||||
this.controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM
|
||||
|
||||
const lineOfSight = new THREE.Vector3()
|
||||
this.camera.getWorldDirection( lineOfSight )
|
||||
const target = new THREE.Vector3()
|
||||
this.controls.getTarget( target )
|
||||
const distance = target.clone().sub( this.camera.position )
|
||||
const depth = distance.dot( lineOfSight )
|
||||
const dims = { x: this.viewer.container.offsetWidth, y: this.viewer.container.offsetHeight }
|
||||
const aspect = dims.x / dims.y
|
||||
const fov = this.camera.fov
|
||||
const height = depth * 2 * Math.atan( ( fov * ( Math.PI / 180 ) ) / 2 )
|
||||
const width = height * aspect
|
||||
|
||||
this.orthoCamera.zoom = 1
|
||||
this.orthoCamera.left = width / -2
|
||||
this.orthoCamera.right = width / 2
|
||||
this.orthoCamera.top = height / 2
|
||||
this.orthoCamera.bottom = height / -2
|
||||
this.orthoCamera.far = this.camera.far
|
||||
this.orthoCamera.near = 0.0001
|
||||
this.orthoCamera.updateProjectionMatrix()
|
||||
this.orthoCamera.position.copy( this.camera.position )
|
||||
this.orthoCamera.quaternion.copy( this.camera.quaternion )
|
||||
|
||||
this.controls.camera = this.orthoCamera
|
||||
|
||||
// fit the camera inside, so we don't have clipping plane issues.
|
||||
// WIP implementation
|
||||
let camPos = this.orthoCamera.position
|
||||
let box = new THREE.Box3().setFromObject( this.viewer.sceneManager.sceneObjects.allObjects )
|
||||
let sphere = new THREE.Sphere()
|
||||
box.getBoundingSphere( sphere )
|
||||
|
||||
let dist = sphere.distanceToPoint( camPos )
|
||||
if( dist < 0 ) {
|
||||
dist *= -1
|
||||
this.controls.setPosition( camPos.x + dist, camPos.y + dist, camPos.z + dist )
|
||||
}
|
||||
|
||||
this.viewer.emit( 'projection-change', 'ortho' )
|
||||
}
|
||||
|
||||
setupPerspectiveCamera() {
|
||||
this.controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY
|
||||
this.camera.position.copy( this.orthoCamera.position )
|
||||
this.camera.quaternion.copy( this.orthoCamera.quaternion )
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.distance = this.previousDistance
|
||||
this.controls.camera = this.camera
|
||||
this.controls.zoomTo( 1 )
|
||||
this.enableRotations()
|
||||
this.viewer.emit( 'projection-change', 'perspective' )
|
||||
}
|
||||
|
||||
disableRotations() {
|
||||
this.controls.mouseButtons.left = CameraControls.ACTION.TRUCK
|
||||
}
|
||||
|
||||
enableRotations() {
|
||||
this.controls.mouseButtons.left = CameraControls.ACTION.ROTATE
|
||||
}
|
||||
|
||||
setupWASDControls() {
|
||||
const KEYCODE = { W: 87, A: 65, S: 83, D: 68 }
|
||||
|
||||
const wKey = new KeyboardKeyHold( KEYCODE.W, 16.666 )
|
||||
const aKey = new KeyboardKeyHold( KEYCODE.A, 16.666 )
|
||||
const sKey = new KeyboardKeyHold( KEYCODE.S, 16.666 )
|
||||
const dKey = new KeyboardKeyHold( KEYCODE.D, 16.666 )
|
||||
aKey.addEventListener( 'holding', function( event ) { if( this.viewer.mouseOverRenderer === false ) return; this.controls.truck( -0.01 * event.deltaTime, 0, false ); return }.bind( this ) )
|
||||
dKey.addEventListener( 'holding', function( event ) { if( this.viewer.mouseOverRenderer === false ) return; this.controls.truck( 0.01 * event.deltaTime, 0, false ); return}.bind( this ) )
|
||||
wKey.addEventListener( 'holding', function( event ) { if( this.viewer.mouseOverRenderer === false ) return; this.controls.forward( 0.01 * event.deltaTime, false ); return }.bind( this ) )
|
||||
sKey.addEventListener( 'holding', function( event ) { if( this.viewer.mouseOverRenderer === false ) return; this.controls.forward( -0.01 * event.deltaTime, false ); return }.bind( this ) )
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = this.viewer.container.offsetWidth / this.viewer.container.offsetHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
|
||||
let aspect = this.viewer.container.offsetWidth / this.viewer.container.offsetHeight
|
||||
let fustrumSize = 50
|
||||
this.orthoCamera.left = ( -fustrumSize * aspect ) / 2
|
||||
this.orthoCamera.right = ( fustrumSize * aspect ) / 2
|
||||
this.orthoCamera.top = fustrumSize / 2
|
||||
this.orthoCamera.bottom = -fustrumSize / 2
|
||||
this.orthoCamera.updateProjectionMatrix()
|
||||
}
|
||||
}
|
||||
+71
-21
@@ -1,5 +1,8 @@
|
||||
import { chunk } from 'lodash'
|
||||
import * as THREE from 'three'
|
||||
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
// import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
|
||||
import ObjectWrapper from './ObjectWrapper'
|
||||
import { getConversionFactor } from './Units'
|
||||
|
||||
@@ -16,6 +19,19 @@ export default class Coverter {
|
||||
|
||||
this.objectLoader = objectLoader
|
||||
this.curveSegmentLength = 0.1
|
||||
|
||||
this.lastAsyncPause = Date.now()
|
||||
this.activePromises = 0
|
||||
this.maxChildrenPromises = 200
|
||||
}
|
||||
|
||||
async asyncPause() {
|
||||
// Don't freeze the UI when doing all those traversals
|
||||
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
||||
this.lastAsyncPause = Date.now()
|
||||
await new Promise( resolve => setTimeout( resolve, 0 ) )
|
||||
// if ( Date.now() - this.lastAsyncPause > 200 ) console.log( 'CONV Event loop lag: ', Date.now() - this.lastAsyncPause )
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,8 +42,11 @@ export default class Coverter {
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async traverseAndConvert( obj, callback, scale = true ) {
|
||||
//console.log("Active promises: ", this.activePromises)
|
||||
await this.asyncPause()
|
||||
|
||||
// Exit on primitives (string, ints, bools, bigints, etc.)
|
||||
if ( typeof obj !== 'object' ) return
|
||||
if ( obj === null || typeof obj !== 'object' ) return
|
||||
if ( obj.referencedId ) obj = await this.resolveReference( obj )
|
||||
|
||||
let childrenConversionPromisses = []
|
||||
@@ -36,10 +55,16 @@ export default class Coverter {
|
||||
if ( Array.isArray( obj ) ) {
|
||||
for ( let element of obj ) {
|
||||
if ( typeof element !== 'object' ) break // exit early for non-object based arrays
|
||||
let childPromise = this.traverseAndConvert( element, callback, scale )
|
||||
childrenConversionPromisses.push( childPromise )
|
||||
if ( this.activePromises >= this.maxChildrenPromises ) {
|
||||
await this.traverseAndConvert( element, callback, scale )
|
||||
} else {
|
||||
let childPromise = this.traverseAndConvert( element, callback, scale )
|
||||
childrenConversionPromisses.push( childPromise )
|
||||
}
|
||||
}
|
||||
this.activePromises += childrenConversionPromisses.length
|
||||
await Promise.all( childrenConversionPromisses )
|
||||
this.activePromises -= childrenConversionPromisses.length
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,7 +73,7 @@ export default class Coverter {
|
||||
|
||||
if ( this[`${type}ToBufferGeometry`] ) {
|
||||
try {
|
||||
callback( await this[`${type}ToBufferGeometry`]( obj.data || obj, scale ) )
|
||||
await callback( await this[`${type}ToBufferGeometry`]( obj.data || obj, scale ) )
|
||||
return
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing - direct) Failed to convert ${type} with id: ${obj.id}`, e )
|
||||
@@ -65,7 +90,7 @@ export default class Coverter {
|
||||
if ( !displayValue.units ) displayValue.units = obj.units
|
||||
try {
|
||||
let { bufferGeometry } = await this.convert( displayValue, scale )
|
||||
callback( new ObjectWrapper( bufferGeometry, obj ) ) // use the parent's metadata!
|
||||
await callback( new ObjectWrapper( bufferGeometry, obj ) ) // use the parent's metadata!
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing) Failed to convert obj with id: ${obj.id} — ${e.message}` )
|
||||
}
|
||||
@@ -74,7 +99,7 @@ export default class Coverter {
|
||||
let val = await this.resolveReference( element )
|
||||
if ( !val.units ) val.units = obj.units
|
||||
let { bufferGeometry } = await this.convert( val, scale )
|
||||
callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial, ...obj } ) )
|
||||
await callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial, ...obj } ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +108,9 @@ export default class Coverter {
|
||||
if ( displayValue && obj.speckle_type.toLowerCase().includes( 'builtelements' ) ) {
|
||||
if ( obj['elements'] ) {
|
||||
childrenConversionPromisses.push( this.traverseAndConvert( obj['elements'], callback, scale ) )
|
||||
this.activePromises += childrenConversionPromisses.length
|
||||
await Promise.all( childrenConversionPromisses )
|
||||
this.activePromises -= childrenConversionPromisses.length
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -93,10 +120,16 @@ export default class Coverter {
|
||||
for ( let prop in target ) {
|
||||
if ( prop === 'bbox' ) continue
|
||||
if ( typeof target[prop] !== 'object' ) continue
|
||||
let childPromise = this.traverseAndConvert( target[prop], callback, scale )
|
||||
childrenConversionPromisses.push( childPromise )
|
||||
if ( this.activePromises >= this.maxChildrenPromises ) {
|
||||
await this.traverseAndConvert( target[prop], callback, scale )
|
||||
} else {
|
||||
let childPromise = this.traverseAndConvert( target[prop], callback, scale )
|
||||
childrenConversionPromisses.push( childPromise )
|
||||
}
|
||||
}
|
||||
this.activePromises += childrenConversionPromisses.length
|
||||
await Promise.all( childrenConversionPromisses )
|
||||
this.activePromises -= childrenConversionPromisses.length
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,11 +163,15 @@ export default class Coverter {
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if ( !arr[0].referencedId ) return arr
|
||||
|
||||
let dechunked = []
|
||||
let chunked = []
|
||||
for ( let ref of arr ) {
|
||||
let real = await this.objectLoader.getObject( ref.referencedId )
|
||||
dechunked.push( ...real.data )
|
||||
chunked.push( real.data )
|
||||
// await this.asyncPause()
|
||||
}
|
||||
|
||||
let dechunked = [].concat( ...chunked )
|
||||
|
||||
return dechunked
|
||||
}
|
||||
|
||||
@@ -144,8 +181,11 @@ export default class Coverter {
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async resolveReference( obj ) {
|
||||
if ( obj.referencedId )
|
||||
return await this.objectLoader.getObject( obj.referencedId )
|
||||
if ( obj.referencedId ) {
|
||||
let resolvedObj = await this.objectLoader.getObject( obj.referencedId )
|
||||
// this.asyncPause()
|
||||
return resolvedObj
|
||||
}
|
||||
else return obj
|
||||
}
|
||||
|
||||
@@ -178,7 +218,7 @@ export default class Coverter {
|
||||
let cF = scale ? getConversionFactor( obj.units ) : 1
|
||||
let definition = await this.resolveReference( obj.blockDefinition )
|
||||
|
||||
const matrix = new THREE.Matrix4().set( ...obj.transform )
|
||||
const matrix = new THREE.Matrix4().set( ...( Array.isArray( obj.transform ) ? obj.transform : obj.transform.value ) )
|
||||
let geoms = []
|
||||
for ( let obj of definition.geometry ) {
|
||||
// Note: we are passing scale = false to the conversion of all objects, as scaling *needs* to happen
|
||||
@@ -256,6 +296,8 @@ export default class Coverter {
|
||||
}
|
||||
|
||||
async MeshToBufferGeometry( obj, scale = true ) {
|
||||
// await this.asyncPause()
|
||||
|
||||
try {
|
||||
if ( !obj ) return
|
||||
|
||||
@@ -271,15 +313,23 @@ export default class Coverter {
|
||||
|
||||
let k = 0
|
||||
while ( k < faces.length ) {
|
||||
if ( faces[ k ] === 1 ) { // QUAD FACE
|
||||
let n = faces[ k ]
|
||||
if ( n <= 3 ) n += 3 // 0 -> 3, 1 -> 4
|
||||
|
||||
if ( n === 4 ) { // QUAD FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
indices.push( faces[ k + 1 ], faces[ k + 3 ], faces[ k + 4 ] )
|
||||
k += 5
|
||||
} else if ( faces[ k ] === 0 ) { // TRIANGLE FACE
|
||||
} else if ( n === 3 ) { // TRIANGLE FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
k += 4
|
||||
} else throw new Error( `Mesh type not supported. Face topology indicator: ${faces[k]}` )
|
||||
}
|
||||
} else { //N-GON FACE
|
||||
//TODO triangulate n-gon face.
|
||||
|
||||
//There is no need to throw an exception here, unsupported faces will simply be ignored.
|
||||
//throw new Error( `Mesh type not supported. Face topology indicator: ${faces[k]}` )
|
||||
}
|
||||
|
||||
k += n + 1
|
||||
}
|
||||
buffer.setIndex( indices )
|
||||
|
||||
buffer.setAttribute(
|
||||
@@ -308,7 +358,7 @@ export default class Coverter {
|
||||
|
||||
|
||||
buffer.computeVertexNormals( )
|
||||
buffer.computeFaceNormals( )
|
||||
//buffer.computeFaceNormals( )
|
||||
buffer.computeBoundingSphere( )
|
||||
|
||||
// delete obj.vertices
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,11 +46,12 @@ const config = {
|
||||
extensions: [ '.json', '.js' ],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join( __dirname, 'example' ),
|
||||
static: path.join( __dirname, 'example' ),
|
||||
compress: false,
|
||||
port: 9000,
|
||||
serveIndex: true,
|
||||
writeToDisk: true
|
||||
devMiddleware: {
|
||||
writeToDisk: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
"homepage": "https://github.com/specklesystems/speckle-server#readme",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "node src/main.js"
|
||||
"dev": "cross-env ALLOW_LOCAL_NETWORK=true node src/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"knex": "^0.95.7",
|
||||
"node-fetch": "^2.6.1",
|
||||
"pg": "^8.6.0"
|
||||
"pg": "^8.6.0",
|
||||
"private-ip": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const crypto = require( 'crypto' )
|
||||
const knex = require( './knex' )
|
||||
|
||||
const { makeNetworkRequest } = require( './webhookCaller' )
|
||||
const { makeNetworkRequest, isLocalNetworkUrl } = require( './webhookCaller' )
|
||||
|
||||
async function startTask() {
|
||||
let { rows } = await knex.raw( `
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
'use strict'
|
||||
|
||||
const dns = require( 'dns' )
|
||||
const isIpPrivate = require( 'private-ip' )
|
||||
|
||||
// Ignore invalid/self-signed https certificate errors for the entire process
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const fetch = require( 'node-fetch' )
|
||||
var debug = require( 'debug' )( 'speckle' )
|
||||
|
||||
async function isLocalNetworkUrl( url ) {
|
||||
let parsedUrl = new URL( url )
|
||||
let hostname = parsedUrl.hostname
|
||||
let ip = await new Promise( ( resolve, reject ) => {
|
||||
dns.lookup( hostname, ( err, addr, fam ) => {
|
||||
if ( err ) {
|
||||
reject( err )
|
||||
} else {
|
||||
resolve( addr )
|
||||
}
|
||||
} )
|
||||
} )
|
||||
|
||||
return isIpPrivate( ip )
|
||||
}
|
||||
|
||||
async function makeNetworkRequest( { url, data, headersData } ) {
|
||||
if ( process.env.ALLOW_LOCAL_NETWORK !== 'true' && isLocalNetworkUrl( url ) ) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Local network requests are not allowed. To allow, use ALLOW_LOCAL_NETWORK=true environment variable',
|
||||
duration: 0,
|
||||
responseCode: null,
|
||||
responseBody: null
|
||||
}
|
||||
}
|
||||
|
||||
let httpSuccessCodes = [ 200 ]
|
||||
let headers = { 'Content-Type': 'application/json' }
|
||||
for ( let k in headersData ) headers[ k ] = headersData[ k ]
|
||||
@@ -45,5 +74,5 @@ async function makeNetworkRequest( { url, data, headersData } ) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { makeNetworkRequest }
|
||||
module.exports = { makeNetworkRequest, isLocalNetworkUrl }
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Make sure to also check and ⭐️ these other Speckle repositories:
|
||||
- [`speckle-unreal`](https://github.com/specklesystems/speckle-unreal): Unreal Engine Connector
|
||||
- [`speckle-qgis`](https://github.com/specklesystems/speckle-qgis): QGIS connectod
|
||||
- [`speckle-powerbi`](https://github.com/specklesystems/speckle-powerbi): PowerBi connector
|
||||
- and more [connectos & tooling](https://github.com/specklesystems/)!
|
||||
- and more [connectors & tooling](https://github.com/specklesystems/)!
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v2
|
||||
name: speckle-server
|
||||
description: Speckle Server
|
||||
|
||||
type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
|
||||
# Set by the build process to the corect value
|
||||
# version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
|
||||
# Set by the build process to the corect value
|
||||
# appVersion: "2.3.3"
|
||||
@@ -0,0 +1,12 @@
|
||||
{{ if .Values.db.useCertificate }}
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-certificate
|
||||
namespace: {{ .Values.namespace }}
|
||||
data:
|
||||
ca-certificate.crt: |
|
||||
{{ .Values.db.certificate | indent 4 }}
|
||||
|
||||
{{ end }}
|
||||
@@ -0,0 +1,185 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: speckle-server
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-server
|
||||
project: speckle-server
|
||||
spec:
|
||||
replicas: {{ .Values.server.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: speckle-server
|
||||
project: speckle-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: speckle-server
|
||||
project: speckle-server
|
||||
spec:
|
||||
priorityClassName: high-priority
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumes:
|
||||
- name: postgres-certificate
|
||||
configMap:
|
||||
name: postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
containers:
|
||||
- name: main
|
||||
image: speckle/speckle-server:{{ .Values.docker_image_tag }}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.server.requests.cpu }}
|
||||
memory: {{ .Values.server.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.server.limits.cpu }}
|
||||
memory: {{ .Values.server.limits.memory }}
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumeMounts:
|
||||
- name: postgres-certificate
|
||||
mountPath: /postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
env:
|
||||
- name: CANONICAL_URL
|
||||
value: https://{{ .Values.domain }}
|
||||
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: DEBUG
|
||||
value: "speckle:*"
|
||||
|
||||
- name: SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.secretName }}"
|
||||
key: session_secret
|
||||
|
||||
# *** Redis ***
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: redis_url
|
||||
|
||||
# *** PostgreSQL Database ***
|
||||
- name: POSTGRES_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: postgres_url
|
||||
|
||||
- name: PGSSLMODE
|
||||
value: "{{ .Values.db.PGSSLMODE }}"
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
- name: NODE_EXTRA_CA_CERTS
|
||||
value: "/postgres-certificate/ca-certificate.crt"
|
||||
{{- end }}
|
||||
|
||||
# *** S3 Object Storage ***
|
||||
{{- if .Values.s3.endpoint }}
|
||||
- name: S3_ENDPOINT
|
||||
value: {{ .Values.s3.endpoint }}
|
||||
- name: S3_ACCESS_KEY
|
||||
value: {{ .Values.s3.access_key }}
|
||||
- name: S3_BUCKET
|
||||
value: {{ .Values.s3.bucket }}
|
||||
- name: S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: s3_secret_key
|
||||
{{- end }}
|
||||
|
||||
# *** Authentication ***
|
||||
|
||||
# Local Auth
|
||||
{{- if .Values.server.auth.local.enabled }}
|
||||
- name: STRATEGY_LOCAL
|
||||
value: "true"
|
||||
{{- else }}
|
||||
- name: STRATEGY_LOCAL
|
||||
value: "false"
|
||||
{{- end }}
|
||||
|
||||
# Google Auth
|
||||
{{- if .Values.server.auth.google.enabled }}
|
||||
- name: STRATEGY_GOOGLE
|
||||
value: "true"
|
||||
- name: GOOGLE_CLIENT_ID
|
||||
value: {{ .Values.server.auth.google.client_id }}
|
||||
- name: GOOGLE_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: google_client_secret
|
||||
{{- end }}
|
||||
|
||||
# Github Auth
|
||||
{{- if .Values.server.auth.github.enabled }}
|
||||
- name: STRATEGY_GITHUB
|
||||
value: "true"
|
||||
- name: GITHUB_CLIENT_ID
|
||||
value: {{ .Values.server.auth.github.client_id }}
|
||||
- name: GITHUB_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: github_client_secret
|
||||
{{- end }}
|
||||
|
||||
# AzureAD Auth
|
||||
{{- if .Values.server.auth.azure_ad.enabled }}
|
||||
- name: STRATEGY_AZURE_AD
|
||||
value: "true"
|
||||
- name: AZURE_AD_ORG_NAME
|
||||
value: {{ .Values.server.auth.azure_ad.org_name }}
|
||||
- name: AZURE_AD_IDENTITY_METADATA
|
||||
value: {{ .Values.server.auth.azure_ad.identity_metadata }}
|
||||
- name: AZURE_AD_ISSUER
|
||||
value: {{ .Values.server.auth.azure_ad.issuer }}
|
||||
- name: AZURE_AD_CLIENT_ID
|
||||
value: {{ .Values.server.auth.azure_ad.client_id }}
|
||||
- name: AZURE_AD_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: azure_ad_client_secret
|
||||
{{- end }}
|
||||
|
||||
|
||||
# *** Email ***
|
||||
|
||||
{{- if .Values.server.email.enabled }}
|
||||
- name: EMAIL
|
||||
value: "true"
|
||||
- name: EMAIL_HOST
|
||||
value: "{{ .Values.server.email.host }}"
|
||||
- name: EMAIL_PORT
|
||||
value: "{{ .Values.server.email.port }}"
|
||||
- name: EMAIL_USERNAME
|
||||
value: "{{ .Values.server.email.username }}"
|
||||
- name: EMAIL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: email_password
|
||||
{{- end }}
|
||||
|
||||
# *** Tracking / Tracing ***
|
||||
- name: SENTRY_DSN
|
||||
value: {{ .Values.server.sentry_dns }}
|
||||
{{- if .Values.server.disable_tracing }}
|
||||
- name: DISABLE_TRACING
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.server.disable_tracking }}
|
||||
- name: DISABLE_TRACKING
|
||||
value: "true"
|
||||
{{- end }}
|
||||
@@ -0,0 +1,78 @@
|
||||
{{- if .Values.s3.endpoint }}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: speckle-fileimport-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-fileimport-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
replicas: {{ .Values.fileimport_service.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: speckle-fileimport-service
|
||||
project: speckle-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: speckle-fileimport-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
priorityClassName: low-priority
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumes:
|
||||
- name: postgres-certificate
|
||||
configMap:
|
||||
name: postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
containers:
|
||||
- name: main
|
||||
image: speckle/speckle-fileimport-service:{{ .Values.docker_image_tag }}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.fileimport_service.requests.cpu }}
|
||||
memory: {{ .Values.fileimport_service.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.fileimport_service.limits.cpu }}
|
||||
memory: {{ .Values.fileimport_service.limits.memory }}
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumeMounts:
|
||||
- name: postgres-certificate
|
||||
mountPath: /postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
env:
|
||||
- name: PG_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: postgres_url
|
||||
|
||||
- name: DEBUG
|
||||
value: "fileimport-service:*"
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
- name: NODE_EXTRA_CA_CERTS
|
||||
value: "/postgres-certificate/ca-certificate.crt"
|
||||
{{- end }}
|
||||
|
||||
|
||||
- name: S3_ENDPOINT
|
||||
value: {{ .Values.s3.endpoint }}
|
||||
- name: S3_ACCESS_KEY
|
||||
value: {{ .Values.s3.access_key }}
|
||||
- name: S3_BUCKET
|
||||
value: {{ .Values.s3.bucket }}
|
||||
- name: S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: s3_secret_key
|
||||
|
||||
{{- end }}
|
||||
@@ -0,0 +1,32 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: speckle-frontend
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-frontend
|
||||
project: speckle-server
|
||||
spec:
|
||||
replicas: {{ .Values.frontend.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: speckle-frontend
|
||||
project: speckle-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: speckle-frontend
|
||||
project: speckle-server
|
||||
spec:
|
||||
priorityClassName: high-priority
|
||||
|
||||
containers:
|
||||
- name: main
|
||||
image: speckle/speckle-frontend:{{ .Values.docker_image_tag }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.frontend.requests.cpu }}
|
||||
memory: {{ .Values.frontend.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.frontend.limits.cpu }}
|
||||
memory: {{ .Values.frontend.limits.memory }}
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: speckle-preview-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-preview-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
replicas: {{ .Values.preview_service.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: speckle-preview-service
|
||||
project: speckle-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: speckle-preview-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
priorityClassName: low-priority
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumes:
|
||||
- name: postgres-certificate
|
||||
configMap:
|
||||
name: postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
containers:
|
||||
- name: main
|
||||
image: speckle/speckle-preview-service:{{ .Values.docker_image_tag }}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.preview_service.requests.cpu }}
|
||||
memory: {{ .Values.preview_service.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.preview_service.limits.cpu }}
|
||||
memory: {{ .Values.preview_service.limits.memory }}
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumeMounts:
|
||||
- name: postgres-certificate
|
||||
mountPath: /postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
env:
|
||||
- name: PG_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: postgres_url
|
||||
|
||||
- name: DEBUG
|
||||
value: "preview-service:*"
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
- name: NODE_EXTRA_CA_CERTS
|
||||
value: "/postgres-certificate/ca-certificate.crt"
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: speckle-webhook-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-webhook-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
replicas: {{ .Values.webhook_service.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: speckle-webhook-service
|
||||
project: speckle-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: speckle-webhook-service
|
||||
project: speckle-server
|
||||
spec:
|
||||
priorityClassName: low-priority
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumes:
|
||||
- name: postgres-certificate
|
||||
configMap:
|
||||
name: postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
containers:
|
||||
- name: main
|
||||
image: speckle/speckle-webhook-service:{{ .Values.docker_image_tag }}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: {{ .Values.webhook_service.requests.cpu }}
|
||||
memory: {{ .Values.webhook_service.requests.memory }}
|
||||
limits:
|
||||
cpu: {{ .Values.webhook_service.limits.cpu }}
|
||||
memory: {{ .Values.webhook_service.limits.memory }}
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
volumeMounts:
|
||||
- name: postgres-certificate
|
||||
mountPath: /postgres-certificate
|
||||
{{- end }}
|
||||
|
||||
env:
|
||||
- name: PG_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.secretName }}
|
||||
key: postgres_url
|
||||
|
||||
- name: DEBUG
|
||||
value: "webhook-service:*"
|
||||
|
||||
{{- if .Values.db.useCertificate }}
|
||||
- name: NODE_EXTRA_CA_CERTS
|
||||
value: "/postgres-certificate/ca-certificate.crt"
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: speckle-server
|
||||
namespace: {{ .Values.namespace }}
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ .Values.cert_manager_issuer }}
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
||||
nginx.org/client-max-body-size: "100m"
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.domain }}
|
||||
secretName: server-tls
|
||||
rules:
|
||||
- host: {{ .Values.domain }}
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: speckle-frontend
|
||||
port:
|
||||
number: 80
|
||||
- pathType: Exact
|
||||
path: "/(graphql|explorer|(auth/.*)|(objects/.*)|(preview/.*)|(api/.*))"
|
||||
backend:
|
||||
service:
|
||||
name: speckle-server
|
||||
port:
|
||||
number: 3000
|
||||
@@ -0,0 +1,18 @@
|
||||
{{ if .Values.enable_prometheus_monitoring }}
|
||||
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: speckle-server
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-server
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
project: speckle-server
|
||||
endpoints:
|
||||
- port: web
|
||||
|
||||
{{ end }}
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: speckle-server
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-server
|
||||
project: speckle-server
|
||||
spec:
|
||||
selector:
|
||||
app: speckle-server
|
||||
project: speckle-server
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: web
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: speckle-frontend
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: speckle-frontend
|
||||
project: speckle-server
|
||||
spec:
|
||||
selector:
|
||||
app: speckle-frontend
|
||||
project: speckle-server
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: www
|
||||
port: 80
|
||||
targetPort: 80
|
||||
@@ -0,0 +1,99 @@
|
||||
namespace: speckle-test
|
||||
|
||||
domain: localhost
|
||||
|
||||
docker_image_tag: v2.3.3
|
||||
|
||||
db:
|
||||
# postgres_url: secret -> postgres_url
|
||||
useCertificate: false
|
||||
certificate: "" # Multi-line string with the contents of `ca-certificate.crt`
|
||||
PGSSLMODE: require
|
||||
|
||||
s3:
|
||||
endpoint: ""
|
||||
bucket: ""
|
||||
access_key: ""
|
||||
# secret_key: secret -> s3_secret_key
|
||||
|
||||
#redis:
|
||||
# redis_url: secret -> redis_url
|
||||
|
||||
server:
|
||||
replicas: 1
|
||||
# session_secret: secret -> `session_secret`
|
||||
auth:
|
||||
local:
|
||||
enabled: true
|
||||
google:
|
||||
enabled: false
|
||||
client_id: ""
|
||||
# client_secret: secret -> `google_client_secret`
|
||||
github:
|
||||
enabled: false
|
||||
client_id: ""
|
||||
# client_secret: secret -> `github_client_secret`
|
||||
azure_ad:
|
||||
enabled: false
|
||||
org_name: ""
|
||||
identity_metadata: ""
|
||||
issuer: ""
|
||||
client_id: ""
|
||||
# client_secret: secret -> `azure_ad_client_secret`
|
||||
email:
|
||||
enabled: false
|
||||
host: ""
|
||||
port: ""
|
||||
username: ""
|
||||
# password: secret -> `email_password`
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 3Gi
|
||||
|
||||
sentry_dns: ""
|
||||
disable_tracking: false
|
||||
disable_tracing: false
|
||||
|
||||
frontend:
|
||||
replicas: 1
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
|
||||
preview_service:
|
||||
replicas: 1
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 2Gi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 4Gi
|
||||
|
||||
webhook_service:
|
||||
replicas: 1
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
|
||||
fileimport_service:
|
||||
replicas: 1
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
secretName: server-vars
|
||||
|
||||
enable_prometheus_monitoring: false
|
||||
cert_manager_issuer: letsencrypt-staging
|
||||
Reference in New Issue
Block a user