Merge branch 'release-1.3.0'
commit
c7033a79ec
@ -0,0 +1,41 @@
|
|||||||
|
name: Linter
|
||||||
|
on: pull_request
|
||||||
|
jobs:
|
||||||
|
Bandit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run checks
|
||||||
|
run: |
|
||||||
|
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
|
||||||
|
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
|
||||||
|
for files in $PR_FILES; do
|
||||||
|
extension="${files##*.}"
|
||||||
|
if [[ $extension == 'py' ]]; then
|
||||||
|
changed_files_bandit+=" ${files}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -z ${changed_files_bandit} ]]; then
|
||||||
|
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
|
||||||
|
python3 -m venv .env
|
||||||
|
. .env/bin/activate
|
||||||
|
pip install -U pip wheel setuptools
|
||||||
|
pip install bandit
|
||||||
|
mkdir -p bandit_report
|
||||||
|
|
||||||
|
echo "Bandit version: "`bandit --version | head -1`
|
||||||
|
echo "The files will be checked: "`echo ${changed_files_bandit}`
|
||||||
|
bandit ${changed_files_bandit} --exclude '**/tests/**' -a file --ini ./.bandit -f html -o ./bandit_report/bandit_checks.html
|
||||||
|
deactivate
|
||||||
|
else
|
||||||
|
echo "No files with the \"py\" extension found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: bandit_report
|
||||||
|
path: bandit_report
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
name: Linter
|
||||||
|
on: pull_request
|
||||||
|
jobs:
|
||||||
|
ESLint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 12
|
||||||
|
|
||||||
|
- name: Run checks
|
||||||
|
run: |
|
||||||
|
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
|
||||||
|
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
|
||||||
|
for files in $PR_FILES; do
|
||||||
|
extension="${files##*.}"
|
||||||
|
if [[ $extension == 'js' || $extension == 'ts' || $extension == 'jsx' || $extension == 'tsx' ]]; then
|
||||||
|
changed_files_eslint+=" ${files}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -z ${changed_files_eslint} ]]; then
|
||||||
|
for package_files in `find -maxdepth 2 -name "package.json" -type f`; do
|
||||||
|
cd $(dirname $package_files) && npm ci && cd ${{ github.workspace }}
|
||||||
|
done
|
||||||
|
npm install eslint-detailed-reporter --save-dev
|
||||||
|
mkdir -p eslint_report
|
||||||
|
|
||||||
|
echo "ESLint version: "`npx eslint --version`
|
||||||
|
echo "The files will be checked: "`echo ${changed_files_eslint}`
|
||||||
|
npx eslint ${changed_files_eslint} -f node_modules/eslint-detailed-reporter/lib/detailed.js -o ./eslint_report/eslint_checks.html
|
||||||
|
else
|
||||||
|
echo "No files with the \"js|ts|jsx|tsx\" extension found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: eslint_report
|
||||||
|
path: eslint_report
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'develop'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 12
|
||||||
|
- name: Build CVAT
|
||||||
|
env:
|
||||||
|
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||||
|
CONTAINER_COVERAGE_DATA_DIR: '/coverage_data'
|
||||||
|
DJANGO_SU_NAME: 'admin'
|
||||||
|
DJANGO_SU_EMAIL: 'admin@localhost.company'
|
||||||
|
DJANGO_SU_PASSWORD: '12qwaszx'
|
||||||
|
run: |
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml build
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm ci && cd ../cvat-core && npm ci && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
|
||||||
|
docker-compose up -d
|
||||||
|
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||||
|
- name: Code instrumentation
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run coverage
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||||
|
- name: End-to-end testing
|
||||||
|
run: |
|
||||||
|
cd ./tests
|
||||||
|
npm ci
|
||||||
|
npx cypress run --headless --browser chrome
|
||||||
|
- name: Uploading cypress screenshots as an artifact
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: cypress_screenshots
|
||||||
|
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||||
|
- name: Collect coverage data
|
||||||
|
env:
|
||||||
|
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||||
|
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
|
||||||
|
COVERALLS_SERVICE_NAME: github
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mv ./tests/.nyc_output ./
|
||||||
|
npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json'
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
name: Publish Docker images
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_to_registry:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 12
|
||||||
|
|
||||||
|
- name: Build images
|
||||||
|
run: |
|
||||||
|
CLAM_AV=yes INSTALL_SOURCES=yes docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml build
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
env:
|
||||||
|
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||||
|
CONTAINER_COVERAGE_DATA_DIR: '/coverage_data'
|
||||||
|
DJANGO_SU_NAME: 'admin'
|
||||||
|
DJANGO_SU_EMAIL: 'admin@localhost.company'
|
||||||
|
DJANGO_SU_PASSWORD: '12qwaszx'
|
||||||
|
run: |
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli'
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm ci && cd ../cvat-core && npm ci && npm run test'
|
||||||
|
docker-compose up -d
|
||||||
|
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||||
|
|
||||||
|
- name: Run end-to-end tests
|
||||||
|
run: |
|
||||||
|
cd ./tests
|
||||||
|
npm ci
|
||||||
|
npm run cypress:run:chrome
|
||||||
|
- name: Uploading cypress screenshots as an artifact
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: cypress_screenshots
|
||||||
|
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Push to Docker Hub
|
||||||
|
env:
|
||||||
|
DOCKERHUB_WORKSPACE: 'openvino'
|
||||||
|
SERVER_IMAGE_REPO: 'cvat_server'
|
||||||
|
UI_IMAGE_REPO: 'cvat_ui'
|
||||||
|
run: |
|
||||||
|
docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||||
|
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||||
|
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest"
|
||||||
|
|
||||||
|
docker tag "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||||
|
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||||
|
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest"
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
name: Linter
|
||||||
|
on: pull_request
|
||||||
|
jobs:
|
||||||
|
PyLint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run checks
|
||||||
|
run: |
|
||||||
|
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
|
||||||
|
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
|
||||||
|
for files in $PR_FILES; do
|
||||||
|
extension="${files##*.}"
|
||||||
|
if [[ $extension == 'py' ]]; then
|
||||||
|
changed_files_pylint+=" ${files}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -z ${changed_files_pylint} ]]; then
|
||||||
|
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
|
||||||
|
python3 -m venv .env
|
||||||
|
. .env/bin/activate
|
||||||
|
pip install -U pip wheel setuptools
|
||||||
|
pip install pylint-json2html
|
||||||
|
pip install $(egrep "pylint.*" ./cvat/requirements/development.txt)
|
||||||
|
pip install $(egrep "Django.*" ./cvat/requirements/base.txt)
|
||||||
|
mkdir -p pylint_report
|
||||||
|
|
||||||
|
echo "Pylint version: "`pylint --version | head -1`
|
||||||
|
echo "The files will be checked: "`echo ${changed_files_pylint}`
|
||||||
|
pylint ${changed_files_pylint} --output-format=json > ./pylint_report/pylint_checks.json || exit_code=`echo $?` || true
|
||||||
|
pylint-json2html -o ./pylint_report/pylint_checks.html ./pylint_report/pylint_checks.json
|
||||||
|
deactivate
|
||||||
|
exit ${exit_code}
|
||||||
|
else
|
||||||
|
echo "No files with the \"py\" extension found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: pylint_report
|
||||||
|
path: pylint_report
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
name: CI-nightly
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 22 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 12
|
||||||
|
- name: Build CVAT
|
||||||
|
env:
|
||||||
|
DJANGO_SU_NAME: "admin"
|
||||||
|
DJANGO_SU_EMAIL: "admin@localhost.company"
|
||||||
|
DJANGO_SU_PASSWORD: "12qwaszx"
|
||||||
|
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
|
||||||
|
run: |
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml up -d --build
|
||||||
|
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
|
||||||
|
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||||
|
- name: End-to-end testing
|
||||||
|
run: |
|
||||||
|
cd ./tests
|
||||||
|
npm ci
|
||||||
|
npm run cypress:run:firefox
|
||||||
|
- name: Uploading cypress screenshots as an artifact
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: cypress_screenshots
|
||||||
|
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||||
@ -1,57 +0,0 @@
|
|||||||
language: generic
|
|
||||||
dist: focal
|
|
||||||
|
|
||||||
cache:
|
|
||||||
npm: true
|
|
||||||
directories:
|
|
||||||
- ~/.cache
|
|
||||||
|
|
||||||
addons:
|
|
||||||
firefox: 'latest'
|
|
||||||
chrome: stable
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- libgconf-2-4
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
env:
|
|
||||||
- CONTAINER_COVERAGE_DATA_DIR="/coverage_data"
|
|
||||||
HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}"
|
|
||||||
DJANGO_SU_NAME="admin"
|
|
||||||
DJANGO_SU_EMAIL="admin@localhost.company"
|
|
||||||
DJANGO_SU_PASSWORD="12qwaszx"
|
|
||||||
NODE_VERSION="12"
|
|
||||||
API_ABOUT_PAGE="localhost:8080/api/v1/server/about"
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- nvm install ${NODE_VERSION}
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- chmod a+rwx ${HOST_COVERAGE_DATA_DIR}
|
|
||||||
|
|
||||||
script:
|
|
||||||
- if [[ $TRAVIS_EVENT_TYPE == "cron" && $TRAVIS_BRANCH == "develop" ]];
|
|
||||||
then
|
|
||||||
docker-compose -f docker-compose.yml -f ./tests/docker-compose.email.yml up -d --build;
|
|
||||||
bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done';
|
|
||||||
docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell";
|
|
||||||
cd ./tests && npm install && npm run cypress:run:firefox; exit $?;
|
|
||||||
fi;
|
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
|
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
|
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
|
|
||||||
- docker-compose up -d
|
|
||||||
- docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
|
||||||
# End-to-end testing
|
|
||||||
- npm install && npm run coverage
|
|
||||||
- docker-compose up -d --build
|
|
||||||
- cd ./tests && npm install && npx cypress run --headless --browser chrome
|
|
||||||
- mv ./.nyc_output ../ && cd ..
|
|
||||||
- npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
|
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html
|
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json'
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
queue.type: persisted
|
||||||
|
queue.max_bytes: 1gb
|
||||||
|
queue.checkpoint.writes: 20
|
||||||
@ -1,2 +1 @@
|
|||||||
src/*.js
|
|
||||||
dist
|
dist
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 303 KiB |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
webpack.config.js
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
ecmaVersion: 6,
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint', 'import'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'airbnb-typescript/base',
|
||||||
|
'plugin:import/errors',
|
||||||
|
'plugin:import/warnings',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
|
'@typescript-eslint/indent': ['warn', 4],
|
||||||
|
'no-plusplus': 0,
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
selector: 'ForOfStatement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'max-len': ['error', { code: 120 }],
|
||||||
|
'no-continue': 0,
|
||||||
|
'func-names': 0,
|
||||||
|
'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
|
||||||
|
'lines-between-class-members': 0,
|
||||||
|
'import/prefer-default-export': 0, // works incorrect with interfaces
|
||||||
|
'newline-per-chained-call': 0, // makes code uglier
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
extensions: ['.ts', '.js', '.json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# Module CVAT-CANVAS-3D
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The CVAT module written in TypeScript language.
|
||||||
|
It presents a canvas to viewing, drawing and editing of 3D annotations.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
If you make changes in this package, please do following:
|
||||||
|
|
||||||
|
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
|
||||||
|
- After changing API (backward compatible new features) do: `npm version minor`
|
||||||
|
- After changing API (changes that break backward compatibility) do: `npm version major`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- Building of the module from sources in the `dist` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run build -- --mode=development # without a minification
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Methods
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Canvas3d {
|
||||||
|
html(): HTMLDivElement;
|
||||||
|
setup(frameData: any): void;
|
||||||
|
mode(): Mode;
|
||||||
|
isAbleToChangeFrame(): boolean;
|
||||||
|
render(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WEB
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create an instance of a canvas
|
||||||
|
const canvas = new window.canvas.Canvas3d();
|
||||||
|
|
||||||
|
console.log('Version ', window.canvas.CanvasVersion);
|
||||||
|
console.log('Current mode is ', window.canvas.mode());
|
||||||
|
|
||||||
|
// Put canvas to a html container
|
||||||
|
htmlContainer.appendChild(canvas.html());
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "cvat-canvas3d",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Part of Computer Vision Annotation Tool which presents its canvas3D library",
|
||||||
|
"main": "src/canvas3d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && webpack --config ./webpack.config.js",
|
||||||
|
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
|
||||||
|
},
|
||||||
|
"author": "Intel",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.5.5",
|
||||||
|
"@babel/core": "^7.5.5",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
|
||||||
|
"@babel/preset-env": "^7.5.5",
|
||||||
|
"@babel/preset-typescript": "^7.3.3",
|
||||||
|
"@types/node": "^12.6.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||||
|
"@typescript-eslint/parser": "^1.13.0",
|
||||||
|
"babel-loader": "^8.0.6",
|
||||||
|
"css-loader": "^3.4.2",
|
||||||
|
"dts-bundle-webpack": "^1.0.2",
|
||||||
|
"eslint": "^6.1.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^4.0.1",
|
||||||
|
"eslint-config-typescript-recommended": "^1.4.17",
|
||||||
|
"eslint-plugin-import": "^2.18.2",
|
||||||
|
"node-sass": "^4.14.1",
|
||||||
|
"nodemon": "^1.19.4",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"postcss-preset-env": "^6.7.0",
|
||||||
|
"sass-loader": "^8.0.2",
|
||||||
|
"style-loader": "^1.0.0",
|
||||||
|
"typescript": "^3.5.3",
|
||||||
|
"webpack": "^4.44.2",
|
||||||
|
"webpack-cli": "^3.3.6",
|
||||||
|
"webpack-dev-server": "^3.11.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/three": "^0.125.3",
|
||||||
|
"camera-controls": "^1.25.3",
|
||||||
|
"three": "^0.125.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parser: false,
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-env': {
|
||||||
|
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pjson from '../../package.json';
|
||||||
|
import { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController';
|
||||||
|
import {
|
||||||
|
Canvas3dModel, Canvas3dModelImpl, Mode, DrawData, ViewType, MouseInteraction,
|
||||||
|
} from './canvas3dModel';
|
||||||
|
import {
|
||||||
|
Canvas3dView, Canvas3dViewImpl, ViewsDOM, CAMERA_ACTION,
|
||||||
|
} from './canvas3dView';
|
||||||
|
import { Master } from './master';
|
||||||
|
|
||||||
|
const Canvas3dVersion = pjson.version;
|
||||||
|
|
||||||
|
interface Canvas3d {
|
||||||
|
html(): ViewsDOM;
|
||||||
|
setup(frameData: any): void;
|
||||||
|
isAbleToChangeFrame(): boolean;
|
||||||
|
mode(): Mode;
|
||||||
|
render(): void;
|
||||||
|
keyControls(keys: KeyboardEvent): void;
|
||||||
|
mouseControls(type: string, event: MouseEvent): void;
|
||||||
|
draw(drawData: DrawData): void;
|
||||||
|
cancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Canvas3dImpl implements Canvas3d {
|
||||||
|
private model: Canvas3dModel & Master;
|
||||||
|
private controller: Canvas3dController;
|
||||||
|
private view: Canvas3dView;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.model = new Canvas3dModelImpl();
|
||||||
|
this.controller = new Canvas3dControllerImpl(this.model);
|
||||||
|
this.view = new Canvas3dViewImpl(this.model, this.controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public html(): ViewsDOM {
|
||||||
|
return this.view.html();
|
||||||
|
}
|
||||||
|
|
||||||
|
public keyControls(keys: KeyboardEvent): void {
|
||||||
|
this.view.keyControls(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
|
||||||
|
this.view.mouseControls(type, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
this.view.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(drawData: DrawData): void {
|
||||||
|
this.model.draw(drawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setup(frameData: any): void {
|
||||||
|
this.model.setup(frameData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mode(): Mode {
|
||||||
|
return this.model.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAbleToChangeFrame(): boolean {
|
||||||
|
return this.model.isAbleToChangeFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel(): void {
|
||||||
|
this.model.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CAMERA_ACTION,
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { Canvas3dModel, Mode, DrawData } from './canvas3dModel';
|
||||||
|
|
||||||
|
export interface Canvas3dController {
|
||||||
|
readonly drawData: DrawData;
|
||||||
|
mode: Mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Canvas3dControllerImpl implements Canvas3dController {
|
||||||
|
private model: Canvas3dModel;
|
||||||
|
|
||||||
|
public constructor(model: Canvas3dModel) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set mode(value: Mode) {
|
||||||
|
this.model.mode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mode(): Mode {
|
||||||
|
return this.model.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get drawData(): DrawData {
|
||||||
|
return this.model.data.drawData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { MasterImpl } from './master';
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
renderWidth: number;
|
||||||
|
renderHeight: number;
|
||||||
|
imageData: ImageData | CanvasImageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrawData {
|
||||||
|
enabled: boolean;
|
||||||
|
initialState?: any;
|
||||||
|
redraw?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FrameZoom {
|
||||||
|
MIN = 0.1,
|
||||||
|
MAX = 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ViewType {
|
||||||
|
PERSPECTIVE = 'perspective',
|
||||||
|
TOP = 'top',
|
||||||
|
SIDE = 'side',
|
||||||
|
FRONT = 'front',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MouseInteraction {
|
||||||
|
CLICK = 'click',
|
||||||
|
DOUBLE_CLICK = 'dblclick',
|
||||||
|
HOVER = 'hover',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UpdateReasons {
|
||||||
|
IMAGE_CHANGED = 'image_changed',
|
||||||
|
OBJECTS_UPDATED = 'objects_updated',
|
||||||
|
FITTED_CANVAS = 'fitted_canvas',
|
||||||
|
DRAW = 'draw',
|
||||||
|
SELECT = 'select',
|
||||||
|
CANCEL = 'cancel',
|
||||||
|
DATA_FAILED = 'data_failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Mode {
|
||||||
|
IDLE = 'idle',
|
||||||
|
DRAG = 'drag',
|
||||||
|
RESIZE = 'resize',
|
||||||
|
DRAW = 'draw',
|
||||||
|
EDIT = 'edit',
|
||||||
|
INTERACT = 'interact',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Canvas3dDataModel {
|
||||||
|
canvasSize: Size;
|
||||||
|
image: Image | null;
|
||||||
|
imageID: number | null;
|
||||||
|
imageOffset: number;
|
||||||
|
imageSize: Size;
|
||||||
|
drawData: DrawData;
|
||||||
|
mode: Mode;
|
||||||
|
exception: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Canvas3dModel {
|
||||||
|
mode: Mode;
|
||||||
|
data: Canvas3dDataModel;
|
||||||
|
setup(frameData: any): void;
|
||||||
|
isAbleToChangeFrame(): boolean;
|
||||||
|
draw(drawData: DrawData): void;
|
||||||
|
cancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
|
||||||
|
public data: Canvas3dDataModel;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
this.data = {
|
||||||
|
canvasSize: {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
image: null,
|
||||||
|
imageID: null,
|
||||||
|
imageOffset: 0,
|
||||||
|
imageSize: {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
drawData: {
|
||||||
|
enabled: false,
|
||||||
|
initialState: null,
|
||||||
|
},
|
||||||
|
mode: Mode.IDLE,
|
||||||
|
exception: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setup(frameData: any): void {
|
||||||
|
if (this.data.imageID !== frameData.number) {
|
||||||
|
this.data.imageID = frameData.number;
|
||||||
|
frameData
|
||||||
|
.data((): void => {
|
||||||
|
this.data.image = null;
|
||||||
|
this.notify(UpdateReasons.IMAGE_CHANGED);
|
||||||
|
})
|
||||||
|
.then((data: Image): void => {
|
||||||
|
if (frameData.number !== this.data.imageID) {
|
||||||
|
// already another image
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.imageSize = {
|
||||||
|
height: frameData.height as number,
|
||||||
|
width: frameData.width as number,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.data.image = data;
|
||||||
|
this.notify(UpdateReasons.IMAGE_CHANGED);
|
||||||
|
})
|
||||||
|
.catch((exception: any): void => {
|
||||||
|
this.data.exception = exception;
|
||||||
|
this.notify(UpdateReasons.DATA_FAILED);
|
||||||
|
throw exception;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public set mode(value: Mode) {
|
||||||
|
this.data.mode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mode(): Mode {
|
||||||
|
return this.data.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAbleToChangeFrame(): boolean {
|
||||||
|
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode)
|
||||||
|
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
|
||||||
|
|
||||||
|
return !isUnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(drawData: DrawData): void {
|
||||||
|
if (drawData.enabled && this.data.drawData.enabled) {
|
||||||
|
throw new Error('Drawing has been already started');
|
||||||
|
}
|
||||||
|
this.data.drawData.enabled = drawData.enabled;
|
||||||
|
this.data.mode = Mode.DRAW;
|
||||||
|
|
||||||
|
this.notify(UpdateReasons.DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel(): void {
|
||||||
|
this.notify(UpdateReasons.CANCEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,437 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader';
|
||||||
|
import CameraControls from 'camera-controls';
|
||||||
|
import { Canvas3dController } from './canvas3dController';
|
||||||
|
import { Listener, Master } from './master';
|
||||||
|
import CONST from './consts';
|
||||||
|
import {
|
||||||
|
Canvas3dModel, UpdateReasons, Mode, DrawData, ViewType, MouseInteraction,
|
||||||
|
} from './canvas3dModel';
|
||||||
|
import { CuboidModel } from './cuboid';
|
||||||
|
|
||||||
|
export interface Canvas3dView {
|
||||||
|
html(): ViewsDOM;
|
||||||
|
render(): void;
|
||||||
|
keyControls(keys: KeyboardEvent): void;
|
||||||
|
mouseControls(type: MouseInteraction, event: MouseEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CAMERA_ACTION {
|
||||||
|
ZOOM_IN = 'KeyI',
|
||||||
|
MOVE_UP = 'KeyU',
|
||||||
|
MOVE_DOWN = 'KeyO',
|
||||||
|
MOVE_LEFT = 'KeyJ',
|
||||||
|
ZOOM_OUT = 'KeyK',
|
||||||
|
MOVE_RIGHT = 'KeyL',
|
||||||
|
TILT_UP = 'ArrowUp',
|
||||||
|
TILT_DOWN = 'ArrowDown',
|
||||||
|
ROTATE_RIGHT = 'ArrowRight',
|
||||||
|
ROTATE_LEFT = 'ArrowLeft',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RayCast {
|
||||||
|
renderer: THREE.Raycaster;
|
||||||
|
mouseVector: THREE.Vector2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Views {
|
||||||
|
perspective: RenderView;
|
||||||
|
top: RenderView;
|
||||||
|
side: RenderView;
|
||||||
|
front: RenderView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CubeObject {
|
||||||
|
perspective: THREE.Mesh;
|
||||||
|
top: THREE.Mesh;
|
||||||
|
side: THREE.Mesh;
|
||||||
|
front: THREE.Mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderView {
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
camera?: THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
||||||
|
controls?: CameraControls;
|
||||||
|
rayCaster?: RayCast;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewsDOM {
|
||||||
|
perspective: HTMLCanvasElement;
|
||||||
|
top: HTMLCanvasElement;
|
||||||
|
side: HTMLCanvasElement;
|
||||||
|
front: HTMLCanvasElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Canvas3dViewImpl implements Canvas3dView, Listener {
|
||||||
|
private controller: Canvas3dController;
|
||||||
|
private views: Views;
|
||||||
|
private clock: THREE.Clock;
|
||||||
|
private speed: number;
|
||||||
|
private cube: CuboidModel;
|
||||||
|
private highlighted: boolean;
|
||||||
|
private selected: CubeObject;
|
||||||
|
|
||||||
|
private set mode(value: Mode) {
|
||||||
|
this.controller.mode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get mode(): Mode {
|
||||||
|
return this.controller.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(model: Canvas3dModel & Master, controller: Canvas3dController) {
|
||||||
|
this.controller = controller;
|
||||||
|
this.clock = new THREE.Clock();
|
||||||
|
this.speed = CONST.MOVEMENT_FACTOR;
|
||||||
|
this.cube = new CuboidModel();
|
||||||
|
this.highlighted = false;
|
||||||
|
this.selected = this.cube;
|
||||||
|
|
||||||
|
this.views = {
|
||||||
|
perspective: {
|
||||||
|
renderer: new THREE.WebGLRenderer({ antialias: true }),
|
||||||
|
scene: new THREE.Scene(),
|
||||||
|
rayCaster: {
|
||||||
|
renderer: new THREE.Raycaster(),
|
||||||
|
mouseVector: new THREE.Vector2(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
renderer: new THREE.WebGLRenderer({ antialias: true }),
|
||||||
|
scene: new THREE.Scene(),
|
||||||
|
},
|
||||||
|
side: {
|
||||||
|
renderer: new THREE.WebGLRenderer({ antialias: true }),
|
||||||
|
scene: new THREE.Scene(),
|
||||||
|
},
|
||||||
|
front: {
|
||||||
|
renderer: new THREE.WebGLRenderer({ antialias: true }),
|
||||||
|
scene: new THREE.Scene(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
CameraControls.install({ THREE });
|
||||||
|
|
||||||
|
this.mode = Mode.IDLE;
|
||||||
|
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
this.views[view as keyof Views].scene.background = new THREE.Color(0x000000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewSize = CONST.ZOOM_FACTOR;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||||
|
|
||||||
|
// setting up the camera and adding it in the scene
|
||||||
|
this.views.perspective.camera = new THREE.PerspectiveCamera(50, aspectRatio, 1, 500);
|
||||||
|
this.views.perspective.camera.position.set(-15, 0, 4);
|
||||||
|
this.views.perspective.camera.up.set(0, 0, 1);
|
||||||
|
this.views.perspective.camera.lookAt(10, 0, 0);
|
||||||
|
|
||||||
|
this.views.top.camera = new THREE.OrthographicCamera(
|
||||||
|
(-aspectRatio * viewSize) / 2 - 2,
|
||||||
|
(aspectRatio * viewSize) / 2 + 2,
|
||||||
|
viewSize / 2 + 2,
|
||||||
|
-viewSize / 2 - 2,
|
||||||
|
-10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.views.side.camera = new THREE.OrthographicCamera(
|
||||||
|
(-aspectRatio * viewSize) / 2,
|
||||||
|
(aspectRatio * viewSize) / 2,
|
||||||
|
viewSize / 2,
|
||||||
|
-viewSize / 2,
|
||||||
|
-10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
this.views.side.camera.position.set(0, 5, 0);
|
||||||
|
this.views.side.camera.lookAt(0, 0, 0);
|
||||||
|
this.views.side.camera.up.set(0, 0, 1);
|
||||||
|
|
||||||
|
this.views.front.camera = new THREE.OrthographicCamera(
|
||||||
|
(-aspectRatio * viewSize) / 2,
|
||||||
|
(aspectRatio * viewSize) / 2,
|
||||||
|
viewSize / 2,
|
||||||
|
-viewSize / 2,
|
||||||
|
-10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
this.views.front.camera.position.set(-7, 0, 0);
|
||||||
|
this.views.front.camera.up.set(0, 0, 1);
|
||||||
|
this.views.front.camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
const viewType = this.views[view as keyof Views];
|
||||||
|
viewType.renderer.setSize(width, height);
|
||||||
|
if (view !== ViewType.PERSPECTIVE) {
|
||||||
|
viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement);
|
||||||
|
viewType.controls.mouseButtons.left = CameraControls.ACTION.NONE;
|
||||||
|
viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE;
|
||||||
|
} else {
|
||||||
|
viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement);
|
||||||
|
}
|
||||||
|
viewType.controls.minDistance = CONST.MIN_DISTANCE;
|
||||||
|
viewType.controls.maxDistance = CONST.MAX_DISTANCE;
|
||||||
|
});
|
||||||
|
|
||||||
|
model.subscribe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public notify(model: Canvas3dModel & Master, reason: UpdateReasons): void {
|
||||||
|
if (reason === UpdateReasons.IMAGE_CHANGED) {
|
||||||
|
const loader = new PCDLoader();
|
||||||
|
this.clearScene();
|
||||||
|
const objectURL = URL.createObjectURL(model.data.image.imageData);
|
||||||
|
loader.load(objectURL, this.addScene.bind(this));
|
||||||
|
URL.revokeObjectURL(objectURL);
|
||||||
|
const event: CustomEvent = new CustomEvent('canvas.setup');
|
||||||
|
this.views.perspective.renderer.domElement.dispatchEvent(event);
|
||||||
|
} else if (reason === UpdateReasons.DRAW) {
|
||||||
|
const data: DrawData = this.controller.drawData;
|
||||||
|
if (data.enabled && this.mode === Mode.IDLE) {
|
||||||
|
this.mode = Mode.DRAW;
|
||||||
|
this.cube = new CuboidModel();
|
||||||
|
} else if (this.mode !== Mode.IDLE) {
|
||||||
|
this.cube = new CuboidModel();
|
||||||
|
}
|
||||||
|
} else if (reason === UpdateReasons.CANCEL) {
|
||||||
|
if (this.mode === Mode.DRAW) {
|
||||||
|
this.controller.drawData.enabled = false;
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
this.views[view as keyof Views].scene.children[0].remove(this.cube[view as keyof Views]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.mode = Mode.IDLE;
|
||||||
|
const event: CustomEvent = new CustomEvent('canvas.canceled');
|
||||||
|
this.views.perspective.renderer.domElement.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearScene(): void {
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
this.views[view as keyof Views].scene.children = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addScene(points: any): void {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
points.material.size = 0.08;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
points.material.color = new THREE.Color(0x0000ff);
|
||||||
|
const sphereCenter = points.geometry.boundingSphere.center;
|
||||||
|
const { radius } = points.geometry.boundingSphere;
|
||||||
|
const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x
|
||||||
|
&& radius / 2 > this.views.perspective.camera.position.x - sphereCenter.x;
|
||||||
|
const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y
|
||||||
|
&& radius / 2 > this.views.perspective.camera.position.y - sphereCenter.y;
|
||||||
|
const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z
|
||||||
|
&& radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z;
|
||||||
|
let newX = 0;
|
||||||
|
let newY = 0;
|
||||||
|
let newZ = 0;
|
||||||
|
if (!xRange) {
|
||||||
|
newX = sphereCenter.x;
|
||||||
|
}
|
||||||
|
if (!yRange) {
|
||||||
|
newY = sphereCenter.y;
|
||||||
|
}
|
||||||
|
if (!zRange) {
|
||||||
|
newZ = sphereCenter.z;
|
||||||
|
}
|
||||||
|
if (newX || newY || newZ) {
|
||||||
|
this.positionAllViews(newX, newY, newZ);
|
||||||
|
}
|
||||||
|
this.views.perspective.scene.add(points);
|
||||||
|
this.views.top.scene.add(points.clone());
|
||||||
|
this.views.side.scene.add(points.clone());
|
||||||
|
this.views.front.scene.add(points.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
private positionAllViews(x: number, y: number, z: number): void {
|
||||||
|
this.views.perspective.controls.setLookAt(x - 8, y - 8, z + 3, x, y, z, false);
|
||||||
|
this.views.top.controls.setLookAt(x, y, z + 8, x, y, z, false);
|
||||||
|
this.views.side.controls.setLookAt(x, y + 8, z, x, y, z, false);
|
||||||
|
this.views.front.controls.setLookAt(x + 8, y, z, x, y, z, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static resizeRendererToDisplaySize(viewName: string, view: RenderView): void {
|
||||||
|
const { camera, renderer } = view;
|
||||||
|
const canvas = renderer.domElement;
|
||||||
|
const width = canvas.parentElement.clientWidth;
|
||||||
|
const height = canvas.parentElement.clientHeight;
|
||||||
|
const needResize = canvas.clientWidth !== width || canvas.clientHeight !== height;
|
||||||
|
if (needResize) {
|
||||||
|
if (camera instanceof THREE.PerspectiveCamera) {
|
||||||
|
camera.aspect = width / height;
|
||||||
|
} else {
|
||||||
|
const topViewFactor = 0; // viewName === ViewType.TOP ? 2 : 0;
|
||||||
|
const viewSize = CONST.ZOOM_FACTOR;
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
if (!(camera instanceof THREE.PerspectiveCamera)) {
|
||||||
|
camera.left = (-aspectRatio * viewSize) / 2 - topViewFactor;
|
||||||
|
camera.right = (aspectRatio * viewSize) / 2 + topViewFactor;
|
||||||
|
camera.top = viewSize / 2 + topViewFactor;
|
||||||
|
camera.bottom = -viewSize / 2 - topViewFactor;
|
||||||
|
}
|
||||||
|
camera.near = -10;
|
||||||
|
camera.far = 10;
|
||||||
|
}
|
||||||
|
view.renderer.setSize(width, height);
|
||||||
|
view.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRayCaster = (viewType: RenderView): void => {
|
||||||
|
viewType.rayCaster.renderer.setFromCamera(viewType.rayCaster.mouseVector, viewType.camera);
|
||||||
|
if (this.mode === Mode.DRAW) {
|
||||||
|
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
|
||||||
|
this.views.perspective.scene.children,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
this.views.perspective.scene.children[0].add(this.cube.perspective);
|
||||||
|
const newPoints = intersects[0].point;
|
||||||
|
this.cube.perspective.position.copy(newPoints);
|
||||||
|
}
|
||||||
|
} else if (this.mode === Mode.IDLE) {
|
||||||
|
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
|
||||||
|
this.views.perspective.scene.children[0].children,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (intersects.length !== 0) {
|
||||||
|
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
|
||||||
|
if (this.selected.perspective !== sceneItem) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
sceneItem.material.color = new THREE.Color(0xff0000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectedObject = intersects[0].object as THREE.Mesh;
|
||||||
|
if (this.selected.perspective !== selectedObject) {
|
||||||
|
selectedObject.material.color = new THREE.Color(0xffff00);
|
||||||
|
this.highlighted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.highlighted) {
|
||||||
|
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
|
||||||
|
if (this.selected.perspective !== sceneItem) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
sceneItem.material.color = new THREE.Color(0xff0000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.highlighted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
const viewType = this.views[view as keyof Views];
|
||||||
|
Canvas3dViewImpl.resizeRendererToDisplaySize(view, viewType);
|
||||||
|
viewType.controls.update(this.clock.getDelta());
|
||||||
|
viewType.renderer.render(viewType.scene, viewType.camera);
|
||||||
|
if (view === ViewType.PERSPECTIVE && viewType.scene.children.length !== 0) {
|
||||||
|
this.renderRayCaster(viewType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public keyControls(key: any): void {
|
||||||
|
const { controls } = this.views.perspective;
|
||||||
|
switch (key.code) {
|
||||||
|
case CAMERA_ACTION.ROTATE_RIGHT:
|
||||||
|
controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.ROTATE_LEFT:
|
||||||
|
controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.TILT_UP:
|
||||||
|
controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.TILT_DOWN:
|
||||||
|
controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (key.altKey === true) {
|
||||||
|
switch (key.code) {
|
||||||
|
case CAMERA_ACTION.ZOOM_IN:
|
||||||
|
controls.dolly(CONST.DOLLY_FACTOR, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.ZOOM_OUT:
|
||||||
|
controls.dolly(-CONST.DOLLY_FACTOR, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.MOVE_LEFT:
|
||||||
|
controls.truck(-0.01 * this.speed, 0, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.MOVE_RIGHT:
|
||||||
|
controls.truck(0.01 * this.speed, 0, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.MOVE_DOWN:
|
||||||
|
controls.truck(0, -0.01 * this.speed, true);
|
||||||
|
break;
|
||||||
|
case CAMERA_ACTION.MOVE_UP:
|
||||||
|
controls.truck(0, 0.01 * this.speed, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
if (type === MouseInteraction.DOUBLE_CLICK && this.mode === Mode.DRAW) {
|
||||||
|
this.controller.drawData.enabled = false;
|
||||||
|
this.mode = Mode.IDLE;
|
||||||
|
const cancelEvent: CustomEvent = new CustomEvent('canvas.canceled');
|
||||||
|
this.views.perspective.renderer.domElement.dispatchEvent(cancelEvent);
|
||||||
|
} else {
|
||||||
|
const canvas = this.views.perspective.renderer.domElement;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const { mouseVector } = this.views.perspective.rayCaster;
|
||||||
|
mouseVector.x = ((event.clientX - (canvas.offsetLeft + rect.left)) / canvas.clientWidth) * 2 - 1;
|
||||||
|
mouseVector.y = -((event.clientY - (canvas.offsetTop + rect.top)) / canvas.clientHeight) * 2 + 1;
|
||||||
|
|
||||||
|
if (type === MouseInteraction.CLICK && this.mode === Mode.IDLE) {
|
||||||
|
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
|
||||||
|
this.views.perspective.scene.children[0].children,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (intersects.length !== 0) {
|
||||||
|
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
sceneItem.material.color = new THREE.Color(0xff0000);
|
||||||
|
});
|
||||||
|
const selectedObject = intersects[0].object;
|
||||||
|
selectedObject.material.color = new THREE.Color(0x00ffff);
|
||||||
|
Object.keys(this.views).forEach((view: string): void => {
|
||||||
|
if (view !== ViewType.PERSPECTIVE) {
|
||||||
|
this.views[view as keyof Views].scene.children[0].children = [selectedObject.clone()];
|
||||||
|
this.views[view as keyof Views].controls.fitToBox(selectedObject, false);
|
||||||
|
this.views[view as keyof Views].controls.zoom(view === ViewType.TOP ? -5 : -5, false);
|
||||||
|
}
|
||||||
|
this.views[view as keyof Views].scene.background = new THREE.Color(0x000000);
|
||||||
|
});
|
||||||
|
this.selected.perspective = selectedObject as THREE.Mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public html(): ViewsDOM {
|
||||||
|
return {
|
||||||
|
perspective: this.views.perspective.renderer.domElement,
|
||||||
|
top: this.views.top.renderer.domElement,
|
||||||
|
side: this.views.side.renderer.domElement,
|
||||||
|
front: this.views.front.renderer.domElement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
const BASE_GRID_WIDTH = 2;
|
||||||
|
const MOVEMENT_FACTOR = 200;
|
||||||
|
const DOLLY_FACTOR = 5;
|
||||||
|
const MAX_DISTANCE = 100;
|
||||||
|
const MIN_DISTANCE = 0;
|
||||||
|
const ZOOM_FACTOR = 7;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BASE_GRID_WIDTH,
|
||||||
|
MOVEMENT_FACTOR,
|
||||||
|
DOLLY_FACTOR,
|
||||||
|
MAX_DISTANCE,
|
||||||
|
MIN_DISTANCE,
|
||||||
|
ZOOM_FACTOR,
|
||||||
|
};
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export class CuboidModel {
|
||||||
|
public perspective: THREE.Mesh;
|
||||||
|
public top: THREE.Mesh;
|
||||||
|
public side: THREE.Mesh;
|
||||||
|
public front: THREE.Mesh;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||||
|
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
|
||||||
|
this.perspective = new THREE.Mesh(geometry, material);
|
||||||
|
this.top = new THREE.Mesh(geometry, material);
|
||||||
|
this.side = new THREE.Mesh(geometry, material);
|
||||||
|
this.front = new THREE.Mesh(geometry, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export interface Master {
|
||||||
|
subscribe(listener: Listener): void;
|
||||||
|
unsubscribe(listener: Listener): void;
|
||||||
|
unsubscribeAll(): void;
|
||||||
|
notify(reason: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Listener {
|
||||||
|
notify(master: Master, reason: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MasterImpl implements Master {
|
||||||
|
private listeners: Listener[];
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(listener: Listener): void {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe(listener: Listener): void {
|
||||||
|
for (let i = 0; i < this.listeners.length; i++) {
|
||||||
|
if (this.listeners[i] === listener) {
|
||||||
|
this.listeners.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeAll(): void {
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public notify(reason: string): void {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener.notify(this, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"module": "es6",
|
||||||
|
"target": "es6",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"declaration": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declarationDir": "dist/declaration",
|
||||||
|
"paths": {
|
||||||
|
"cvat-canvas.node": ["dist/cvat-canvas3d.node"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/typescript/*.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (C) 2021 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const DtsBundleWebpack = require('dts-bundle-webpack');
|
||||||
|
|
||||||
|
const nodeConfig = {
|
||||||
|
target: 'node',
|
||||||
|
mode: 'production',
|
||||||
|
devtool: 'source-map',
|
||||||
|
entry: './src/typescript/canvas3d.ts',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'cvat-canvas3d.node.js',
|
||||||
|
library: 'canvas3d',
|
||||||
|
libraryTarget: 'commonjs',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.json'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
],
|
||||||
|
presets: [['@babel/preset-env'], ['@babel/typescript']],
|
||||||
|
sourceType: 'unambiguous',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(css|scss)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
importLoaders: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'postcss-loader',
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new DtsBundleWebpack({
|
||||||
|
name: 'cvat-canvas3d.node',
|
||||||
|
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
|
||||||
|
out: '../cvat-canvas3d.node.d.ts',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const webConfig = {
|
||||||
|
target: 'web',
|
||||||
|
mode: 'production',
|
||||||
|
devtool: 'source-map',
|
||||||
|
entry: {
|
||||||
|
'cvat-canvas3d': './src/typescript/canvas3d.ts',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: '[name].[contenthash].js',
|
||||||
|
library: 'canvas3d',
|
||||||
|
libraryTarget: 'window',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
|
compress: false,
|
||||||
|
inline: true,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.json'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
targets: '> 2.5%', // https://github.com/browserslist/browserslist
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['@babel/typescript'],
|
||||||
|
],
|
||||||
|
sourceType: 'unambiguous',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
importLoaders: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'postcss-loader',
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new DtsBundleWebpack({
|
||||||
|
name: 'cvat-canvas3d',
|
||||||
|
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
|
||||||
|
out: '../cvat-canvas3d.d.ts',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = [webConfig, nodeConfig];
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,121 +0,0 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
// Setup mock for a server
|
|
||||||
jest.mock('../../src/server-proxy', () => {
|
|
||||||
const mock = require('../mocks/server-proxy.mock');
|
|
||||||
return mock;
|
|
||||||
});
|
|
||||||
|
|
||||||
const AnnotationsFilter = require('../../src/annotations-filter');
|
|
||||||
// Initialize api
|
|
||||||
window.cvat = require('../../src/api');
|
|
||||||
|
|
||||||
// Test cases
|
|
||||||
describe('Feature: toJSONQuery', () => {
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery([]);
|
|
||||||
expect(Array.isArray(groups)).toBeTruthy();
|
|
||||||
expect(typeof query).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert empty fitlers to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [, query] = annotationsFilter.toJSONQuery([]);
|
|
||||||
expect(query).toBe('$.objects[*].clientID');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert wrong fitlers (empty string) to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
expect(() => {
|
|
||||||
annotationsFilter.toJSONQuery(['']);
|
|
||||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert wrong fitlers (wrong number argument) to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
expect(() => {
|
|
||||||
annotationsFilter.toJSONQuery(1);
|
|
||||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert wrong fitlers (wrong array argument) to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
expect(() => {
|
|
||||||
annotationsFilter.toJSONQuery(['clientID ==6', 1]);
|
|
||||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert wrong filters (wrong expression) to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
expect(() => {
|
|
||||||
annotationsFilter.toJSONQuery(['clientID=5']);
|
|
||||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
|
|
||||||
expect(groups).toEqual([['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]']]);
|
|
||||||
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery(['label=="car" | width >= height & type=="track"']);
|
|
||||||
expect(groups).toEqual([['label=="car"', '|', 'width >= height', '&', 'type=="track"']]);
|
|
||||||
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
|
||||||
'label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]',
|
|
||||||
]);
|
|
||||||
expect(groups).toEqual([['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]']]);
|
|
||||||
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
|
||||||
'label=="car" & attr["parked"]==true',
|
|
||||||
'label=="pedestrian" & width > 150',
|
|
||||||
]);
|
|
||||||
expect(groups).toEqual([
|
|
||||||
['label=="car"', '&', 'attr["parked"]==true'],
|
|
||||||
'|',
|
|
||||||
['label=="pedestrian"', '&', 'width > 150'],
|
|
||||||
]);
|
|
||||||
expect(query).toBe(
|
|
||||||
'$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('convert filters to a json query', () => {
|
|
||||||
const annotationsFilter = new AnnotationsFilter();
|
|
||||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
|
||||||
// eslint-disable-next-line
|
|
||||||
'(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ',
|
|
||||||
]);
|
|
||||||
expect(groups).toEqual([
|
|
||||||
[
|
|
||||||
[
|
|
||||||
['label==["car `mazda`"]'],
|
|
||||||
'&',
|
|
||||||
[
|
|
||||||
'attr["sunglass ( help ) es"]==true',
|
|
||||||
'|',
|
|
||||||
['width > 150', '|', 'height > 150', '&', ['clientID == serverID']],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
expect(query).toBe(
|
|
||||||
// eslint-disable-next-line
|
|
||||||
'$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
|||||||
|
<!--
|
||||||
|
The file has been downloaded from: https://icon-icons.com/ru/%D0%B7%D0%BD%D0%B0%D1%87%D0%BE%D0%BA/%D0%92-%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B5-OpenCV/132129
|
||||||
|
License: Attribution 4.0 International (CC BY 4.0) https://creativecommons.org/licenses/by/4.0/
|
||||||
|
The file has been modified
|
||||||
|
-->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="40" height="40">
|
||||||
|
<g style="transform: scale(0.078)">
|
||||||
|
<path d="M148.6458282,81.0641403C191.8570709-0.3458452,307.612915-4.617764,356.5062561,73.3931732c37.8880615,60.4514771,13.7960815,135.4847717-41.8233948,167.7876129l-36.121521-62.5643005c22.1270447-12.8510284,31.7114563-42.7013397,16.6385498-66.750618c-19.4511414-31.034935-65.5021057-29.3354645-82.692749,3.0517044c-12.7206879,23.9658356-2.6391449,51.5502472,18.3088379,63.7294922l-36.1482544,62.6105804C142.0118256,210.643219,116.6704254,141.3057709,148.6458282,81.0641403z M167.9667206,374.4708557c-0.0435791,24.2778625-18.934967,46.8978271-46.092804,47.9000549c-36.6418304,1.3522339-61.0877724-37.6520386-43.8971252-70.0392151c13.2918015-25.0418091,43.8297424-31.7192383,65.9928284-19.1222839l36.2165222-62.7288513c-55.7241974-31.7991638-132.6246796-15.0146027-166.0706635,47.9976501c-43.2111893,81.4099731,18.2372913,179.4530945,110.3418884,176.0540161c68.1375427-2.5146179,115.5750122-59.1652527,115.8612366-120.0613708H167.9667206z M451.714386,270.7571411l-36.1215515,62.5642395c22.2027588,12.816864,31.8418274,42.7249451,16.744751,66.8127441c-19.4511414,31.0349426-65.5021057,29.3354797-82.692688-3.0516968c-12.742218-24.0063782-2.6048279-51.643219,18.4154358-63.7908325l-36.1482544-62.6105652c-52.7280579,30.5827942-78.1254272,99.9726562-46.128479,160.2548218c43.2111816,81.4099731,158.9670105,85.6818848,207.8603821,7.6710205C531.5561523,378.1168213,507.4096069,303.0259705,451.714386,270.7571411z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,170 +0,0 @@
|
|||||||
// Copyright (C) 2020 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
|
|
||||||
import Title from 'antd/lib/typography/Title';
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
|
||||||
import Tooltip from 'antd/lib/tooltip';
|
|
||||||
import Modal from 'antd/lib/modal';
|
|
||||||
import { FilterOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
import {
|
|
||||||
changeAnnotationsFilters as changeAnnotationsFiltersAction,
|
|
||||||
fetchAnnotationsAsync,
|
|
||||||
} from 'actions/annotation-actions';
|
|
||||||
import { CombinedState } from 'reducers/interfaces';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
annotationsFilters: string[];
|
|
||||||
annotationsFiltersHistory: string[];
|
|
||||||
searchForwardShortcut: string;
|
|
||||||
searchBackwardShortcut: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchToProps {
|
|
||||||
changeAnnotationsFilters(value: SelectValue): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: CombinedState): StateToProps {
|
|
||||||
const {
|
|
||||||
annotation: {
|
|
||||||
annotations: { filters: annotationsFilters, filtersHistory: annotationsFiltersHistory },
|
|
||||||
},
|
|
||||||
shortcuts: { normalizedKeyMap },
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
return {
|
|
||||||
annotationsFilters,
|
|
||||||
annotationsFiltersHistory,
|
|
||||||
searchForwardShortcut: normalizedKeyMap.SEARCH_FORWARD,
|
|
||||||
searchBackwardShortcut: normalizedKeyMap.SEARCH_BACKWARD,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
|
||||||
return {
|
|
||||||
changeAnnotationsFilters(value: SelectValue) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
dispatch(changeAnnotationsFiltersAction([value]));
|
|
||||||
dispatch(fetchAnnotationsAsync());
|
|
||||||
} else if (
|
|
||||||
Array.isArray(value) &&
|
|
||||||
value.every((element: string | number | LabeledValue): boolean => typeof element === 'string')
|
|
||||||
) {
|
|
||||||
dispatch(changeAnnotationsFiltersAction(value as string[]));
|
|
||||||
dispatch(fetchAnnotationsAsync());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function filtersHelpModalContent(searchForwardShortcut: string, searchBackwardShortcut: string): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Paragraph>
|
|
||||||
<Title level={3}>General</Title>
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
You can use filters to display only subset of objects on a frame or to search objects that satisfy the
|
|
||||||
filters using hotkeys
|
|
||||||
<Text strong>{` ${searchForwardShortcut} `}</Text>
|
|
||||||
and
|
|
||||||
<Text strong>{` ${searchBackwardShortcut} `}</Text>
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
<Text strong>Supported properties: </Text>
|
|
||||||
width, height, label, serverID, clientID, type, shape, occluded
|
|
||||||
<br />
|
|
||||||
<Text strong>Supported operators: </Text>
|
|
||||||
==, !=, >, >=, <, <=, (), & and |
|
|
||||||
<br />
|
|
||||||
<Text strong>
|
|
||||||
If you have double quotes in your query string, please escape them using back slash: \" (see
|
|
||||||
the latest example)
|
|
||||||
</Text>
|
|
||||||
<br />
|
|
||||||
All properties and values are case-sensitive. CVAT uses json queries to perform search.
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
<Title level={3}>Examples</Title>
|
|
||||||
<ul>
|
|
||||||
<li>label=="car" | label==["road sign"]</li>
|
|
||||||
<li>shape == "polygon"</li>
|
|
||||||
<li>width >= height</li>
|
|
||||||
<li>attr["Attribute 1"] == attr["Attribute 2"]</li>
|
|
||||||
<li>clientID == 50</li>
|
|
||||||
<li>
|
|
||||||
(label=="car" & attr["parked"]==true) | (label=="pedestrian"
|
|
||||||
& width > 150)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
(( label==["car \"mazda\""]) & (attr["sunglasses"]==true |
|
|
||||||
(width > 150 | height > 150 & (clientID == serverID)))))
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Paragraph>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
|
|
||||||
const {
|
|
||||||
annotationsFilters,
|
|
||||||
annotationsFiltersHistory,
|
|
||||||
searchForwardShortcut,
|
|
||||||
searchBackwardShortcut,
|
|
||||||
changeAnnotationsFilters,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [underCursor, setUnderCursor] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
className='cvat-annotations-filters-input'
|
|
||||||
allowClear
|
|
||||||
value={annotationsFilters}
|
|
||||||
mode='tags'
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={
|
|
||||||
underCursor ? (
|
|
||||||
<>
|
|
||||||
<Tooltip title='Click to open help' mouseLeaveDelay={0}>
|
|
||||||
<FilterOutlined
|
|
||||||
onClick={(e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
Modal.info({
|
|
||||||
width: 700,
|
|
||||||
title: 'How to use filters?',
|
|
||||||
content: filtersHelpModalContent(searchForwardShortcut, searchBackwardShortcut),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FilterOutlined style={{ transform: 'scale(0.9)' }} />
|
|
||||||
<span style={{ marginLeft: 5 }}>Annotations filters</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onChange={changeAnnotationsFilters}
|
|
||||||
onMouseEnter={() => setUnderCursor(true)}
|
|
||||||
onMouseLeave={() => setUnderCursor(false)}
|
|
||||||
>
|
|
||||||
{annotationsFiltersHistory.map(
|
|
||||||
(element: string): JSX.Element => (
|
|
||||||
<Select.Option key={element} value={element} className='cvat-annotations-filters-input-history-element'>
|
|
||||||
{element}
|
|
||||||
</Select.Option>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AnnotationsFiltersInput);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue