diff --git a/.circleci/build.sh b/.circleci/build.sh index 9ebe82eeb..9ea3f0bb6 100755 --- a/.circleci/build.sh +++ b/.circleci/build.sh @@ -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 diff --git a/.circleci/config.yml b/.circleci/config.yml index 48d3209a5..aee53c6a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.circleci/deploy.sh b/.circleci/deploy.sh index bc10c64e0..bf6839020 100755 --- a/.circleci/deploy.sh +++ b/.circleci/deploy.sh @@ -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 diff --git a/.circleci/deploy_in_new_setup.sh b/.circleci/deploy_in_new_setup.sh index bb20f60cd..f7f1d5861 100755 --- a/.circleci/deploy_in_new_setup.sh +++ b/.circleci/deploy_in_new_setup.sh @@ -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 diff --git a/.circleci/get_version.sh b/.circleci/get_version.sh new file mode 100755 index 000000000..6b90b026d --- /dev/null +++ b/.circleci/get_version.sh @@ -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 diff --git a/.circleci/publish_helm_chart.sh b/.circleci/publish_helm_chart.sh new file mode 100755 index 000000000..84eeb3557 --- /dev/null +++ b/.circleci/publish_helm_chart.sh @@ -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 diff --git a/package-lock.json b/package-lock.json index c9a5b041e..618d32150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 } } } diff --git a/packages/frontend/src/auth-helpers.js b/packages/frontend/src/auth-helpers.js index aafd3c2f3..8f75872cf 100644 --- a/packages/frontend/src/auth-helpers.js +++ b/packages/frontend/src/auth-helpers.js @@ -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) +} diff --git a/packages/frontend/src/components/CommitReceivedReceipts.vue b/packages/frontend/src/components/CommitReceivedReceipts.vue index 47d92a067..7013fdc8d 100644 --- a/packages/frontend/src/components/CommitReceivedReceipts.vue +++ b/packages/frontend/src/components/CommitReceivedReceipts.vue @@ -7,7 +7,7 @@ >
mdi-call-received diff --git a/packages/frontend/src/components/ListItemActivity.vue b/packages/frontend/src/components/ListItemActivity.vue index 1233e9dc0..0ef04e814 100644 --- a/packages/frontend/src/components/ListItemActivity.vue +++ b/packages/frontend/src/components/ListItemActivity.vue @@ -184,7 +184,7 @@ @@ -208,7 +208,9 @@ small color="primary" > - mdi-source-branch + + mdi-source-branch + {{ commit.branchName }} diff --git a/packages/frontend/src/components/PreviewImage.vue b/packages/frontend/src/components/PreviewImage.vue index 6009516f5..18e9c6698 100644 --- a/packages/frontend/src/components/PreviewImage.vue +++ b/packages/frontend/src/components/PreviewImage.vue @@ -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)' + }`" /> - - diff --git a/packages/frontend/src/views/admin/AdminUsers.vue b/packages/frontend/src/views/admin/AdminUsers.vue index ae7fd839e..c15fd2082 100644 --- a/packages/frontend/src/views/admin/AdminUsers.vue +++ b/packages/frontend/src/views/admin/AdminUsers.vue @@ -27,7 +27,9 @@ - {{ user.name }} + + {{ user.name }} + @@ -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!) { diff --git a/packages/frontend/src/views/auth/Login.vue b/packages/frontend/src/views/auth/Login.vue index 4a04a4ba0..33b0a68a2 100644 --- a/packages/frontend/src/views/auth/Login.vue +++ b/packages/frontend/src/views/auth/Login.vue @@ -4,7 +4,7 @@ :style="`${serverInfo.inviteOnly ? 'border: 2px solid #047EFB' : ''}`" rounded="lg" > -
+
mdi-shield-alert-outline This Speckle server is invite only.
@@ -81,7 +81,8 @@ - -
-
-
-

Viewer

-
Controls summary:
-

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.

-
-
- - - - - -
-
- - -
-
- - View: - - - - - -
-
-
-
-
-
-
-
-
+ -
+ + + Speckle Viewer + + + + + + + + + +
+
+
+

Viewer

+
Controls summary:
+

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.

+
+
+ + + + + +
+
+ + +
+
+ + View: + + + + + +
+ +
+ Used Memory (MB): - / + LoadProgress: - / + ViewerBusy: - / + Draw Calls: - +
+ +
- - +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 68e5d550b..1d35a45e3 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -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" } } diff --git a/packages/viewer/readme.md b/packages/viewer/readme.md index 5e4c0327f..a7fa415eb 100644 --- a/packages/viewer/readme.md +++ b/packages/viewer/readme.md @@ -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. diff --git a/packages/viewer/src/app.js b/packages/viewer/src/app.js index 5ecb4e5a7..6422b83d2 100644 --- a/packages/viewer/src/app.js +++ b/packages/viewer/src/app.js @@ -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}.` ) diff --git a/packages/viewer/src/assets/example.html b/packages/viewer/src/assets/example.html index a7020d472..daa287c18 100644 --- a/packages/viewer/src/assets/example.html +++ b/packages/viewer/src/assets/example.html @@ -1,58 +1,74 @@ - - - - Speckle Viewer - - - - - - - - -
-
-
-

Viewer

-
Controls summary:
-

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.

-
-
- - - - - -
-
- - -
-
- - View: - - - - - -
-
-
-
-
-
-
-
-
+ -
+ + + Speckle Viewer + + + + + + + + + +
+
+
+

Viewer

+
Controls summary:
+

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.

+
+
+ + + + + +
+
+ + +
+
+ + View: + + + + + + + +
+ +
+ Used Memory (MB): - / + LoadProgress: - / + ViewerBusy: - / + Draw Calls: - +
+ +
- - +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/packages/viewer/src/index.js b/packages/viewer/src/index.js index e238d53ee..e29f2d6b5 100644 --- a/packages/viewer/src/index.js +++ b/packages/viewer/src/index.js @@ -1,4 +1,4 @@ import Viewer from './modules/Viewer' -import Converter from './modules/Converter' +import Converter from './modules/converter/Converter' export { Viewer, Converter } diff --git a/packages/viewer/src/modules/FilteringManager.js b/packages/viewer/src/modules/FilteringManager.js new file mode 100644 index 000000000..f9eb75c8f --- /dev/null +++ b/packages/viewer/src/modules/FilteringManager.js @@ -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 = {} + } +} + diff --git a/packages/viewer/src/modules/InteractionHandler.js b/packages/viewer/src/modules/InteractionHandler.js index d4dab3a1d..7f3390f79 100644 --- a/packages/viewer/src/modules/InteractionHandler.js +++ b/packages/viewer/src/modules/InteractionHandler.js @@ -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 ) } } diff --git a/packages/viewer/src/modules/SceneObjectManager.js b/packages/viewer/src/modules/SceneObjectManager.js index 299aca39d..8742e2b3d 100644 --- a/packages/viewer/src/modules/SceneObjectManager.js +++ b/packages/viewer/src/modules/SceneObjectManager.js @@ -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 ) { diff --git a/packages/viewer/src/modules/SceneObjects.js b/packages/viewer/src/modules/SceneObjects.js new file mode 100644 index 000000000..0fae0ab13 --- /dev/null +++ b/packages/viewer/src/modules/SceneObjects.js @@ -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 + } + +} diff --git a/packages/viewer/src/modules/SectionBox.js b/packages/viewer/src/modules/SectionBox.js index dd18afb4d..7740431c4 100644 --- a/packages/viewer/src/modules/SectionBox.js +++ b/packages/viewer/src/modules/SectionBox.js @@ -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 + } + } diff --git a/packages/viewer/src/modules/SectionPlaneHelper.js b/packages/viewer/src/modules/SectionPlaneHelper.js deleted file mode 100644 index 80dd73c50..000000000 --- a/packages/viewer/src/modules/SectionPlaneHelper.js +++ /dev/null @@ -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 ) - } ) - - } - -} diff --git a/packages/viewer/src/modules/SelectionHelper.js b/packages/viewer/src/modules/SelectionHelper.js index 31aa3dc4e..463724629 100644 --- a/packages/viewer/src/modules/SelectionHelper.js +++ b/packages/viewer/src/modules/SelectionHelper.js @@ -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 } diff --git a/packages/viewer/src/modules/Viewer.js b/packages/viewer/src/modules/Viewer.js index 13e7a71aa..b76bd0ba0 100644 --- a/packages/viewer/src/modules/Viewer.js +++ b/packages/viewer/src/modules/Viewer.js @@ -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 :) } } diff --git a/packages/viewer/src/modules/ViewerObjectLoader.js b/packages/viewer/src/modules/ViewerObjectLoader.js index b9d69fdf6..3df1f3ef3 100644 --- a/packages/viewer/src/modules/ViewerObjectLoader.js +++ b/packages/viewer/src/modules/ViewerObjectLoader.js @@ -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 + } } diff --git a/packages/viewer/src/modules/context/CameraHanlder.js b/packages/viewer/src/modules/context/CameraHanlder.js new file mode 100644 index 000000000..8b5752dd4 --- /dev/null +++ b/packages/viewer/src/modules/context/CameraHanlder.js @@ -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() + } +} \ No newline at end of file diff --git a/packages/viewer/src/modules/Converter.js b/packages/viewer/src/modules/converter/Converter.js similarity index 85% rename from packages/viewer/src/modules/Converter.js rename to packages/viewer/src/modules/converter/Converter.js index 48f1f79b2..2b58524e3 100644 --- a/packages/viewer/src/modules/Converter.js +++ b/packages/viewer/src/modules/converter/Converter.js @@ -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 diff --git a/packages/viewer/src/modules/ObjectWrapper.js b/packages/viewer/src/modules/converter/ObjectWrapper.js similarity index 100% rename from packages/viewer/src/modules/ObjectWrapper.js rename to packages/viewer/src/modules/converter/ObjectWrapper.js diff --git a/packages/viewer/src/modules/Units.js b/packages/viewer/src/modules/converter/Units.js similarity index 100% rename from packages/viewer/src/modules/Units.js rename to packages/viewer/src/modules/converter/Units.js diff --git a/packages/viewer/src/modules/external/TransformControls.js b/packages/viewer/src/modules/external/TransformControls.js deleted file mode 100644 index 2955e291f..000000000 --- a/packages/viewer/src/modules/external/TransformControls.js +++ /dev/null @@ -1,1709 +0,0 @@ -/* eslint-disable */ -// src: https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/TransformControls.js -// Note: customisations: -// - cosmetic blues for materials -// - added "hover" event -// - added "translationDotsOnly" prop for box face manipulation - -import { - BoxGeometry, - BufferGeometry, - Color, - CylinderGeometry, - DoubleSide, - Euler, - Float32BufferAttribute, - Line, - LineBasicMaterial, - Matrix4, - Mesh, - MeshBasicMaterial, - Object3D, - OctahedronGeometry, - PlaneGeometry, - Quaternion, - Raycaster, - SphereGeometry, - TorusGeometry, - Vector3 -} from 'three'; - -var TransformControls = function ( camera, domElement, translationDotsOnly ) { - - if ( domElement === undefined ) { - - console.warn( 'THREE.TransformControls: The second parameter "domElement" is now mandatory.' ); - domElement = document; - - } - - Object3D.call( this ); - - this.visible = false; - this.domElement = domElement; - - var _gizmo = new TransformControlsGizmo( translationDotsOnly ); - this.add( _gizmo ); - - var _plane = new TransformControlsPlane(); - this.add( _plane ); - - var scope = this; - - // Define properties with getters/setter - // Setting the defined property will automatically trigger change event - // Defined properties are passed down to gizmo and plane - - defineProperty( 'camera', camera ); - defineProperty( 'object', undefined ); - defineProperty( 'enabled', true ); - defineProperty( 'axis', null ); - defineProperty( 'mode', 'translate' ); - defineProperty( 'translationSnap', null ); - defineProperty( 'rotationSnap', null ); - defineProperty( 'scaleSnap', null ); - defineProperty( 'space', 'world' ); - defineProperty( 'size', 1 ); - defineProperty( 'dragging', false ); - defineProperty( 'showX', true ); - defineProperty( 'showY', true ); - defineProperty( 'showZ', true ); - - var changeEvent = { type: 'change' }; - var mouseDownEvent = { type: 'mouseDown' }; - var mouseUpEvent = { type: 'mouseUp', mode: scope.mode }; - var objectChangeEvent = { type: 'objectChange' }; - - // Reusable utility variables - - var raycaster = new Raycaster(); - - function intersectObjectWithRay( object, raycaster, includeInvisible ) { - - var allIntersections = raycaster.intersectObject( object, true ); - - for ( var i = 0; i < allIntersections.length; i ++ ) { - - if ( allIntersections[ i ].object.visible || includeInvisible ) { - - return allIntersections[ i ]; - - } - - } - - return false; - - } - - var _tempVector = new Vector3(); - var _tempVector2 = new Vector3(); - var _tempQuaternion = new Quaternion(); - var _unit = { - X: new Vector3( 1, 0, 0 ), - Y: new Vector3( 0, 1, 0 ), - Z: new Vector3( 0, 0, 1 ) - }; - - var pointStart = new Vector3(); - var pointEnd = new Vector3(); - var offset = new Vector3(); - var rotationAxis = new Vector3(); - var startNorm = new Vector3(); - var endNorm = new Vector3(); - var rotationAngle = 0; - - var cameraPosition = new Vector3(); - var cameraQuaternion = new Quaternion(); - var cameraScale = new Vector3(); - - var parentPosition = new Vector3(); - var parentQuaternion = new Quaternion(); - var parentQuaternionInv = new Quaternion(); - var parentScale = new Vector3(); - - var worldPositionStart = new Vector3(); - var worldQuaternionStart = new Quaternion(); - var worldScaleStart = new Vector3(); - - var worldPosition = new Vector3(); - var worldQuaternion = new Quaternion(); - var worldQuaternionInv = new Quaternion(); - var worldScale = new Vector3(); - - var eye = new Vector3(); - - var positionStart = new Vector3(); - var quaternionStart = new Quaternion(); - var scaleStart = new Vector3(); - - // TODO: remove properties unused in plane and gizmo - - defineProperty( 'worldPosition', worldPosition ); - defineProperty( 'worldPositionStart', worldPositionStart ); - defineProperty( 'worldQuaternion', worldQuaternion ); - defineProperty( 'worldQuaternionStart', worldQuaternionStart ); - defineProperty( 'cameraPosition', cameraPosition ); - defineProperty( 'cameraQuaternion', cameraQuaternion ); - defineProperty( 'pointStart', pointStart ); - defineProperty( 'pointEnd', pointEnd ); - defineProperty( 'rotationAxis', rotationAxis ); - defineProperty( 'rotationAngle', rotationAngle ); - defineProperty( 'eye', eye ); - - { - - domElement.addEventListener( 'pointerdown', onPointerDown ); - domElement.addEventListener( 'pointermove', onPointerHover ); - scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); - - } - - this.dispose = function () { - - domElement.removeEventListener( 'pointerdown', onPointerDown ); - domElement.removeEventListener( 'pointermove', onPointerHover ); - scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); - scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); - - this.traverse( function ( child ) { - - if ( child.geometry ) child.geometry.dispose(); - if ( child.material ) child.material.dispose(); - - } ); - - }; - - // Set current object - this.attach = function ( object ) { - - this.object = object; - this.visible = true; - - return this; - - }; - - // Detach from object - this.detach = function () { - - this.object = undefined; - this.visible = false; - this.axis = null; - - return this; - - }; - - // Defined getter, setter and store for a property - function defineProperty( propName, defaultValue ) { - - var propValue = defaultValue; - - Object.defineProperty( scope, propName, { - - get: function () { - - return propValue !== undefined ? propValue : defaultValue; - - }, - - set: function ( value ) { - - if ( propValue !== value ) { - - propValue = value; - _plane[ propName ] = value; - _gizmo[ propName ] = value; - - scope.dispatchEvent( { type: propName + '-changed', value: value } ); - scope.dispatchEvent( changeEvent ); - - } - - } - - } ); - - scope[ propName ] = defaultValue; - _plane[ propName ] = defaultValue; - _gizmo[ propName ] = defaultValue; - - } - - // updateMatrixWorld updates key transformation variables - this.updateMatrixWorld = function () { - - if ( this.object !== undefined ) { - - this.object.updateMatrixWorld(); - - if ( this.object.parent === null ) { - - console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' ); - - } else { - - this.object.parent.matrixWorld.decompose( parentPosition, parentQuaternion, parentScale ); - - } - - this.object.matrixWorld.decompose( worldPosition, worldQuaternion, worldScale ); - - parentQuaternionInv.copy( parentQuaternion ).invert(); - worldQuaternionInv.copy( worldQuaternion ).invert(); - - } - - this.camera.updateMatrixWorld(); - this.camera.matrixWorld.decompose( cameraPosition, cameraQuaternion, cameraScale ); - - eye.copy( cameraPosition ).sub( worldPosition ).normalize(); - - Object3D.prototype.updateMatrixWorld.call( this ); - - }; - - this.pointerHover = function ( pointer ) { - - if ( this.object === undefined || this.dragging === true ) return; - - raycaster.setFromCamera( pointer, this.camera ); - - var intersect = intersectObjectWithRay( _gizmo.picker[ this.mode ], raycaster ); - - if ( intersect ) { - - this.axis = intersect.object.name; - - this.dispatchEvent( { type: 'hover', value: true } ); - - } else { - - this.axis = null; - - this.dispatchEvent( { type: 'hover', value: false } ); - } - - }; - - this.pointerDown = function ( pointer ) { - - if ( this.object === undefined || this.dragging === true || pointer.button !== 0 ) return; - - if ( this.axis !== null ) { - - raycaster.setFromCamera( pointer, this.camera ); - - var planeIntersect = intersectObjectWithRay( _plane, raycaster, true ); - - if ( planeIntersect ) { - - var space = this.space; - - if ( this.mode === 'scale' ) { - - space = 'local'; - - } else if ( this.axis === 'E' || this.axis === 'XYZE' || this.axis === 'XYZ' ) { - - space = 'world'; - - } - - if ( space === 'local' && this.mode === 'rotate' ) { - - var snap = this.rotationSnap; - - if ( this.axis === 'X' && snap ) this.object.rotation.x = Math.round( this.object.rotation.x / snap ) * snap; - if ( this.axis === 'Y' && snap ) this.object.rotation.y = Math.round( this.object.rotation.y / snap ) * snap; - if ( this.axis === 'Z' && snap ) this.object.rotation.z = Math.round( this.object.rotation.z / snap ) * snap; - - } - - this.object.updateMatrixWorld(); - this.object.parent.updateMatrixWorld(); - - positionStart.copy( this.object.position ); - quaternionStart.copy( this.object.quaternion ); - scaleStart.copy( this.object.scale ); - - this.object.matrixWorld.decompose( worldPositionStart, worldQuaternionStart, worldScaleStart ); - - pointStart.copy( planeIntersect.point ).sub( worldPositionStart ); - - } - - this.dragging = true; - mouseDownEvent.mode = this.mode; - this.dispatchEvent( mouseDownEvent ); - - } - - }; - - this.pointerMove = function ( pointer ) { - - var axis = this.axis; - var mode = this.mode; - var object = this.object; - var space = this.space; - - if ( mode === 'scale' ) { - - space = 'local'; - - } else if ( axis === 'E' || axis === 'XYZE' || axis === 'XYZ' ) { - - space = 'world'; - - } - - if ( object === undefined || axis === null || this.dragging === false || pointer.button !== - 1 ) return; - - raycaster.setFromCamera( pointer, this.camera ); - - var planeIntersect = intersectObjectWithRay( _plane, raycaster, true ); - - if ( ! planeIntersect ) return; - - pointEnd.copy( planeIntersect.point ).sub( worldPositionStart ); - - if ( mode === 'translate' ) { - - // Apply translate - - offset.copy( pointEnd ).sub( pointStart ); - - if ( space === 'local' && axis !== 'XYZ' ) { - - offset.applyQuaternion( worldQuaternionInv ); - - } - - if ( axis.indexOf( 'X' ) === - 1 ) offset.x = 0; - if ( axis.indexOf( 'Y' ) === - 1 ) offset.y = 0; - if ( axis.indexOf( 'Z' ) === - 1 ) offset.z = 0; - - if ( space === 'local' && axis !== 'XYZ' ) { - - offset.applyQuaternion( quaternionStart ).divide( parentScale ); - - } else { - - offset.applyQuaternion( parentQuaternionInv ).divide( parentScale ); - - } - - object.position.copy( offset ).add( positionStart ); - - // Apply translation snap - - if ( this.translationSnap ) { - - if ( space === 'local' ) { - - object.position.applyQuaternion( _tempQuaternion.copy( quaternionStart ).invert() ); - - if ( axis.search( 'X' ) !== - 1 ) { - - object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; - - } - - if ( axis.search( 'Y' ) !== - 1 ) { - - object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; - - } - - if ( axis.search( 'Z' ) !== - 1 ) { - - object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; - - } - - object.position.applyQuaternion( quaternionStart ); - - } - - if ( space === 'world' ) { - - if ( object.parent ) { - - object.position.add( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); - - } - - if ( axis.search( 'X' ) !== - 1 ) { - - object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; - - } - - if ( axis.search( 'Y' ) !== - 1 ) { - - object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; - - } - - if ( axis.search( 'Z' ) !== - 1 ) { - - object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; - - } - - if ( object.parent ) { - - object.position.sub( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) ); - - } - - } - - } - - } else if ( mode === 'scale' ) { - - if ( axis.search( 'XYZ' ) !== - 1 ) { - - var d = pointEnd.length() / pointStart.length(); - - if ( pointEnd.dot( pointStart ) < 0 ) d *= - 1; - - _tempVector2.set( d, d, d ); - - } else { - - _tempVector.copy( pointStart ); - _tempVector2.copy( pointEnd ); - - _tempVector.applyQuaternion( worldQuaternionInv ); - _tempVector2.applyQuaternion( worldQuaternionInv ); - - _tempVector2.divide( _tempVector ); - - if ( axis.search( 'X' ) === - 1 ) { - - _tempVector2.x = 1; - - } - - if ( axis.search( 'Y' ) === - 1 ) { - - _tempVector2.y = 1; - - } - - if ( axis.search( 'Z' ) === - 1 ) { - - _tempVector2.z = 1; - - } - - } - - // Apply scale - - object.scale.copy( scaleStart ).multiply( _tempVector2 ); - - if ( this.scaleSnap ) { - - if ( axis.search( 'X' ) !== - 1 ) { - - object.scale.x = Math.round( object.scale.x / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; - - } - - if ( axis.search( 'Y' ) !== - 1 ) { - - object.scale.y = Math.round( object.scale.y / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; - - } - - if ( axis.search( 'Z' ) !== - 1 ) { - - object.scale.z = Math.round( object.scale.z / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; - - } - - } - - } else if ( mode === 'rotate' ) { - - offset.copy( pointEnd ).sub( pointStart ); - - var ROTATION_SPEED = 20 / worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) ); - - if ( axis === 'E' ) { - - rotationAxis.copy( eye ); - rotationAngle = pointEnd.angleTo( pointStart ); - - startNorm.copy( pointStart ).normalize(); - endNorm.copy( pointEnd ).normalize(); - - rotationAngle *= ( endNorm.cross( startNorm ).dot( eye ) < 0 ? 1 : - 1 ); - - } else if ( axis === 'XYZE' ) { - - rotationAxis.copy( offset ).cross( eye ).normalize(); - rotationAngle = offset.dot( _tempVector.copy( rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED; - - } else if ( axis === 'X' || axis === 'Y' || axis === 'Z' ) { - - rotationAxis.copy( _unit[ axis ] ); - - _tempVector.copy( _unit[ axis ] ); - - if ( space === 'local' ) { - - _tempVector.applyQuaternion( worldQuaternion ); - - } - - rotationAngle = offset.dot( _tempVector.cross( eye ).normalize() ) * ROTATION_SPEED; - - } - - // Apply rotation snap - - if ( this.rotationSnap ) rotationAngle = Math.round( rotationAngle / this.rotationSnap ) * this.rotationSnap; - - this.rotationAngle = rotationAngle; - - // Apply rotate - if ( space === 'local' && axis !== 'E' && axis !== 'XYZE' ) { - - object.quaternion.copy( quaternionStart ); - object.quaternion.multiply( _tempQuaternion.setFromAxisAngle( rotationAxis, rotationAngle ) ).normalize(); - - } else { - - rotationAxis.applyQuaternion( parentQuaternionInv ); - object.quaternion.copy( _tempQuaternion.setFromAxisAngle( rotationAxis, rotationAngle ) ); - object.quaternion.multiply( quaternionStart ).normalize(); - - } - - } - - this.dispatchEvent( changeEvent ); - this.dispatchEvent( objectChangeEvent ); - - }; - - this.pointerUp = function ( pointer ) { - - if ( pointer.button !== 0 ) return; - - if ( this.dragging && ( this.axis !== null ) ) { - - mouseUpEvent.mode = this.mode; - this.dispatchEvent( mouseUpEvent ); - - } - - this.dragging = false; - this.axis = null; - - }; - - // normalize mouse / touch pointer and remap {x,y} to view space. - - function getPointer( event ) { - - if ( scope.domElement.ownerDocument.pointerLockElement ) { - - return { - x: 0, - y: 0, - button: event.button - }; - - } else { - - var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event; - - var rect = domElement.getBoundingClientRect(); - - return { - x: ( pointer.clientX - rect.left ) / rect.width * 2 - 1, - y: - ( pointer.clientY - rect.top ) / rect.height * 2 + 1, - button: event.button - }; - - } - - } - - // mouse / touch event handlers - - function onPointerHover( event ) { - - if ( ! scope.enabled ) return; - - switch ( event.pointerType ) { - - case 'mouse': - case 'pen': - scope.pointerHover( getPointer( event ) ); - break; - - } - - } - - function onPointerDown( event ) { - - if ( ! scope.enabled ) return; - - scope.domElement.style.touchAction = 'none'; // disable touch scroll - scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); - - scope.pointerHover( getPointer( event ) ); - scope.pointerDown( getPointer( event ) ); - - } - - function onPointerMove( event ) { - - if ( ! scope.enabled ) return; - - scope.pointerMove( getPointer( event ) ); - - } - - function onPointerUp( event ) { - - if ( ! scope.enabled ) return; - - scope.domElement.style.touchAction = ''; - scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); - - scope.pointerUp( getPointer( event ) ); - - } - - // TODO: deprecate - - this.getMode = function () { - - return scope.mode; - - }; - - this.setMode = function ( mode ) { - - scope.mode = mode; - - }; - - this.setTranslationSnap = function ( translationSnap ) { - - scope.translationSnap = translationSnap; - - }; - - this.setRotationSnap = function ( rotationSnap ) { - - scope.rotationSnap = rotationSnap; - - }; - - this.setScaleSnap = function ( scaleSnap ) { - - scope.scaleSnap = scaleSnap; - - }; - - this.setSize = function ( size ) { - - scope.size = size; - - }; - - this.setSpace = function ( space ) { - - scope.space = space; - - }; - - this.update = function () { - - console.warn( 'THREE.TransformControls: update function has no more functionality and therefore has been deprecated.' ); - - }; - -}; - -TransformControls.prototype = Object.assign( Object.create( Object3D.prototype ), { - - constructor: TransformControls, - - isTransformControls: true - -} ); - - -var TransformControlsGizmo = function ( translationDotsOnly ) { - - 'use strict'; - - Object3D.call( this ); - - this.type = 'TransformControlsGizmo'; - - // shared materials - - var gizmoMaterial = new MeshBasicMaterial( { - depthTest: false, - depthWrite: false, - transparent: true, - side: DoubleSide, - fog: false, - toneMapped: false - } ); - - var gizmoLineMaterial = new LineBasicMaterial( { - depthTest: false, - depthWrite: false, - transparent: true, - linewidth: 1, - fog: false, - toneMapped: false - } ); - - // Make unique material for each axis/color - - var matInvisible = gizmoMaterial.clone(); - matInvisible.opacity = 0.15; - - var matHelper = gizmoMaterial.clone(); - matHelper.opacity = 0.33; - - var matRed = gizmoMaterial.clone(); - matRed.color.set( 0x0A66FF ); - - var matGreen = gizmoMaterial.clone(); - matGreen.color.set( 0x0A66FF ); - - var matBlue = gizmoMaterial.clone(); - matBlue.color.set( 0x0A66FF ); - - var matWhiteTransparent = gizmoMaterial.clone(); - matWhiteTransparent.opacity = 0.25; - - var matYellowTransparent = matWhiteTransparent.clone(); - matYellowTransparent.color.set( 0x0A66FF ); - - var matCyanTransparent = matWhiteTransparent.clone(); - matCyanTransparent.color.set( 0x0A66FF ); - - var matMagentaTransparent = matWhiteTransparent.clone(); - matMagentaTransparent.color.set( 0x0A66FF ); - - var matYellow = gizmoMaterial.clone(); - matYellow.color.set( 0x0A66FF ); - - var matLineRed = gizmoLineMaterial.clone(); - matLineRed.color.set( 0x0A66FF ); - - var matLineGreen = gizmoLineMaterial.clone(); - matLineGreen.color.set( 0x0A66FF ); - - var matLineBlue = gizmoLineMaterial.clone(); - matLineBlue.color.set( 0x0A66FF ); - - var matLineCyan = gizmoLineMaterial.clone(); - matLineCyan.color.set( 0x0A66FF ); - - var matLineMagenta = gizmoLineMaterial.clone(); - matLineMagenta.color.set( 0x0A66FF ); - - var matLineYellow = gizmoLineMaterial.clone(); - matLineYellow.color.set( 0x0A66FF ); - - var matLineGray = gizmoLineMaterial.clone(); - matLineGray.color.set( 0x787878 ); - - var matLineYellowTransparent = matLineYellow.clone(); - matLineYellowTransparent.opacity = 0.25; - - // reusable geometry - - var arrowGeometry = new CylinderGeometry( 0, 0.1, 0.2, 12, 1, false ); - var sphereGeometry = new SphereGeometry( 0.075, 10, 10 ); - - var scaleHandleGeometry = new BoxGeometry( 0.125, 0.125, 0.125 ); - - var lineGeometry = new BufferGeometry(); - lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) ); - - var CircleGeometry = function ( radius, arc ) { - - var geometry = new BufferGeometry( ); - var vertices = []; - - for ( var i = 0; i <= 64 * arc; ++ i ) { - - vertices.push( 0, Math.cos( i / 32 * Math.PI ) * radius, Math.sin( i / 32 * Math.PI ) * radius ); - - } - - geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); - - return geometry; - - }; - - // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position - - var TranslateHelperGeometry = function () { - - var geometry = new BufferGeometry(); - - geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 1, 1 ], 3 ) ); - - return geometry; - - }; - - // Gizmo definitions - custom hierarchy definitions for setupGizmo() function - - var gizmoTranslate = { - X: [ - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matRed ), [ 1, 0, 0 ], [ 0, 0, - Math.PI / 2 ], null, 'fwd' ], - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matRed ), [ 1, 0, 0 ], [ 0, 0, Math.PI / 2 ], null, 'bwd' ], - [ translationDotsOnly ? null : new Line( lineGeometry, matLineRed ) ] - ], - Y: [ - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matGreen ), [ 0, 1, 0 ], null, null, 'fwd' ], - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matGreen ), [ 0, 1, 0 ], [ Math.PI, 0, 0 ], null, 'bwd' ], - [ translationDotsOnly ? null : new Line( lineGeometry, matLineGreen ), null, [ 0, 0, Math.PI / 2 ]] - ], - Z: [ - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matBlue ), [ 0, 0, 1 ], [ Math.PI / 2, 0, 0 ], null, 'fwd' ], - [ new Mesh( translationDotsOnly ? sphereGeometry : arrowGeometry, matBlue ), [ 0, 0, 1 ], [ - Math.PI / 2, 0, 0 ], null, 'bwd' ], - [ translationDotsOnly ? null : new Line( lineGeometry, matLineBlue ), null, [ 0, - Math.PI / 2, 0 ]] - ], - XYZ: [ - [ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent.clone() ), [ 0, 0, 0 ], [ 0, 0, 0 ]] - ], - XY: [ - [ new Mesh( new PlaneGeometry( 0.295, 0.295 ), matYellowTransparent.clone() ), [ 0.15, 0.15, 0 ]], - [ new Line( lineGeometry, matLineYellow ), [ 0.18, 0.3, 0 ], null, [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineYellow ), [ 0.3, 0.18, 0 ], [ 0, 0, Math.PI / 2 ], [ 0.125, 1, 1 ]] - ], - YZ: [ - [ new Mesh( new PlaneGeometry( 0.295, 0.295 ), matCyanTransparent.clone() ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]], - [ new Line( lineGeometry, matLineCyan ), [ 0, 0.18, 0.3 ], [ 0, 0, Math.PI / 2 ], [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineCyan ), [ 0, 0.3, 0.18 ], [ 0, - Math.PI / 2, 0 ], [ 0.125, 1, 1 ]] - ], - XZ: [ - [ new Mesh( new PlaneGeometry( 0.295, 0.295 ), matMagentaTransparent.clone() ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]], - [ new Line( lineGeometry, matLineMagenta ), [ 0.18, 0, 0.3 ], null, [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineMagenta ), [ 0.3, 0, 0.18 ], [ 0, - Math.PI / 2, 0 ], [ 0.125, 1, 1 ]] - ] - }; - - var pickerTranslate = { - X: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 1, 4, 1, false ), matInvisible ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ]] - ], - Y: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 1, 4, 1, false ), matInvisible ), [ 0, 0.6, 0 ]] - ], - Z: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 1, 4, 1, false ), matInvisible ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ]] - ], - XYZ: [ - [ new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ) ] - ], - XY: [ - [ new Mesh( new PlaneGeometry( 0.4, 0.4 ), matInvisible ), [ 0.2, 0.2, 0 ]] - ], - YZ: [ - [ new Mesh( new PlaneGeometry( 0.4, 0.4 ), matInvisible ), [ 0, 0.2, 0.2 ], [ 0, Math.PI / 2, 0 ]] - ], - XZ: [ - [ new Mesh( new PlaneGeometry( 0.4, 0.4 ), matInvisible ), [ 0.2, 0, 0.2 ], [ - Math.PI / 2, 0, 0 ]] - ] - }; - - var helperTranslate = { - START: [ - [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] - ], - END: [ - [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] - ], - DELTA: [ - [ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ] - ], - X: [ - [ translationDotsOnly ? null : new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] - ], - Y: [ - [ translationDotsOnly ? null : new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] - ], - Z: [ - [ translationDotsOnly ? null : new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] - ] - }; - - var gizmoRotate = { - X: [ - [ new Line( CircleGeometry( 1, 0.5 ), matLineRed ) ], - [ new Mesh( new OctahedronGeometry( 0.04, 0 ), matRed ), [ 0, 0, 0.99 ], null, [ 1, 3, 1 ]], - ], - Y: [ - [ new Line( CircleGeometry( 1, 0.5 ), matLineGreen ), null, [ 0, 0, - Math.PI / 2 ]], - [ new Mesh( new OctahedronGeometry( 0.04, 0 ), matGreen ), [ 0, 0, 0.99 ], null, [ 3, 1, 1 ]], - ], - Z: [ - [ new Line( CircleGeometry( 1, 0.5 ), matLineBlue ), null, [ 0, Math.PI / 2, 0 ]], - [ new Mesh( new OctahedronGeometry( 0.04, 0 ), matBlue ), [ 0.99, 0, 0 ], null, [ 1, 3, 1 ]], - ], - E: [ - [ new Line( CircleGeometry( 1.25, 1 ), matLineYellowTransparent ), null, [ 0, Math.PI / 2, 0 ]], - [ new Mesh( new CylinderGeometry( 0.03, 0, 0.15, 4, 1, false ), matLineYellowTransparent ), [ 1.17, 0, 0 ], [ 0, 0, - Math.PI / 2 ], [ 1, 1, 0.001 ]], - [ new Mesh( new CylinderGeometry( 0.03, 0, 0.15, 4, 1, false ), matLineYellowTransparent ), [ - 1.17, 0, 0 ], [ 0, 0, Math.PI / 2 ], [ 1, 1, 0.001 ]], - [ new Mesh( new CylinderGeometry( 0.03, 0, 0.15, 4, 1, false ), matLineYellowTransparent ), [ 0, - 1.17, 0 ], [ Math.PI, 0, 0 ], [ 1, 1, 0.001 ]], - [ new Mesh( new CylinderGeometry( 0.03, 0, 0.15, 4, 1, false ), matLineYellowTransparent ), [ 0, 1.17, 0 ], [ 0, 0, 0 ], [ 1, 1, 0.001 ]], - ], - XYZE: [ - [ new Line( CircleGeometry( 1, 1 ), matLineGray ), null, [ 0, Math.PI / 2, 0 ]] - ] - }; - - var helperRotate = { - AXIS: [ - [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] - ] - }; - - var pickerRotate = { - X: [ - [ new Mesh( new TorusGeometry( 1, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ]], - ], - Y: [ - [ new Mesh( new TorusGeometry( 1, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], - ], - Z: [ - [ new Mesh( new TorusGeometry( 1, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], - ], - E: [ - [ new Mesh( new TorusGeometry( 1.25, 0.1, 2, 24 ), matInvisible ) ] - ], - XYZE: [ - [ new Mesh( new SphereGeometry( 0.7, 10, 8 ), matInvisible ) ] - ] - }; - - var gizmoScale = { - X: [ - [ new Mesh( scaleHandleGeometry, matRed ), [ 0.8, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], - [ new Line( lineGeometry, matLineRed ), null, null, [ 0.8, 1, 1 ]] - ], - Y: [ - [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, 0.8, 0 ]], - [ new Line( lineGeometry, matLineGreen ), null, [ 0, 0, Math.PI / 2 ], [ 0.8, 1, 1 ]] - ], - Z: [ - [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, 0.8 ], [ Math.PI / 2, 0, 0 ]], - [ new Line( lineGeometry, matLineBlue ), null, [ 0, - Math.PI / 2, 0 ], [ 0.8, 1, 1 ]] - ], - XY: [ - [ new Mesh( scaleHandleGeometry, matYellowTransparent ), [ 0.85, 0.85, 0 ], null, [ 2, 2, 0.2 ]], - [ new Line( lineGeometry, matLineYellow ), [ 0.855, 0.98, 0 ], null, [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineYellow ), [ 0.98, 0.855, 0 ], [ 0, 0, Math.PI / 2 ], [ 0.125, 1, 1 ]] - ], - YZ: [ - [ new Mesh( scaleHandleGeometry, matCyanTransparent ), [ 0, 0.85, 0.85 ], null, [ 0.2, 2, 2 ]], - [ new Line( lineGeometry, matLineCyan ), [ 0, 0.855, 0.98 ], [ 0, 0, Math.PI / 2 ], [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineCyan ), [ 0, 0.98, 0.855 ], [ 0, - Math.PI / 2, 0 ], [ 0.125, 1, 1 ]] - ], - XZ: [ - [ new Mesh( scaleHandleGeometry, matMagentaTransparent ), [ 0.85, 0, 0.85 ], null, [ 2, 0.2, 2 ]], - [ new Line( lineGeometry, matLineMagenta ), [ 0.855, 0, 0.98 ], null, [ 0.125, 1, 1 ]], - [ new Line( lineGeometry, matLineMagenta ), [ 0.98, 0, 0.855 ], [ 0, - Math.PI / 2, 0 ], [ 0.125, 1, 1 ]] - ], - XYZX: [ - [ new Mesh( new BoxGeometry( 0.125, 0.125, 0.125 ), matWhiteTransparent.clone() ), [ 1.1, 0, 0 ]], - ], - XYZY: [ - [ new Mesh( new BoxGeometry( 0.125, 0.125, 0.125 ), matWhiteTransparent.clone() ), [ 0, 1.1, 0 ]], - ], - XYZZ: [ - [ new Mesh( new BoxGeometry( 0.125, 0.125, 0.125 ), matWhiteTransparent.clone() ), [ 0, 0, 1.1 ]], - ] - }; - - var pickerScale = { - X: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 0.8, 4, 1, false ), matInvisible ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]] - ], - Y: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 0.8, 4, 1, false ), matInvisible ), [ 0, 0.5, 0 ]] - ], - Z: [ - [ new Mesh( new CylinderGeometry( 0.2, 0, 0.8, 4, 1, false ), matInvisible ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]] - ], - XY: [ - [ new Mesh( scaleHandleGeometry, matInvisible ), [ 0.85, 0.85, 0 ], null, [ 3, 3, 0.2 ]], - ], - YZ: [ - [ new Mesh( scaleHandleGeometry, matInvisible ), [ 0, 0.85, 0.85 ], null, [ 0.2, 3, 3 ]], - ], - XZ: [ - [ new Mesh( scaleHandleGeometry, matInvisible ), [ 0.85, 0, 0.85 ], null, [ 3, 0.2, 3 ]], - ], - XYZX: [ - [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 1.1, 0, 0 ]], - ], - XYZY: [ - [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 1.1, 0 ]], - ], - XYZZ: [ - [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 0, 1.1 ]], - ] - }; - - var helperScale = { - X: [ - [ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] - ], - Y: [ - [ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] - ], - Z: [ - [ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] - ] - }; - - // Creates an Object3D with gizmos described in custom hierarchy definition. - - var setupGizmo = function ( gizmoMap ) { - - var gizmo = new Object3D(); - - for ( var name in gizmoMap ) { - - for ( var i = gizmoMap[ name ].length; i --; ) { - - var object = gizmoMap[ name ][ i ][ 0 ] - if( object ) object = object.clone(); - else continue - - var position = gizmoMap[ name ][ i ][ 1 ]; - var rotation = gizmoMap[ name ][ i ][ 2 ]; - var scale = gizmoMap[ name ][ i ][ 3 ]; - var tag = gizmoMap[ name ][ i ][ 4 ]; - - // name and tag properties are essential for picking and updating logic. - object.name = name; - object.tag = tag; - - if ( position ) { - - object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] ); - - } - - if ( rotation ) { - - object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] ); - - } - - if ( scale ) { - - object.scale.set( scale[ 0 ], scale[ 1 ], scale[ 2 ] ); - - } - - object.updateMatrix(); - - if( !translationDotsOnly ) { - - var tempGeometry = object.geometry.clone(); - - tempGeometry.applyMatrix4( object.matrix ); - - object.geometry = tempGeometry; - - } - - object.renderOrder = Infinity; - - object.position.set( 0, 0, 0 ); - object.rotation.set( 0, 0, 0 ); - object.scale.set( 1, 1, 1 ); - - gizmo.add( object ); - - } - - } - - return gizmo; - - }; - - // Reusable utility variables - - var tempVector = new Vector3( 0, 0, 0 ); - var tempEuler = new Euler(); - var alignVector = new Vector3( 0, 1, 0 ); - var zeroVector = new Vector3( 0, 0, 0 ); - var lookAtMatrix = new Matrix4(); - var tempQuaternion = new Quaternion(); - var tempQuaternion2 = new Quaternion(); - var identityQuaternion = new Quaternion(); - - var unitX = new Vector3( 1, 0, 0 ); - var unitY = new Vector3( 0, 1, 0 ); - var unitZ = new Vector3( 0, 0, 1 ); - - // Gizmo creation - - this.gizmo = {}; - this.picker = {}; - this.helper = {}; - - this.add( this.gizmo[ 'translate' ] = setupGizmo( gizmoTranslate ) ); - this.add( this.gizmo[ 'rotate' ] = setupGizmo( gizmoRotate ) ); - this.add( this.gizmo[ 'scale' ] = setupGizmo( gizmoScale ) ); - this.add( this.picker[ 'translate' ] = setupGizmo( pickerTranslate ) ); - this.add( this.picker[ 'rotate' ] = setupGizmo( pickerRotate ) ); - this.add( this.picker[ 'scale' ] = setupGizmo( pickerScale ) ); - this.add( this.helper[ 'translate' ] = setupGizmo( helperTranslate ) ); - this.add( this.helper[ 'rotate' ] = setupGizmo( helperRotate ) ); - this.add( this.helper[ 'scale' ] = setupGizmo( helperScale ) ); - - // Pickers should be hidden always - - this.picker[ 'translate' ].visible = false; - this.picker[ 'rotate' ].visible = false; - this.picker[ 'scale' ].visible = false; - - // updateMatrixWorld will update transformations and appearance of individual handles - - this.updateMatrixWorld = function () { - - var space = this.space; - - if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation - - var quaternion = space === 'local' ? this.worldQuaternion : identityQuaternion; - - // Show only gizmos for current transform mode - - this.gizmo[ 'translate' ].visible = this.mode === 'translate'; - this.gizmo[ 'rotate' ].visible = this.mode === 'rotate'; - this.gizmo[ 'scale' ].visible = this.mode === 'scale'; - - this.helper[ 'translate' ].visible = this.mode === 'translate'; - this.helper[ 'rotate' ].visible = this.mode === 'rotate'; - this.helper[ 'scale' ].visible = this.mode === 'scale'; - - - var handles = []; - handles = handles.concat( this.picker[ this.mode ].children ); - handles = handles.concat( this.gizmo[ this.mode ].children ); - handles = handles.concat( this.helper[ this.mode ].children ); - - for ( var i = 0; i < handles.length; i ++ ) { - - var handle = handles[ i ]; - - // hide aligned to camera - - handle.visible = true; - handle.rotation.set( 0, 0, 0 ); - handle.position.copy( this.worldPosition ); - - var factor; - - if ( this.camera.isOrthographicCamera ) { - - factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom; - - } else { - - factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 ); - - } - - handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 7 ); - - // TODO: simplify helpers and consider decoupling from gizmo - - if ( handle.tag === 'helper' ) { - - handle.visible = false; - - if ( handle.name === 'AXIS' ) { - - handle.position.copy( this.worldPositionStart ); - handle.visible = !! this.axis; - - if ( this.axis === 'X' ) { - - tempQuaternion.setFromEuler( tempEuler.set( 0, 0, 0 ) ); - handle.quaternion.copy( quaternion ).multiply( tempQuaternion ); - - if ( Math.abs( alignVector.copy( unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { - - handle.visible = false; - - } - - } - - if ( this.axis === 'Y' ) { - - tempQuaternion.setFromEuler( tempEuler.set( 0, 0, Math.PI / 2 ) ); - handle.quaternion.copy( quaternion ).multiply( tempQuaternion ); - - if ( Math.abs( alignVector.copy( unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { - - handle.visible = false; - - } - - } - - if ( this.axis === 'Z' ) { - - tempQuaternion.setFromEuler( tempEuler.set( 0, Math.PI / 2, 0 ) ); - handle.quaternion.copy( quaternion ).multiply( tempQuaternion ); - - if ( Math.abs( alignVector.copy( unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { - - handle.visible = false; - - } - - } - - if ( this.axis === 'XYZE' ) { - - tempQuaternion.setFromEuler( tempEuler.set( 0, Math.PI / 2, 0 ) ); - alignVector.copy( this.rotationAxis ); - handle.quaternion.setFromRotationMatrix( lookAtMatrix.lookAt( zeroVector, alignVector, unitY ) ); - handle.quaternion.multiply( tempQuaternion ); - handle.visible = this.dragging; - - } - - if ( this.axis === 'E' ) { - - handle.visible = false; - - } - - - } else if ( handle.name === 'START' ) { - - handle.position.copy( this.worldPositionStart ); - handle.visible = this.dragging; - - } else if ( handle.name === 'END' ) { - - handle.position.copy( this.worldPosition ); - handle.visible = this.dragging; - - } else if ( handle.name === 'DELTA' ) { - - handle.position.copy( this.worldPositionStart ); - handle.quaternion.copy( this.worldQuaternionStart ); - tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 ); - tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() ); - handle.scale.copy( tempVector ); - handle.visible = this.dragging; - - } else { - - handle.quaternion.copy( quaternion ); - - if ( this.dragging ) { - - handle.position.copy( this.worldPositionStart ); - - } else { - - handle.position.copy( this.worldPosition ); - - } - - if ( this.axis ) { - - handle.visible = this.axis.search( handle.name ) !== - 1; - - } - - } - - // If updating helper, skip rest of the loop - continue; - - } - - // Align handles to current local or world rotation - - handle.quaternion.copy( quaternion ); - - if ( this.mode === 'translate' || this.mode === 'scale' ) { - - // Hide translate and scale axis facing the camera - - var AXIS_HIDE_TRESHOLD = 0.99; - var PLANE_HIDE_TRESHOLD = 0.2; - var AXIS_FLIP_TRESHOLD = 0.0; - - - if ( handle.name === 'X' || handle.name === 'XYZX' ) { - - if ( Math.abs( alignVector.copy( unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - if ( handle.name === 'Y' || handle.name === 'XYZY' ) { - - if ( Math.abs( alignVector.copy( unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - if ( handle.name === 'Z' || handle.name === 'XYZZ' ) { - - if ( Math.abs( alignVector.copy( unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - if ( handle.name === 'XY' ) { - - if ( Math.abs( alignVector.copy( unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - if ( handle.name === 'YZ' ) { - - if ( Math.abs( alignVector.copy( unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - if ( handle.name === 'XZ' ) { - - if ( Math.abs( alignVector.copy( unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_TRESHOLD ) { - - handle.scale.set( 1e-10, 1e-10, 1e-10 ); - handle.visible = false; - - } - - } - - // Flip translate and scale axis ocluded behind another axis - - if ( handle.name.search( 'X' ) !== - 1 ) { - - if ( alignVector.copy( unitX ).applyQuaternion( quaternion ).dot( this.eye ) < AXIS_FLIP_TRESHOLD ) { - - if ( handle.tag === 'fwd' ) { - - handle.visible = false; - - } else { - - handle.scale.x *= - 1; - - } - - } else if ( handle.tag === 'bwd' ) { - - handle.visible = false; - - } - - } - - if ( handle.name.search( 'Y' ) !== - 1 ) { - - if ( alignVector.copy( unitY ).applyQuaternion( quaternion ).dot( this.eye ) < AXIS_FLIP_TRESHOLD ) { - - if ( handle.tag === 'fwd' ) { - - handle.visible = false; - - } else { - - handle.scale.y *= - 1; - - } - - } else if ( handle.tag === 'bwd' ) { - - handle.visible = false; - - } - - } - - if ( handle.name.search( 'Z' ) !== - 1 ) { - - if ( alignVector.copy( unitZ ).applyQuaternion( quaternion ).dot( this.eye ) < AXIS_FLIP_TRESHOLD ) { - - if ( handle.tag === 'fwd' ) { - - handle.visible = false; - - } else { - - handle.scale.z *= - 1; - - } - - } else if ( handle.tag === 'bwd' ) { - - handle.visible = false; - - } - - } - - } else if ( this.mode === 'rotate' ) { - - // Align handles to current local or world rotation - - tempQuaternion2.copy( quaternion ); - alignVector.copy( this.eye ).applyQuaternion( tempQuaternion.copy( quaternion ).invert() ); - - if ( handle.name.search( 'E' ) !== - 1 ) { - - handle.quaternion.setFromRotationMatrix( lookAtMatrix.lookAt( this.eye, zeroVector, unitY ) ); - - } - - if ( handle.name === 'X' ) { - - tempQuaternion.setFromAxisAngle( unitX, Math.atan2( - alignVector.y, alignVector.z ) ); - tempQuaternion.multiplyQuaternions( tempQuaternion2, tempQuaternion ); - handle.quaternion.copy( tempQuaternion ); - - } - - if ( handle.name === 'Y' ) { - - tempQuaternion.setFromAxisAngle( unitY, Math.atan2( alignVector.x, alignVector.z ) ); - tempQuaternion.multiplyQuaternions( tempQuaternion2, tempQuaternion ); - handle.quaternion.copy( tempQuaternion ); - - } - - if ( handle.name === 'Z' ) { - - tempQuaternion.setFromAxisAngle( unitZ, Math.atan2( alignVector.y, alignVector.x ) ); - tempQuaternion.multiplyQuaternions( tempQuaternion2, tempQuaternion ); - handle.quaternion.copy( tempQuaternion ); - - } - - } - - // Hide disabled axes - handle.visible = handle.visible && ( handle.name.indexOf( 'X' ) === - 1 || this.showX ); - handle.visible = handle.visible && ( handle.name.indexOf( 'Y' ) === - 1 || this.showY ); - handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ ); - handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) ); - - // highlight selected axis - - handle.material._opacity = handle.material._opacity || handle.material.opacity; - handle.material._color = handle.material._color || handle.material.color.clone(); - - handle.material.color.copy( handle.material._color ); - handle.material.opacity = handle.material._opacity; - - if ( ! this.enabled ) { - - handle.material.opacity *= 0.5; - handle.material.color.lerp( new Color( 1, 1, 1 ), 0.5 ); - - } else if ( this.axis ) { - - if ( handle.name === this.axis ) { - - handle.material.opacity = 1.0; - handle.material.color.lerp( new Color( 1, 1, 1 ), 0.5 ); - - } else if ( this.axis.split( '' ).some( function ( a ) { - - return handle.name === a; - - } ) ) { - - handle.material.opacity = 1.0; - handle.material.color.lerp( new Color( 1, 1, 1 ), 0.5 ); - - } else { - - handle.material.opacity *= 0.25; - handle.material.color.lerp( new Color( 1, 1, 1 ), 0.5 ); - - } - - } - - } - - Object3D.prototype.updateMatrixWorld.call( this ); - - }; - -}; - -TransformControlsGizmo.prototype = Object.assign( Object.create( Object3D.prototype ), { - - constructor: TransformControlsGizmo, - - isTransformControlsGizmo: true - -} ); - - -var TransformControlsPlane = function () { - - 'use strict'; - - Mesh.call( this, - new PlaneGeometry( 100000, 100000, 2, 2 ), - new MeshBasicMaterial( { visible: false, wireframe: true, side: DoubleSide, transparent: true, opacity: 0.1, toneMapped: false } ) - ); - - this.type = 'TransformControlsPlane'; - - var unitX = new Vector3( 1, 0, 0 ); - var unitY = new Vector3( 0, 1, 0 ); - var unitZ = new Vector3( 0, 0, 1 ); - - var tempVector = new Vector3(); - var dirVector = new Vector3(); - var alignVector = new Vector3(); - var tempMatrix = new Matrix4(); - var identityQuaternion = new Quaternion(); - - this.updateMatrixWorld = function () { - - var space = this.space; - - this.position.copy( this.worldPosition ); - - if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation - - unitX.set( 1, 0, 0 ).applyQuaternion( space === 'local' ? this.worldQuaternion : identityQuaternion ); - unitY.set( 0, 1, 0 ).applyQuaternion( space === 'local' ? this.worldQuaternion : identityQuaternion ); - unitZ.set( 0, 0, 1 ).applyQuaternion( space === 'local' ? this.worldQuaternion : identityQuaternion ); - - // Align the plane for current transform mode, axis and space. - - alignVector.copy( unitY ); - - switch ( this.mode ) { - - case 'translate': - case 'scale': - switch ( this.axis ) { - - case 'X': - alignVector.copy( this.eye ).cross( unitX ); - dirVector.copy( unitX ).cross( alignVector ); - break; - case 'Y': - alignVector.copy( this.eye ).cross( unitY ); - dirVector.copy( unitY ).cross( alignVector ); - break; - case 'Z': - alignVector.copy( this.eye ).cross( unitZ ); - dirVector.copy( unitZ ).cross( alignVector ); - break; - case 'XY': - dirVector.copy( unitZ ); - break; - case 'YZ': - dirVector.copy( unitX ); - break; - case 'XZ': - alignVector.copy( unitZ ); - dirVector.copy( unitY ); - break; - case 'XYZ': - case 'E': - dirVector.set( 0, 0, 0 ); - break; - - } - - break; - case 'rotate': - default: - // special case for rotate - dirVector.set( 0, 0, 0 ); - - } - - if ( dirVector.length() === 0 ) { - - // If in rotate mode, make the plane parallel to camera - this.quaternion.copy( this.cameraQuaternion ); - - } else { - - tempMatrix.lookAt( tempVector.set( 0, 0, 0 ), dirVector, alignVector ); - - this.quaternion.setFromRotationMatrix( tempMatrix ); - - } - - Object3D.prototype.updateMatrixWorld.call( this ); - - }; - -}; - -TransformControlsPlane.prototype = Object.assign( Object.create( Mesh.prototype ), { - - constructor: TransformControlsPlane, - - isTransformControlsPlane: true - -} ); - -export { TransformControls, TransformControlsGizmo, TransformControlsPlane }; diff --git a/packages/viewer/webpack.config.example.js b/packages/viewer/webpack.config.example.js index 7c0312d4d..a48bc6826 100644 --- a/packages/viewer/webpack.config.example.js +++ b/packages/viewer/webpack.config.example.js @@ -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 + } } } diff --git a/packages/webhook-service/package.json b/packages/webhook-service/package.json index b72b20d84..aba434fdb 100644 --- a/packages/webhook-service/package.json +++ b/packages/webhook-service/package.json @@ -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" } } diff --git a/packages/webhook-service/src/main.js b/packages/webhook-service/src/main.js index dc32c5e7b..d21e49ef6 100644 --- a/packages/webhook-service/src/main.js +++ b/packages/webhook-service/src/main.js @@ -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( ` diff --git a/packages/webhook-service/src/webhookCaller.js b/packages/webhook-service/src/webhookCaller.js index 057d9b4e5..e73db3a24 100644 --- a/packages/webhook-service/src/webhookCaller.js +++ b/packages/webhook-service/src/webhookCaller.js @@ -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 } diff --git a/readme.md b/readme.md index 33d7d9238..69d84f0a4 100644 --- a/readme.md +++ b/readme.md @@ -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/)! diff --git a/utils/helm/speckle-server/.helmignore b/utils/helm/speckle-server/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/utils/helm/speckle-server/.helmignore @@ -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/ diff --git a/utils/helm/speckle-server/Chart.yaml b/utils/helm/speckle-server/Chart.yaml new file mode 100644 index 000000000..ba4561c1d --- /dev/null +++ b/utils/helm/speckle-server/Chart.yaml @@ -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" diff --git a/utils/helm/speckle-server/templates/configmap-db-certificate.yml b/utils/helm/speckle-server/templates/configmap-db-certificate.yml new file mode 100644 index 000000000..d9e8a8b85 --- /dev/null +++ b/utils/helm/speckle-server/templates/configmap-db-certificate.yml @@ -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 }} diff --git a/utils/helm/speckle-server/templates/deployment-backend.yml b/utils/helm/speckle-server/templates/deployment-backend.yml new file mode 100644 index 000000000..138d92920 --- /dev/null +++ b/utils/helm/speckle-server/templates/deployment-backend.yml @@ -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 }} diff --git a/utils/helm/speckle-server/templates/deployment-fileimport-service.yml b/utils/helm/speckle-server/templates/deployment-fileimport-service.yml new file mode 100644 index 000000000..5d1a830b4 --- /dev/null +++ b/utils/helm/speckle-server/templates/deployment-fileimport-service.yml @@ -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 }} diff --git a/utils/helm/speckle-server/templates/deployment-frontend.yml b/utils/helm/speckle-server/templates/deployment-frontend.yml new file mode 100644 index 000000000..4b1e371f7 --- /dev/null +++ b/utils/helm/speckle-server/templates/deployment-frontend.yml @@ -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 }} diff --git a/utils/helm/speckle-server/templates/deployment-preview-service.yml b/utils/helm/speckle-server/templates/deployment-preview-service.yml new file mode 100644 index 000000000..09d205769 --- /dev/null +++ b/utils/helm/speckle-server/templates/deployment-preview-service.yml @@ -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 }} + diff --git a/utils/helm/speckle-server/templates/deployment-webhook-service.yml b/utils/helm/speckle-server/templates/deployment-webhook-service.yml new file mode 100644 index 000000000..49408b406 --- /dev/null +++ b/utils/helm/speckle-server/templates/deployment-webhook-service.yml @@ -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 }} + diff --git a/utils/helm/speckle-server/templates/ingress.yml b/utils/helm/speckle-server/templates/ingress.yml new file mode 100644 index 000000000..baadb5bf3 --- /dev/null +++ b/utils/helm/speckle-server/templates/ingress.yml @@ -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 diff --git a/utils/helm/speckle-server/templates/servicemonitor.yml b/utils/helm/speckle-server/templates/servicemonitor.yml new file mode 100644 index 000000000..4d2ec826d --- /dev/null +++ b/utils/helm/speckle-server/templates/servicemonitor.yml @@ -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 }} diff --git a/utils/helm/speckle-server/templates/services.yml b/utils/helm/speckle-server/templates/services.yml new file mode 100644 index 000000000..0ef12ae92 --- /dev/null +++ b/utils/helm/speckle-server/templates/services.yml @@ -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 diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml new file mode 100644 index 000000000..05b542039 --- /dev/null +++ b/utils/helm/speckle-server/values.yaml @@ -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