Merge branch 'main' of github.com:specklesystems/speckle-server into gergo/node16

This commit is contained in:
Gergő Jedlicska
2021-12-07 11:27:05 +01:00
83 changed files with 4253 additions and 2902 deletions
+2 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -4
View File
@@ -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
+14
View File
@@ -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
+25
View File
@@ -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
+349 -183
View File
@@ -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
}
}
}
+5
View File
@@ -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 {
+36 -13
View File
@@ -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) => {
+2 -4
View File
@@ -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 {
+10 -10
View File
@@ -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
# }
# }
}
}
}
+6 -5
View File
@@ -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 -1
View File
@@ -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 -3
View File
@@ -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,
+182 -21
View File
@@ -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>
&nbsp;
<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>
&nbsp;
<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]
}
}
}
}
}
})
}
}
}
+29 -2
View File
@@ -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
View File
@@ -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) )
}
-13
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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 )
+36 -18
View File
@@ -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() }
}() )
+3 -2
View File
@@ -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 }]
]
}
+3 -2
View File
@@ -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'
}
}
+69 -55
View File
@@ -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>
+26 -22
View File
@@ -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"
}
}
+42
View File
@@ -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.
+10 -2
View File
@@ -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}.` )
+71 -55
View File
@@ -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 -1
View File
@@ -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 = {}
}
}
+163 -108
View File
@@ -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 ) {
+289
View File
@@ -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
}
}
+344 -208
View File
@@ -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 )
} )
}
}
+16 -21
View File
@@ -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
}
+119 -65
View File
@@ -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()
}
}
@@ -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
+4 -3
View File
@@ -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
}
}
}
+6 -2
View File
@@ -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"
}
}
+1 -1
View File
@@ -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( `
+30 -1
View File
@@ -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 }
+1 -1
View File
@@ -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/)!
+23
View File
@@ -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/
+19
View File
@@ -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
+99
View File
@@ -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