├── .babelrc ├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── public ├── EditorControls.js ├── favicon.ico ├── forklift │ └── forklift.stl ├── image │ └── viz │ │ ├── viz-image.png │ │ ├── viz-interactive-marker.png │ │ ├── viz-laserscan.png │ │ ├── viz-map.png │ │ ├── viz-marker.png │ │ ├── viz-markerarray.png │ │ ├── viz-odometry.png │ │ ├── viz-path.png │ │ ├── viz-point.png │ │ ├── viz-pointcloud.png │ │ ├── viz-pose.png │ │ ├── viz-posearray.png │ │ ├── viz-range.png │ │ ├── viz-robotmodel.png │ │ ├── viz-tf.png │ │ └── viz-wrench.png ├── index.html ├── logo.svg ├── screenshot.png ├── zethus.svg ├── zethus_full.svg └── zethus_mark.svg ├── src ├── components │ ├── chevron.jsx │ ├── connectionDot.jsx │ ├── errorBoundary.jsx │ ├── logo.jsx │ ├── optionRow.jsx │ └── styled │ │ ├── constants.js │ │ ├── index.js │ │ ├── modal.js │ │ └── viz.js ├── index.jsx ├── panels │ ├── addModal │ │ ├── index.jsx │ │ ├── options.jsx │ │ ├── optionsGeneric.jsx │ │ ├── optionsRobotModel.jsx │ │ ├── tabTopicName.jsx │ │ ├── tabVizType.jsx │ │ ├── vizTypeDetails.jsx │ │ └── vizTypeItem.jsx │ ├── configurationModal │ │ └── index.jsx │ ├── graphVisualizationModal │ │ ├── Tree.jsx │ │ ├── constants.js │ │ ├── index.jsx │ │ ├── utils.js │ │ └── visualizationToolbar.jsx │ ├── header │ │ ├── index.jsx │ │ ├── tool.jsx │ │ └── toolbar.jsx │ ├── index.jsx │ ├── info │ │ ├── addInfoPanelModal.jsx │ │ ├── content.jsx │ │ ├── formattedContent.jsx │ │ ├── index.jsx │ │ └── rawContent.jsx │ ├── sidebar │ │ ├── globalOptions.jsx │ │ ├── index.jsx │ │ ├── rosReconnectHandler.jsx │ │ └── vizOptions │ │ │ ├── arrow.jsx │ │ │ ├── axes.jsx │ │ │ ├── colorTransformer.jsx │ │ │ ├── flatArrow.jsx │ │ │ ├── index.jsx │ │ │ ├── interactiveMarkerOptions.jsx │ │ │ ├── laserScan.jsx │ │ │ ├── map.jsx │ │ │ ├── marker.jsx │ │ │ ├── odometry.jsx │ │ │ ├── path.jsx │ │ │ ├── point.jsx │ │ │ ├── pointcloud.jsx │ │ │ ├── pose.jsx │ │ │ ├── range.jsx │ │ │ ├── robotModel.jsx │ │ │ ├── shape.jsx │ │ │ ├── vizSpecificOption.jsx │ │ │ └── wrench.jsx │ ├── sources │ │ └── index.js │ ├── tools │ │ └── index.jsx │ ├── viewer │ │ └── index.jsx │ └── visualizations │ │ └── index.jsx ├── utils │ ├── common.js │ ├── constants.js │ ├── editorControls.js │ ├── index.js │ ├── raycaster.js │ ├── sanitize.js │ ├── toolPublisher.js │ ├── toolbar.jsx │ └── vizOptions.jsx └── zethus.jsx ├── webpack.app.dev.js ├── webpack.app.prod.js └── webpack.lib.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-styled-components", 6 | { 7 | "fileName": false, 8 | "pure": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log 3 | */build 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-airbnb", "prettier", "prettier/react"], 3 | "plugins": ["prettier"], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "rules": { 10 | "jsx-a11y/label-has-associated-control": "off", 11 | "semi": 2, 12 | "func-names": "off", 13 | "no-plusplus": "off", 14 | "comma-dangle": "off", 15 | "import/prefer-default-export": "off", 16 | "class-methods-use-this": "off", 17 | "no-unused-vars": [ 18 | "warn", 19 | { "vars": "all", "args": "after-used", "ignoreRestSiblings": false } 20 | ], 21 | "no-param-reassign": "off", 22 | "no-unused-expressions": "warn", 23 | "no-bitwise": "off", 24 | "no-continue": "off", 25 | "no-restricted-syntax": "off", 26 | "no-multi-spaces": "off", 27 | "no-undef": "warn", 28 | "no-case-declarations": "off", 29 | "array-callback-return": "off", 30 | "prettier/prettier": "error", 31 | "react/jsx-filename-extension": [ 32 | 1, 33 | { 34 | "extensions": [".js", ".jsx"] 35 | } 36 | ], 37 | "import/no-named-as-default": 0, 38 | "react/sort-comp": 0, 39 | "jsx-a11y/no-noninteractive-element-interactions": 0, 40 | "react/no-did-mount-set-state": 0, 41 | "import/first": 0, 42 | "no-console": 0, 43 | "react/prop-types": 0, 44 | "react/no-unescaped-entities": 0, 45 | "no-underscore-dangle": 0, 46 | "default-case": 0, 47 | "jsx-a11y/label-has-for": 0, 48 | "function-paren-newline": 0, 49 | "no-confusing-arrow": 0, 50 | "arrow-parens": 0, 51 | "space-before-function-paren": 0, 52 | "object-curly-newline": 0, 53 | "jsx-a11y/no-static-element-interactions": 0, 54 | "jsx-a11y/click-events-have-key-events": 0, 55 | "import/no-extraneous-dependencies": [ 56 | "error", 57 | { 58 | "devDependencies": true, 59 | "optionalDependencies": false, 60 | "peerDependencies": false 61 | } 62 | ], 63 | "jsx-a11y/anchor-is-valid": [ 64 | "error", 65 | { 66 | "components": ["Link"], 67 | "specialLink": ["to"] 68 | } 69 | ] 70 | }, 71 | "globals": {} 72 | } 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is and what is expected. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. OS: [e.g. iOS], Browser [e.g. chrome, safari], Version [e.g. 22] 16 | 2. Go to '...' 17 | 3. Click on '....' 18 | 4. See error 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### All Submissions: 2 | 3 | * [ ] Have you followed the guidelines in our Contributing document? 4 | * [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change? 5 | 6 | 7 | 8 | ### New Feature Submissions: 9 | 10 | 1. [ ] Does your submission pass tests? 11 | 2. [ ] Have you lint your code locally prior to submission? 12 | 13 | ### Changes to Core Features: 14 | 15 | * [ ] Have you added an explanation of what your changes do and why you'd like us to include them? 16 | * [ ] Have you written new tests for your core changes, as applicable? 17 | * [ ] Have you successfully ran tests with your changes locally? 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | packaget-lock.json 5 | **/node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | **/build 14 | **/dist 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Editor 28 | .idea/ 29 | /build-lib/ 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | .github 20 | .gitignore 21 | 22 | 23 | 24 | ################################################ 25 | # Local Configuration 26 | # 27 | # Explicitly ignore files which contain: 28 | # 29 | # 1. Sensitive information you'd rather not push to 30 | # your git repository. 31 | # e.g., your personal API keys or passwords. 32 | # 33 | # 2. Environment-specific configuration 34 | # Basically, anything that would be annoying 35 | # to have to change every time you do a 36 | # `git pull` 37 | # e.g., your local development database, or 38 | # the S3 bucket you're using for file uploads 39 | # development. 40 | # 41 | ################################################ 42 | 43 | config/local.js 44 | 45 | 46 | 47 | 48 | 49 | ################################################ 50 | # Dependencies 51 | # 52 | # When releasing a production app, you may 53 | # consider including your node_modules and 54 | # bower_components directory in your git repo, 55 | # but during development, its best to exclude it, 56 | # since different developers may be working on 57 | # different kernels, where dependencies would 58 | # need to be recompiled anyway. 59 | # 60 | # More on that here about node_modules dir: 61 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 62 | # (credit Mikeal Rogers, @mikeal) 63 | # 64 | # About bower_components dir, you can see this: 65 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 66 | # (credit Addy Osmani, @addyosmani) 67 | # 68 | ################################################ 69 | 70 | node_modules 71 | bower_components 72 | 73 | 74 | 75 | 76 | ################################################ 77 | # Sails.js / Waterline / Webpack 78 | # 79 | # Files generated by Sails and Grunt, or related 80 | # tasks and adapters. 81 | ################################################ 82 | .tmp 83 | public/dist 84 | assets.json 85 | dump.rdb 86 | 87 | 88 | 89 | 90 | 91 | ################################################ 92 | # Node.js / NPM 93 | # 94 | # Common files generated by Node, NPM, and the 95 | # related ecosystem. 96 | ################################################ 97 | lib-cov 98 | *.seed 99 | *.log 100 | *.out 101 | *.pid 102 | npm-debug.log 103 | 104 | 105 | 106 | 107 | 108 | ################################################ 109 | # Miscellaneous 110 | # 111 | # Common files generated by text editors, 112 | # operating systems, file systems, etc. 113 | ################################################ 114 | 115 | *~ 116 | *# 117 | .DS_STORE 118 | .netbeans 119 | nbproject 120 | .idea 121 | .node_history -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | **/node_modules 4 | public/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "overrides": [ 5 | { 6 | "files": ["*.js", "*.jsx"], 7 | "options": { 8 | "parser": "node_modules/prettier-sort-destructure/index.js" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-prettier"], 4 | "rules": { 5 | "prettier/prettier": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "11" 4 | script: 5 | - npm run lint 6 | - npm run build 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | ### Added 3 | 4 | ### Removed 5 | 6 | ### Changed 7 | 8 | ### Fixed 9 | 10 | ## [0.1.20] - 2019-11-14 11 | ### Added 12 | 1. Added support for Amphion.ImageStream 13 | 2. Added support for Amphion.DepthCloud 14 | 3. Added toolbar 15 | 4. Added Point publish tool in the toolbar 16 | 5. Added configuration input in sidebar 17 | 18 | ### Changed 19 | 1. Ejected from CRA 20 | 21 | ## [0.1.10] - 2019-08-19 22 | ### Added 23 | - Pointcloud options: color channel, points' size, use rainbow 24 | 25 | ### Changed 26 | - update amphion version 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@rapyuta-robotics.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the project! 4 | 5 | Contributions of all kinds are welcome including pull requests, issues, and reports of or links to repos using the project! 6 | 7 | ## Filing Issues 8 | 9 | When submitting a bug report try to include a clear, minimal repro case along with the issue. More information means the problem can be fixed faster and better! 10 | 11 | When submitting a feature request please include a well-defined use case and even better if you include code modeling how the new feature could be used with a proposed API! 12 | 13 | Promote discussion! Let's talk about the change and discuss what the best, most flexible option might be. 14 | 15 | ## Pull Requests 16 | 17 | Keep it simple! Code clean up and linting changes should be submitted as separate PRS from logic changes so the impact to the codebase is clear. 18 | 19 | Keep PRs with logic changes to the essential modifications if possible -- people have to read it! 20 | 21 | Open an issue for discussion first so we can have consensus on the change and be sure to reference the issue that the PR is addressing. 22 | 23 | Keep commit messages descriptive. "Update" and "oops" doesn't tell anyone what happened there! 24 | 25 | Don't modify existing commits when responding to PR comments. New commits make it easier to follow what changed. 26 | 27 | ## Code Style 28 | 29 | Follow the `.editorconfig`, `.eslintrc` and `.prettierrc` style configurations included in the repo to keep the code looking consistent. 30 | 31 | Try to keep code as clear as possible! Code for readability! For example longer, descriptive variable names are preferred to short ones. If a line of code includes a lot of nested statements (even just one or two) consider breaking the line up into multiple variables to improve the clarity of what's happening. 32 | 33 | Include comments describing _why_ a change was made. If code was moved from one part of a function to another then tell what happened and why the change got made so it doesn't get moved back. Comments aren't just for others, they're for your future self, too! 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM exiasr/alpine-yarn-nginx 2 | RUN apk update 3 | COPY . /usr/src/app/ 4 | RUN (cd /usr/src/app && npm ci && npm run build) 5 | WORKDIR /usr/src/app 6 | RUN mkdir -p /usr/share/nginx/html/ && \ 7 | cp -R build/* /usr/share/nginx/html/ && \ 8 | cp /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf && \ 9 | rm -rf /usr/src/app 10 | 11 | CMD ["nginx", "-g", "daemon off;"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zethus 2 | 3 | [![NPM package][npm]][npm-url] 4 | [![Dependencies][dependencies]][dependencies-url] 5 | [![Dev Dependencies][dev-dependencies]][dev-dependencies-url] 6 | [![Language Grade][lgtm]][lgtm-url] 7 | 8 | Realtime robot data visualization in the browser 9 | 10 | Zethus Screenshot 11 | 12 | ### Getting started 13 | #### Development version 14 | **You’ll need to have Node 8.16.0 or Node 10.16.0 or later version on your local development machine** (but it’s not required on the server). You can use [nvm](https://github.com/creationix/nvm#installation) (macOS/Linux) or [nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows) to easily switch Node versions between different projects. 15 | To start the user interface locally in dev mode, run the following commands: 16 | ``` 17 | npm install 18 | npm start 19 | ``` 20 | #### Production version 21 | The production version can be run locally with either docker or by building from source. 22 | **Running the docker container:** 23 | ``` 24 | docker build -t=zethus . 25 | docker run -p 8080:8080 zethus 26 | ``` 27 | **Building the source:** 28 | ``` 29 | npm install 30 | npm run build 31 | ``` 32 | Then start a server in `build` directory. You can use [serve](https://www.npmjs.com/package/serve) npm package or any similar software 33 | 34 | ### Visualizations and Documentation 35 | Available options for each visualization are on the [Github Wiki page](https://github.com/rapyuta-robotics/zethus/wiki) 36 | 37 | ### Contributing 38 | PRs, bug reports, and feature requests are welcome! Please observe [CONTRIBUTING.md](https://github.com/rapyuta-robotics/zethus/blob/devel/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](https://github.com/rapyuta-robotics/zethus/blob/devel/CODE_OF_CONDUCT.md) when making a contribution. 39 | 40 | ### Maintenance Status 41 | Active: Rapyuta Robotics is actively working on this project, and we expect to continue for work for the foreseeable future. 42 | 43 | [npm]: https://badge.fury.io/js/zethus.svg 44 | [npm-url]: https://www.npmjs.com/package/zethus 45 | [dependencies]: https://img.shields.io/david/rapyuta-robotics/zethus.svg 46 | [dependencies-url]: https://david-dm.org/rapyuta-robotics/zethus 47 | [dev-dependencies]: https://img.shields.io/david/dev/rapyuta-robotics/zethus.svg 48 | [dev-dependencies-url]: https://david-dm.org/rapyuta-robotics/zethus#info=devDependencies 49 | [lgtm]: https://img.shields.io/lgtm/grade/javascript/g/rapyuta-robotics/zethus.svg?label=code%20quality 50 | [lgtm-url]: https://lgtm.com/projects/g/rapyuta-robotics/zethus 51 | 52 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080 default_server; 3 | listen [::]:8080 default_server; 4 | 5 | root /usr/share/nginx/html; 6 | index index.html index.htm index.nginx-debian.html; 7 | 8 | server_name _; 9 | location / { 10 | try_files $uri/index.html $uri $uri/ /index.html; 11 | } 12 | 13 | error_page 404 /404.html; 14 | location = /404.html { 15 | root /usr/share/nginx/html/; 16 | internal; 17 | } 18 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { 19 | expires 1M; 20 | access_log off; 21 | add_header Cache-Control "public"; 22 | } 23 | location ~* \.(?:css|js)$ { 24 | try_files $uri =404; 25 | expires 1y; 26 | access_log off; 27 | add_header Cache-Control "public"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@robostack/zethus", 3 | "version": "0.1.23", 4 | "description": "Realtime robot data visualization in the browser", 5 | "main": "build-lib/zethus.umd.js", 6 | "homepage": "https://rapyuta-robotics.github.io/zethus", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/robostack/zethus.git" 10 | }, 11 | "keywords": [ 12 | "ros", 13 | "robotics", 14 | "riz", 15 | "javascript", 16 | "threejs" 17 | ], 18 | "author": "Rapyuta Robotics ", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/robostack/zethus/issues" 22 | }, 23 | "dependencies": { 24 | "amphion": "npm:@robostack/amphion@^0.1.24", 25 | "brace": "^0.11.1", 26 | "classnames": "^2.2.6", 27 | "d3": "^5.16.0", 28 | "dagre-d3": "^0.6.4", 29 | "is-valid-http-url": "^1.0.3", 30 | "jsoneditor-react": "^2.0.0", 31 | "lodash": "^4.17.13", 32 | "mousetrap": "^1.6.5", 33 | "prop-types": "^15.7.2", 34 | "react-graceful-unmount": "^1.0.7", 35 | "react-markdown": "^4.3.1", 36 | "react-rnd": "^10.1.10", 37 | "react-router-dom": "^5.2.0", 38 | "react-select": "^3.1.0", 39 | "react-tagsinput": "^3.19.0", 40 | "react-virtualized": "^9.21.2", 41 | "roslib": "npm:@robostack/roslib@^1.1.0", 42 | "shortid": "^2.2.14", 43 | "stats-js": "^1.0.0", 44 | "store": "^2.0.12", 45 | "styled-components": "^4.3.2", 46 | "three": "^0.117.0" 47 | }, 48 | "scripts": { 49 | "start": "webpack-dev-server --config webpack.app.dev.js --host 192.168.64.6", 50 | "build": "webpack --config webpack.app.prod.js", 51 | "build-lib": "webpack --config webpack.lib.prod.js", 52 | "lint": "eslint src/**/*.{js,jsx} --fix", 53 | "predeploy": "npm run build", 54 | "deploy": "gh-pages -d build", 55 | "prettier": "prettier src/**/* --write" 56 | }, 57 | "browserslist": [ 58 | ">0.2%", 59 | "not dead", 60 | "not ie <= 11", 61 | "not op_mini all" 62 | ], 63 | "devDependencies": { 64 | "@babel/core": "^7.10.2", 65 | "@babel/preset-env": "^7.10.2", 66 | "@babel/preset-react": "^7.10.1", 67 | "@types/d3": "^5.7.2", 68 | "babel-loader": "^8.1.0", 69 | "clean-webpack-plugin": "^3.0.0", 70 | "copy-webpack-plugin": "^5.1.1", 71 | "css-loader": "^3.6.0", 72 | "eslint": "^6.8.0", 73 | "eslint-config-airbnb": "^18.1.0", 74 | "eslint-config-prettier": "^6.11.0", 75 | "eslint-loader": "^3.0.4", 76 | "eslint-plugin-import": "^2.21.2", 77 | "eslint-plugin-jsx-a11y": "^6.2.1", 78 | "eslint-plugin-prettier": "^3.1.4", 79 | "eslint-plugin-react": "^7.20.0", 80 | "eslint-restricted-globals": "^0.2.0", 81 | "file-loader": "^4.3.0", 82 | "gh-pages": "^2.2.0", 83 | "html-webpack-plugin": "^3.2.0", 84 | "husky": "^3.1.0", 85 | "lint-staged": "^9.5.0", 86 | "node-sass": "^4.14.1", 87 | "npm-run-all": "^4.1.5", 88 | "prettier": "^1.19.1", 89 | "prettier-sort-destructure": "0.0.4", 90 | "react": "^16.13.1", 91 | "react-dom": "^16.13.1", 92 | "sass-loader": "^8.0.2", 93 | "style-loader": "^1.2.1", 94 | "stylelint-config-standard": "^19.0.0", 95 | "stylelint-prettier": "^1.1.2", 96 | "url-loader": "^2.3.0", 97 | "webpack": "^4.43.0", 98 | "webpack-cli": "^3.3.11", 99 | "webpack-dev-server": "^3.11.0" 100 | }, 101 | "husky": { 102 | "hooks": { 103 | "pre-commit": "lint-staged" 104 | } 105 | }, 106 | "lint-staged": { 107 | "*.{js,jsx}": [ 108 | "eslint --fix", 109 | "prettier --write", 110 | "git add" 111 | ], 112 | "*.{css,scss}": [ 113 | "prettier --write", 114 | "git add" 115 | ] 116 | }, 117 | "peerDependencies": { 118 | "react": "^16.10.1", 119 | "react-dom": "^16.10.1" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /public/EditorControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | */ 7 | 8 | THREE.EditorControls = function ( object, domElement ) { 9 | 10 | domElement = ( domElement !== undefined ) ? domElement : document; 11 | 12 | // API 13 | 14 | this.enabled = true; 15 | this.center = new THREE.Vector3(); 16 | this.panSpeed = 0.001; 17 | this.zoomSpeed = 0.1; 18 | this.rotationSpeed = 0.005; 19 | 20 | // internals 21 | 22 | var scope = this; 23 | var vector = new THREE.Vector3(); 24 | var delta = new THREE.Vector3(); 25 | var box = new THREE.Box3(); 26 | 27 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 }; 28 | var state = STATE.NONE; 29 | 30 | var center = this.center; 31 | var normalMatrix = new THREE.Matrix3(); 32 | // normalMatrix.set(0, 0, -1, -1, 0, 0, 0, 1, 0); 33 | var pointer = new THREE.Vector2(); 34 | var pointerOld = new THREE.Vector2(); 35 | var spherical = new THREE.Spherical(); 36 | 37 | // events 38 | 39 | var changeEvent = { type: 'change' }; 40 | 41 | this.focus = function ( target ) { 42 | 43 | var distance; 44 | 45 | box.setFromObject( target ); 46 | 47 | if ( box.isEmpty() === false ) { 48 | 49 | center.copy( box.getCenter() ); 50 | distance = box.getBoundingSphere().radius; 51 | 52 | } else { 53 | 54 | // Focusing on an Group, AmbientLight, etc 55 | 56 | center.setFromMatrixPosition( target.matrixWorld ); 57 | distance = 0.1; 58 | 59 | } 60 | 61 | delta.set( 0, 0, 1 ); 62 | delta.applyQuaternion( object.quaternion ); 63 | delta.multiplyScalar( distance * 4 ); 64 | 65 | object.position.copy( center ).add( delta ); 66 | 67 | scope.dispatchEvent( changeEvent ); 68 | 69 | }; 70 | 71 | this.pan = function ( delta ) { 72 | 73 | var distance = object.position.distanceTo( center ); 74 | 75 | delta.multiplyScalar( distance * scope.panSpeed ); 76 | delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) ); 77 | 78 | object.position.add( delta ); 79 | center.add( delta ); 80 | 81 | scope.dispatchEvent( changeEvent ); 82 | 83 | }; 84 | 85 | this.zoom = function ( delta ) { 86 | 87 | var distance = object.position.distanceTo( center ); 88 | 89 | delta.multiplyScalar( distance * scope.zoomSpeed ); 90 | 91 | if ( delta.length() > distance ) return; 92 | 93 | delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) ); 94 | 95 | object.position.add( delta ); 96 | 97 | scope.dispatchEvent( changeEvent ); 98 | 99 | }; 100 | 101 | this.rotate = function ( delta ) { 102 | 103 | vector.copy( object.position ).sub( center ); 104 | 105 | // spherical.setFromVector3( vector ); 106 | spherical.setFromCartesianCoords( -1 * vector.x, vector.z, vector.y ); 107 | 108 | spherical.theta += delta.x; 109 | spherical.phi += delta.y; 110 | 111 | spherical.makeSafe(); 112 | 113 | const tempRelPosition = vector.setFromSpherical( spherical ); 114 | vector.set(-1 * tempRelPosition.x, tempRelPosition.z, tempRelPosition.y); 115 | 116 | object.position.copy( center ).add( vector ); 117 | 118 | object.lookAt( center ); 119 | 120 | scope.dispatchEvent( changeEvent ); 121 | 122 | }; 123 | 124 | // mouse 125 | 126 | function onMouseDown( event ) { 127 | 128 | if ( scope.enabled === false ) return; 129 | 130 | if ( event.button === 0 ) { 131 | 132 | state = STATE.ROTATE; 133 | 134 | } else if ( event.button === 1 ) { 135 | 136 | state = STATE.ZOOM; 137 | 138 | } else if ( event.button === 2 ) { 139 | 140 | state = STATE.PAN; 141 | 142 | } 143 | 144 | pointerOld.set( event.clientX, event.clientY ); 145 | 146 | domElement.addEventListener( 'mousemove', onMouseMove, false ); 147 | domElement.addEventListener( 'mouseup', onMouseUp, false ); 148 | domElement.addEventListener( 'mouseout', onMouseUp, false ); 149 | domElement.addEventListener( 'dblclick', onMouseUp, false ); 150 | 151 | } 152 | 153 | function onMouseMove( event ) { 154 | 155 | if ( scope.enabled === false ) return; 156 | 157 | pointer.set( event.clientX, event.clientY ); 158 | 159 | var movementX = pointer.x - pointerOld.x; 160 | var movementY = pointer.y - pointerOld.y; 161 | 162 | if ( state === STATE.ROTATE ) { 163 | 164 | scope.rotate( delta.set( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) ); 165 | 166 | } else if ( state === STATE.ZOOM ) { 167 | 168 | scope.zoom( delta.set( 0, 0, movementY ) ); 169 | 170 | } else if ( state === STATE.PAN ) { 171 | 172 | scope.pan( delta.set( - movementX, movementY, 0 ) ); 173 | 174 | } 175 | 176 | pointerOld.set( event.clientX, event.clientY ); 177 | 178 | } 179 | 180 | function onMouseUp( event ) { 181 | 182 | domElement.removeEventListener( 'mousemove', onMouseMove, false ); 183 | domElement.removeEventListener( 'mouseup', onMouseUp, false ); 184 | domElement.removeEventListener( 'mouseout', onMouseUp, false ); 185 | domElement.removeEventListener( 'dblclick', onMouseUp, false ); 186 | 187 | state = STATE.NONE; 188 | 189 | } 190 | 191 | function onMouseWheel( event ) { 192 | 193 | event.preventDefault(); 194 | 195 | // Normalize deltaY due to https://bugzilla.mozilla.org/show_bug.cgi?id=1392460 196 | scope.zoom( delta.set( 0, 0, event.deltaY > 0 ? 1 : - 1 ) ); 197 | 198 | } 199 | 200 | function contextmenu( event ) { 201 | 202 | event.preventDefault(); 203 | 204 | } 205 | 206 | this.dispose = function () { 207 | 208 | domElement.removeEventListener( 'contextmenu', contextmenu, false ); 209 | domElement.removeEventListener( 'mousedown', onMouseDown, false ); 210 | domElement.removeEventListener( 'wheel', onMouseWheel, false ); 211 | 212 | domElement.removeEventListener( 'mousemove', onMouseMove, false ); 213 | domElement.removeEventListener( 'mouseup', onMouseUp, false ); 214 | domElement.removeEventListener( 'mouseout', onMouseUp, false ); 215 | domElement.removeEventListener( 'dblclick', onMouseUp, false ); 216 | 217 | domElement.removeEventListener( 'touchstart', touchStart, false ); 218 | domElement.removeEventListener( 'touchmove', touchMove, false ); 219 | 220 | }; 221 | 222 | domElement.addEventListener( 'contextmenu', contextmenu, false ); 223 | domElement.addEventListener( 'mousedown', onMouseDown, false ); 224 | domElement.addEventListener( 'wheel', onMouseWheel, false ); 225 | 226 | // touch 227 | 228 | var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ]; 229 | var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ]; 230 | 231 | var prevDistance = null; 232 | 233 | function touchStart( event ) { 234 | 235 | if ( scope.enabled === false ) return; 236 | 237 | switch ( event.touches.length ) { 238 | 239 | case 1: 240 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 241 | touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 242 | break; 243 | 244 | case 2: 245 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 246 | touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 ); 247 | prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] ); 248 | break; 249 | 250 | } 251 | 252 | prevTouches[ 0 ].copy( touches[ 0 ] ); 253 | prevTouches[ 1 ].copy( touches[ 1 ] ); 254 | 255 | } 256 | 257 | 258 | function touchMove( event ) { 259 | 260 | if ( scope.enabled === false ) return; 261 | 262 | event.preventDefault(); 263 | event.stopPropagation(); 264 | 265 | function getClosest( touch, touches ) { 266 | 267 | var closest = touches[ 0 ]; 268 | 269 | for ( var i in touches ) { 270 | 271 | if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ]; 272 | 273 | } 274 | 275 | return closest; 276 | 277 | } 278 | 279 | switch ( event.touches.length ) { 280 | 281 | case 1: 282 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 283 | touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 284 | scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) ); 285 | break; 286 | 287 | case 2: 288 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ); 289 | touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 ); 290 | var distance = touches[ 0 ].distanceTo( touches[ 1 ] ); 291 | scope.zoom( delta.set( 0, 0, prevDistance - distance ) ); 292 | prevDistance = distance; 293 | 294 | 295 | var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) ); 296 | var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) ); 297 | offset0.x = - offset0.x; 298 | offset1.x = - offset1.x; 299 | 300 | scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) ); 301 | 302 | break; 303 | 304 | } 305 | 306 | prevTouches[ 0 ].copy( touches[ 0 ] ); 307 | prevTouches[ 1 ].copy( touches[ 1 ] ); 308 | 309 | } 310 | 311 | domElement.addEventListener( 'touchstart', touchStart, false ); 312 | domElement.addEventListener( 'touchmove', touchMove, false ); 313 | 314 | }; 315 | 316 | THREE.EditorControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 317 | THREE.EditorControls.prototype.constructor = THREE.EditorControls; 318 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/favicon.ico -------------------------------------------------------------------------------- /public/forklift/forklift.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/forklift/forklift.stl -------------------------------------------------------------------------------- /public/image/viz/viz-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-image.png -------------------------------------------------------------------------------- /public/image/viz/viz-interactive-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-interactive-marker.png -------------------------------------------------------------------------------- /public/image/viz/viz-laserscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-laserscan.png -------------------------------------------------------------------------------- /public/image/viz/viz-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-map.png -------------------------------------------------------------------------------- /public/image/viz/viz-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-marker.png -------------------------------------------------------------------------------- /public/image/viz/viz-markerarray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-markerarray.png -------------------------------------------------------------------------------- /public/image/viz/viz-odometry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-odometry.png -------------------------------------------------------------------------------- /public/image/viz/viz-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-path.png -------------------------------------------------------------------------------- /public/image/viz/viz-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-point.png -------------------------------------------------------------------------------- /public/image/viz/viz-pointcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-pointcloud.png -------------------------------------------------------------------------------- /public/image/viz/viz-pose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-pose.png -------------------------------------------------------------------------------- /public/image/viz/viz-posearray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-posearray.png -------------------------------------------------------------------------------- /public/image/viz/viz-range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-range.png -------------------------------------------------------------------------------- /public/image/viz/viz-robotmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-robotmodel.png -------------------------------------------------------------------------------- /public/image/viz/viz-tf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-tf.png -------------------------------------------------------------------------------- /public/image/viz/viz-wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-wrench.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | Zethus 13 | 14 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | Zethus 2 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/screenshot.png -------------------------------------------------------------------------------- /public/zethus.svg: -------------------------------------------------------------------------------- 1 | zethusZETHUS -------------------------------------------------------------------------------- /public/zethus_full.svg: -------------------------------------------------------------------------------- 1 | zethus_full -------------------------------------------------------------------------------- /public/zethus_mark.svg: -------------------------------------------------------------------------------- 1 | zethus_mark -------------------------------------------------------------------------------- /src/components/chevron.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Chevron = () => ( 4 | 13 | 14 | 15 | ); 16 | 17 | export default Chevron; 18 | -------------------------------------------------------------------------------- /src/components/connectionDot.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { RosStatusIndicator } from './styled'; 4 | 5 | class ConnectionDot extends React.PureComponent { 6 | render() { 7 | const { status } = this.props; 8 | return ; 9 | } 10 | } 11 | 12 | export default ConnectionDot; 13 | -------------------------------------------------------------------------------- /src/components/errorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React, { Component } from 'react'; 3 | import { ButtonOutline, Flex } from './styled'; 4 | import { downloadFile } from '../utils'; 5 | 6 | const Wrapper = styled(Flex)` 7 | flex-direction: column; 8 | align-items: center; 9 | width: 100%; 10 | height: 100%; 11 | padding-top: 200px; 12 | `; 13 | 14 | const Icon = styled.svg` 15 | width: 200px; 16 | height: 200px; 17 | `; 18 | 19 | const Heading = styled.h1` 20 | font-size: 20px; 21 | margin-bottom: 0; 22 | `; 23 | 24 | const ButtonsWrapper = styled(Flex)` 25 | ${ButtonOutline} { 26 | margin: 0 10px; 27 | } 28 | `; 29 | 30 | class ErrorBoundary extends Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { error: null }; 34 | 35 | this.downloadConfig = this.downloadConfig.bind(this); 36 | } 37 | 38 | componentDidCatch(error, errorInfo) { 39 | this.setState({ error }); 40 | } 41 | 42 | downloadConfig() { 43 | const { configuration } = this.props; 44 | downloadFile(JSON.stringify(configuration, null, 2), 'zethus_config.json'); 45 | } 46 | 47 | render() { 48 | const { error } = this.state; 49 | const { children, resetReload } = this.props; 50 | if (error) { 51 | return ( 52 | 53 | 60 | 61 | 62 | We're sorry — something's gone wrong 63 |

{error.message || 'An unknown error occured'}

64 | 65 | 66 | Download config 67 | 68 | 69 | Reset and reload 70 | 71 | 72 |
73 | ); 74 | } 75 | return children; 76 | } 77 | } 78 | 79 | export default ErrorBoundary; 80 | -------------------------------------------------------------------------------- /src/components/logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Logo = () => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default Logo; 19 | -------------------------------------------------------------------------------- /src/components/optionRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HalfWidth, StyledOptionRow } from './styled'; 3 | 4 | const OptionRow = ({ label, children, separator }) => { 5 | return ( 6 | 7 | 8 | {label} 9 | {separator || ':'} 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default OptionRow; 17 | -------------------------------------------------------------------------------- /src/components/styled/constants.js: -------------------------------------------------------------------------------- 1 | export const COLOR_PRIMARY = '#dc1d30'; 2 | 3 | export const COLOR_BLUE = '#013c89'; 4 | export const COLOR_RED = '#dc1d30'; 5 | 6 | export const COLOR_GREY_LIGHT_1 = '#f6f6f6'; 7 | export const COLOR_GREY_LIGHT_2 = '#dddddd'; 8 | 9 | export const COLOR_GREY_TEXT_1 = '#222222'; 10 | export const COLOR_GREY_TEXT_2 = '#444444'; 11 | export const COLOR_GREY_TEXT_3 = '#666666'; 12 | export const COLOR_GREY_TEXT_4 = '#888888'; 13 | 14 | export const FONT_SIZE_DEFAULT = '0.8rem'; 15 | export const FONT_SIZE_S = '0.7rem'; 16 | 17 | export const HEADER_HEIGHT_PX = 60; 18 | -------------------------------------------------------------------------------- /src/components/styled/modal.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Button, Flex, Paragraph } from './index'; 3 | import { 4 | COLOR_GREY_LIGHT_1, 5 | COLOR_GREY_LIGHT_2, 6 | COLOR_GREY_TEXT_2, 7 | COLOR_PRIMARY, 8 | } from './constants'; 9 | 10 | export const ModalWrapper = styled.div` 11 | position: absolute; 12 | z-index: 1000; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | bottom: 0; 17 | background-color: rgba(255, 255, 255, 0.9); 18 | `; 19 | 20 | export const ModalContents = styled.div` 21 | width: 1000px; 22 | margin: 100px auto; 23 | height: 600px; 24 | background-color: #ffffff; 25 | box-shadow: 0 0 40px rgba(0, 0, 0, 0.08); 26 | border-radius: 8px; 27 | padding: 20px; 28 | display: flex; 29 | flex-direction: column; 30 | `; 31 | 32 | export const ModalTitle = styled.h2` 33 | margin-bottom: 6px; 34 | margin-top: 0; 35 | `; 36 | 37 | export const ModalActions = styled.div` 38 | display: flex; 39 | flex-shrink: 0; 40 | padding: 10px 0 0; 41 | 42 | button { 43 | margin-left: 10px; 44 | } 45 | `; 46 | 47 | export const TypeEmpty = styled(Paragraph)` 48 | padding-left: 30px; 49 | font-style: italic; 50 | `; 51 | 52 | export const TypeUnsupported = styled(Flex)` 53 | padding: 10px; 54 | opacity: 0.4; 55 | cursor: not-allowed; 56 | `; 57 | 58 | export const TypeContainer = styled.div` 59 | display: flex; 60 | flex-grow: 1; 61 | overflow: hidden; 62 | `; 63 | 64 | export const TypeSelection = styled.div` 65 | flex: 1 0 50%; 66 | overflow-y: auto; 67 | `; 68 | 69 | export const TypeInfo = styled.div` 70 | border-left: 1px solid ${COLOR_GREY_LIGHT_2}; 71 | padding: 0 0 0 20px; 72 | flex: 1 0 50%; 73 | overflow-y: auto; 74 | 75 | h4 { 76 | margin-top: 10px; 77 | margin-bottom: 8px; 78 | font-size: 22px; 79 | } 80 | 81 | img { 82 | max-width: 100%; 83 | } 84 | 85 | & > div { 86 | margin-bottom: 8px; 87 | } 88 | `; 89 | 90 | export const TypeHeading = styled.h4` 91 | display: flex; 92 | align-items: center; 93 | text-transform: uppercase; 94 | margin-bottom: 5px; 95 | `; 96 | 97 | export const AddVizForm = styled.form` 98 | display: flex; 99 | flex-grow: 1; 100 | flex-shrink: 1; 101 | height: 0; 102 | flex-direction: column; 103 | `; 104 | 105 | export const TopicRow = styled(Button)` 106 | text-align: left; 107 | width: 100%; 108 | font-size: 16px; 109 | border: 0; 110 | background: transparent; 111 | padding: 10px; 112 | display: flex; 113 | align-items: center; 114 | 115 | .reactSelect { 116 | color: ${COLOR_GREY_TEXT_2}; 117 | } 118 | 119 | &:hover { 120 | color: ${COLOR_PRIMARY}; 121 | background-color: ${COLOR_GREY_LIGHT_1}; 122 | .reactSelect { 123 | color: ${COLOR_GREY_TEXT_2}; 124 | } 125 | } 126 | 127 | ${({ selected }) => 128 | selected && 129 | ` 130 | color: ${COLOR_PRIMARY}; 131 | background-color: ${COLOR_GREY_LIGHT_1}; 132 | `} 133 | `; 134 | 135 | export const TypeRow = styled(TopicRow)` 136 | padding-left: 30px; 137 | `; 138 | -------------------------------------------------------------------------------- /src/components/styled/viz.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { Button, Container, Flex } from './index'; 3 | import { 4 | COLOR_GREY_LIGHT_1, 5 | COLOR_GREY_LIGHT_2, 6 | COLOR_GREY_TEXT_4, 7 | COLOR_PRIMARY, 8 | } from './constants'; 9 | 10 | export const VizImageClose = styled(Button)` 11 | cursor: pointer; 12 | background-color: transparent; 13 | border: 0; 14 | color: inherit; 15 | text-decoration: underline; 16 | font-size: inherit; 17 | font-family: inherit; 18 | &:hover { 19 | text-decoration: none; 20 | } 21 | `; 22 | 23 | export const SidebarVizContainer = styled(Container)` 24 | overflow-y: auto; 25 | `; 26 | 27 | export const VizItemContent = styled.div` 28 | margin-top: 8px; 29 | margin-left: 24px; 30 | `; 31 | 32 | export const VizItemIcon = styled.div` 33 | width: 20px; 34 | height: 20px; 35 | display: inline-block; 36 | background-color: ${COLOR_GREY_LIGHT_1}; 37 | margin-right: 10px; 38 | border-radius: 4px; 39 | svg { 40 | width: 100%; 41 | } 42 | `; 43 | 44 | export const VizImageContainer = styled.div` 45 | background: black; 46 | display: flex; 47 | align-items: center; 48 | flex-direction: column; 49 | cursor: move; 50 | border-radius: 4px; 51 | overflow: hidden; 52 | height: 100%; 53 | width: 100%; 54 | border: 1px solid ${COLOR_GREY_LIGHT_2}; 55 | 56 | img { 57 | width: 100%; 58 | height: 100%; 59 | object-fit: contain; 60 | } 61 | 62 | canvas { 63 | width: 100%; 64 | height: 100%; 65 | } 66 | `; 67 | 68 | export const RosStatus = styled(Flex)` 69 | margin-bottom: 10px; 70 | align-items: center; 71 | `; 72 | 73 | export const VizImageHeader = styled.div` 74 | background: #fff; 75 | display: flex; 76 | width: 100%; 77 | font-size: 14px; 78 | padding: 2px 5px; 79 | min-height: 25px; 80 | border-bottom: 1px solid ${COLOR_GREY_LIGHT_2}; 81 | word-break: break-all; 82 | overflow: hidden; 83 | `; 84 | 85 | export const VizImageName = styled.div` 86 | pointer-events: none; 87 | user-select: none; 88 | `; 89 | 90 | export const VizItem = styled.div` 91 | padding: 10px 0; 92 | border-bottom: 1px solid ${COLOR_GREY_LIGHT_1}; 93 | `; 94 | 95 | export const VizItemActions = styled.div` 96 | display: flex; 97 | margin-top: 8px; 98 | 99 | button { 100 | background: ${COLOR_GREY_LIGHT_1}; 101 | border: 0; 102 | border-radius: 4px; 103 | color: ${COLOR_GREY_TEXT_4}; 104 | padding: 5px 10px; 105 | font-size: 0.6rem; 106 | &:hover { 107 | background-color: ${COLOR_PRIMARY}; 108 | color: #ffffff; 109 | } 110 | & + button { 111 | margin-left: 10px; 112 | } 113 | } 114 | `; 115 | 116 | export const VizItemCollapse = styled(Button)` 117 | background-color: transparent; 118 | border: 0; 119 | padding: 0; 120 | margin: 0 3px 0 0; 121 | width: 20px; 122 | height: 20px; 123 | svg { 124 | width: 14px; 125 | } 126 | ${({ collapsed }) => 127 | collapsed && 128 | css` 129 | transform: rotate(-90deg); 130 | `} 131 | `; 132 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | 4 | import Zethus from './zethus'; 5 | import { GlobalStyle } from './components/styled'; 6 | 7 | ReactDOM.render( 8 | <> 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ); 14 | -------------------------------------------------------------------------------- /src/panels/addModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import { TabsButton, TabsHeader } from '../../components/styled'; 5 | import { 6 | ModalWrapper, 7 | ModalContents, 8 | ModalTitle, 9 | } from '../../components/styled/modal'; 10 | import TabVizType from './tabVizType'; 11 | import TabTopicName from './tabTopicName'; 12 | import SelectedVizOptionsForm from './options'; 13 | import { stopPropagation } from '../../utils'; 14 | 15 | const tabs = { 16 | vizType: 'Visualization type', 17 | topicName: 'Topic name', 18 | }; 19 | 20 | class AddModal extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | tabType: tabs.vizType, 25 | selectedViz: '', 26 | }; 27 | 28 | this.updateTab = this.updateTab.bind(this); 29 | this.selectViz = this.selectViz.bind(this); 30 | } 31 | 32 | updateTab(tabType) { 33 | this.setState({ 34 | tabType, 35 | }); 36 | } 37 | 38 | selectViz(vizType, topicName, messageType) { 39 | this.setState({ 40 | selectedViz: vizType 41 | ? { 42 | vizType, 43 | topicName, 44 | messageType, 45 | } 46 | : '', 47 | }); 48 | } 49 | 50 | render() { 51 | const { 52 | addVisualization, 53 | closeModal, 54 | ros, 55 | rosParams, 56 | rosTopics, 57 | } = this.props; 58 | const { selectedViz, tabType } = this.state; 59 | return ( 60 | 61 | 62 | Add Visualization 63 | {selectedViz ? ( 64 | this.selectViz(null)} 69 | /> 70 | ) : ( 71 | <> 72 | 73 | {_.map(tabs, tabText => ( 74 | this.updateTab(tabText)} 79 | > 80 | {tabText} 81 | 82 | ))} 83 | 84 | {tabType === tabs.vizType ? ( 85 | 91 | ) : ( 92 | 97 | )} 98 | 99 | )} 100 | 101 | 102 | ); 103 | } 104 | } 105 | 106 | export default AddModal; 107 | -------------------------------------------------------------------------------- /src/panels/addModal/options.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import RobotModelOptions from './optionsRobotModel'; 4 | import GenericOptions from './optionsGeneric'; 5 | 6 | const { VIZ_TYPE_ROBOTMODEL } = CONSTANTS; 7 | 8 | class SelectedVizOptionsForm extends React.PureComponent { 9 | render() { 10 | const { 11 | addVisualization, 12 | back, 13 | ros, 14 | selectedViz: { vizType: type }, 15 | selectedViz, 16 | } = this.props; 17 | if (type === VIZ_TYPE_ROBOTMODEL) { 18 | return ( 19 | 25 | ); 26 | } 27 | return ( 28 | 33 | ); 34 | } 35 | } 36 | 37 | export default SelectedVizOptionsForm; 38 | -------------------------------------------------------------------------------- /src/panels/addModal/optionsGeneric.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ButtonPrimary, 4 | FlexGrow, 5 | Input, 6 | InputLabel, 7 | InputWrapper, 8 | } from '../../components/styled'; 9 | import { ModalActions } from '../../components/styled/modal'; 10 | 11 | class GenericOptions extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | name: props.selectedViz.vizType, 16 | }; 17 | this.onSubmit = this.onSubmit.bind(this); 18 | this.updateName = this.updateName.bind(this); 19 | } 20 | 21 | updateName(e) { 22 | this.setState({ 23 | name: e.target.value, 24 | }); 25 | } 26 | 27 | onSubmit(e) { 28 | e.preventDefault(); 29 | const { addVisualization, selectedViz } = this.props; 30 | const { name } = this.state; 31 | addVisualization({ 32 | ...selectedViz, 33 | name, 34 | visible: true, 35 | }); 36 | } 37 | 38 | render() { 39 | const { name } = this.state; 40 | const { back } = this.props; 41 | return ( 42 |
43 | 44 | Visualization name 45 | 46 | 47 | 48 | 49 | 50 | Add visualization 51 | 52 | Back 53 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | export default GenericOptions; 61 | -------------------------------------------------------------------------------- /src/panels/addModal/optionsRobotModel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Amphion from 'amphion'; 3 | import _ from 'lodash'; 4 | import { 5 | Button, 6 | ButtonPrimary, 7 | FlexGrow, 8 | Input, 9 | InputLabel, 10 | InputWrapper, 11 | Paragraph, 12 | } from '../../components/styled'; 13 | import { ModalActions, TypeHeading } from '../../components/styled/modal'; 14 | 15 | const statuses = { 16 | loading: 0, 17 | loaded: 1, 18 | error: 2, 19 | }; 20 | 21 | class RobotModelOptions extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | name: props.selectedViz.vizType, 26 | packages: {}, 27 | status: statuses.loading, 28 | }; 29 | this.onSubmit = this.onSubmit.bind(this); 30 | this.getPackages = this.getPackages.bind(this); 31 | this.updateName = this.updateName.bind(this); 32 | this.updatePackage = this.updatePackage.bind(this); 33 | } 34 | 35 | componentDidMount() { 36 | this.getPackages(); 37 | } 38 | 39 | updateName(e) { 40 | this.setState({ 41 | name: e.target.value, 42 | }); 43 | } 44 | 45 | getURLEndpoint() { 46 | const urlSearchParams = new URLSearchParams(window.location.search); 47 | const urlParams = Object.fromEntries(urlSearchParams.entries()); 48 | return urlParams.pkgs || `http://localhost:9090/ros/pkgs`; 49 | } 50 | 51 | getPackages() { 52 | const { 53 | ros, 54 | selectedViz: { topicName }, 55 | } = this.props; 56 | try { 57 | const robotInstance = new Amphion.RobotModel(ros, topicName); 58 | robotInstance.getPackages(packages => { 59 | this.setState({ 60 | packages: _.mapValues( 61 | _.keyBy(packages), 62 | p => `${this.getURLEndpoint()}/${p}`, 63 | ), 64 | status: statuses.loaded, 65 | }); 66 | }); 67 | } catch (e) { 68 | this.setState({ 69 | status: statuses.error, 70 | }); 71 | } 72 | } 73 | 74 | updatePackage(e) { 75 | const { 76 | dataset: { id: packageId }, 77 | value, 78 | } = e.target; 79 | const { packages } = this.state; 80 | this.setState({ 81 | packages: { 82 | ...packages, 83 | [packageId]: value, 84 | }, 85 | }); 86 | } 87 | 88 | onSubmit(e) { 89 | e.preventDefault(); 90 | const { addVisualization, selectedViz } = this.props; 91 | const { name, packages } = this.state; 92 | addVisualization({ 93 | ...selectedViz, 94 | name, 95 | packages, 96 | }); 97 | } 98 | 99 | render() { 100 | const { name, packages, status } = this.state; 101 | const { back } = this.props; 102 | if (status === statuses.loading) { 103 | return Loading list of packages...; 104 | } 105 | if (status === statuses.error) { 106 | return ( 107 | 108 | Error occured while fetching packages. Please{' '} 109 | 110 | 111 | ); 112 | } 113 | return ( 114 |
115 | 116 | Visualization name 117 | 118 | 119 | Packages 120 | {_.map(packages, (path, packageName) => ( 121 | 122 | {packageName} 123 | 129 | 130 | ))} 131 | 132 | 133 | Add Robot model 134 | 135 | Back 136 | 137 | 138 |
139 | ); 140 | } 141 | } 142 | 143 | export default RobotModelOptions; 144 | -------------------------------------------------------------------------------- /src/panels/addModal/tabTopicName.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { vizOptions } from '../../utils/vizOptions'; 4 | import { ButtonPrimary, FlexGrow } from '../../components/styled'; 5 | import { 6 | AddVizForm, 7 | ModalActions, 8 | TopicRow, 9 | TypeContainer, 10 | TypeSelection, 11 | TypeUnsupported, 12 | } from '../../components/styled/modal'; 13 | 14 | class TopicName extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | selectedViz: '', 19 | }; 20 | this.selectViz = this.selectViz.bind(this); 21 | this.onSubmit = this.onSubmit.bind(this); 22 | } 23 | 24 | selectViz(vizType, topicName, messageType) { 25 | this.setState({ 26 | selectedViz: { 27 | vizType, 28 | topicName, 29 | messageType, 30 | }, 31 | }); 32 | } 33 | 34 | onSubmit(e) { 35 | e.preventDefault(); 36 | const { selectViz } = this.props; 37 | const { 38 | selectedViz: { messageType, topicName, vizType }, 39 | } = this.state; 40 | selectViz(vizType, topicName, messageType); 41 | } 42 | 43 | render() { 44 | const { closeModal, rosTopics } = this.props; 45 | const { selectedViz } = this.state; 46 | return ( 47 | 48 | 49 | 50 | {_.map(_.sortBy(rosTopics, 'name'), ({ name, messageType }) => { 51 | const vizOption = _.find(vizOptions, v => 52 | _.includes(v.messageTypes, messageType), 53 | ); 54 | return vizOption ? ( 55 | 60 | this.selectViz(vizOption.type, name, messageType) 61 | } 62 | > 63 | {name} 64 | ({messageType}) 65 | 66 | ) : ( 67 | 68 | {name} 69 | 70 | (Unsupported type: {messageType}) 71 | 72 | ); 73 | })} 74 | 75 | 76 | 77 | 78 | 79 | Proceed 80 | 81 | 82 | Close 83 | 84 | 85 | 86 | ); 87 | } 88 | } 89 | 90 | export default TopicName; 91 | -------------------------------------------------------------------------------- /src/panels/addModal/tabVizType.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { vizOptions } from '../../utils/vizOptions'; 4 | import VizTypeItem from './vizTypeItem'; 5 | import VizTypeDetails from './vizTypeDetails'; 6 | import { ButtonPrimary, FlexGrow } from '../../components/styled'; 7 | import { 8 | AddVizForm, 9 | ModalActions, 10 | TypeContainer, 11 | TypeInfo, 12 | TypeSelection, 13 | } from '../../components/styled/modal'; 14 | 15 | class VizType extends React.PureComponent { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | selectedViz: '', 20 | }; 21 | this.selectViz = this.selectViz.bind(this); 22 | this.onSubmit = this.onSubmit.bind(this); 23 | } 24 | 25 | selectViz(vizType, topicName, messageType) { 26 | this.setState({ 27 | selectedViz: { 28 | vizType, 29 | topicName, 30 | messageType, 31 | }, 32 | }); 33 | } 34 | 35 | onSubmit(e) { 36 | e.preventDefault(); 37 | const { selectViz } = this.props; 38 | const { 39 | selectedViz: { messageType, topicName, vizType }, 40 | } = this.state; 41 | selectViz(vizType, topicName, messageType); 42 | } 43 | 44 | render() { 45 | const { selectedViz } = this.state; 46 | const { closeModal, rosParams, rosTopics } = this.props; 47 | return ( 48 | 49 | 50 | 51 | {_.map(vizOptions, op => { 52 | return ( 53 | 60 | _.includes(op.messageTypes, t.messageType), 61 | )} 62 | /> 63 | ); 64 | })} 65 | 66 | 67 | {selectedViz ? ( 68 | 69 | ) : ( 70 |

71 | No visualization selected. 72 |
73 | Please choose a visualization on the left to see details 74 |

75 | )} 76 |
77 |
78 | 79 | 80 | 81 | Proceed 82 | 83 | 84 | Close 85 | 86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | export default VizType; 93 | -------------------------------------------------------------------------------- /src/panels/addModal/vizTypeDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import ReactMarkdown from 'react-markdown'; 4 | 5 | import { vizOptions } from '../../utils/vizOptions'; 6 | import { Anchor } from '../../components/styled'; 7 | 8 | class VizTypeDetails extends React.PureComponent { 9 | render() { 10 | const { vizType } = this.props; 11 | const { description, docsLink, type } = _.find( 12 | vizOptions, 13 | v => v.type === vizType, 14 | ); 15 | return ( 16 | <> 17 |

{type}

18 | 19 | View docs 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default VizTypeDetails; 26 | -------------------------------------------------------------------------------- /src/panels/addModal/vizTypeItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import isValidUrl from 'is-valid-http-url'; 4 | import { CONSTANTS } from 'amphion'; 5 | import Select from 'react-select'; 6 | import { TypeEmpty, TypeHeading, TypeRow } from '../../components/styled/modal'; 7 | import { VizItemIcon } from '../../components/styled/viz'; 8 | import { 9 | VIZ_TYPE_DEPTHCLOUD_STREAM, 10 | VIZ_TYPE_IMAGE_STREAM, 11 | } from '../../utils/vizOptions'; 12 | import { Input } from '../../components/styled'; 13 | 14 | const { VIZ_TYPE_ROBOTMODEL } = CONSTANTS; 15 | 16 | const customStyles = { 17 | container: provided => ({ 18 | ...provided, 19 | fontSize: '16px', 20 | width: '250px', 21 | }), 22 | control: provided => ({ 23 | ...provided, 24 | border: '1px solid #dddddd', 25 | }), 26 | }; 27 | 28 | class VizTypeItem extends React.PureComponent { 29 | orderTopics() {} 30 | 31 | render() { 32 | const { 33 | rosParams, 34 | selectedViz, 35 | selectViz, 36 | topics, 37 | vizDetails, 38 | } = this.props; 39 | const topicName = _.get(selectedViz, 'topicName'); 40 | const isRobotmodel = _.get(selectedViz, 'vizType') === VIZ_TYPE_ROBOTMODEL; 41 | const isAdditionalTypeSelected = { 42 | [VIZ_TYPE_ROBOTMODEL]: 43 | _.get(selectedViz, 'vizType') === VIZ_TYPE_ROBOTMODEL, 44 | [VIZ_TYPE_DEPTHCLOUD_STREAM]: 45 | _.get(selectedViz, 'vizType') === VIZ_TYPE_DEPTHCLOUD_STREAM, 46 | [VIZ_TYPE_IMAGE_STREAM]: 47 | _.get(selectedViz, 'vizType') === VIZ_TYPE_IMAGE_STREAM, 48 | }; 49 | if (vizDetails.type === VIZ_TYPE_ROBOTMODEL) { 50 | const params = rosParams.filter(param => 51 | param.includes('robot_description'), 52 | ); 53 | params.sort((a, b) => a.length - b.length); 54 | return ( 55 |
56 | 57 | {vizDetails.icon} 58 | {vizDetails.type} 59 | 60 | 64 | { 102 | if (!isValidUrl(e.target.value)) { 103 | return; 104 | } 105 | selectViz(vizDetails.type, e.target.value, ''); 106 | }} 107 | /> 108 | 109 |
110 | ); 111 | } 112 | return ( 113 |
114 | 115 | {vizDetails.icon} 116 | {vizDetails.type} 117 | 118 | {_.map(topics, topic => ( 119 | 124 | selectViz(vizDetails.type, topic.name, topic.messageType) 125 | } 126 | > 127 | {topic.name} 128 | 129 | ))} 130 | {_.size(topics) === 0 && ( 131 | No topics available for the visualization type 132 | )} 133 |
134 | ); 135 | } 136 | } 137 | 138 | export default VizTypeItem; 139 | -------------------------------------------------------------------------------- /src/panels/configurationModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { JsonEditor as Editor } from 'jsoneditor-react'; 4 | import 'jsoneditor-react/es/editor.min.css'; 5 | import ace from 'brace'; 6 | import 'brace/mode/json'; 7 | import 'brace/theme/xcode'; 8 | 9 | import { 10 | ButtonPrimary, 11 | ButtonOutline, 12 | FlexGrow, 13 | } from '../../components/styled'; 14 | 15 | import { 16 | ModalActions, 17 | ModalWrapper, 18 | ModalContents, 19 | ModalTitle, 20 | } from '../../components/styled/modal'; 21 | import { COLOR_GREY_LIGHT_1 } from '../../components/styled/constants'; 22 | import { downloadFile, stopPropagation } from '../../utils'; 23 | 24 | const StyledEditor = styled.div` 25 | flex-grow: 1; 26 | flex-shrink: 1; 27 | background-color: ${COLOR_GREY_LIGHT_1}; 28 | .jsoneditor { 29 | border: 0; 30 | border-radius: 4px; 31 | } 32 | .ace_content { 33 | background-color: ${COLOR_GREY_LIGHT_1}; 34 | } 35 | .ace_editor { 36 | font-family: 'Source Code Pro', sans-serif; 37 | } 38 | .jsoneditor-text { 39 | background-color: ${COLOR_GREY_LIGHT_1}; 40 | } 41 | `; 42 | 43 | const EditorWrapper = styled.div` 44 | display: flex; 45 | flex-direction: column; 46 | flex-grow: 1; 47 | flex-shrink: 1; 48 | position: relative; 49 | `; 50 | 51 | const DragHoverText = styled.div` 52 | position: absolute; 53 | top: 0; 54 | bottom: 0; 55 | left: 0; 56 | right: 0; 57 | background-color: rgba(255, 255, 255, 0.7); 58 | z-index: 1010; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | > p { 63 | text-align: center; 64 | } 65 | `; 66 | 67 | const overrideEventDefaults = event => { 68 | event.preventDefault(); 69 | event.stopPropagation(); 70 | }; 71 | 72 | class ConfigurationModal extends React.Component { 73 | constructor(props) { 74 | super(props); 75 | this.state = { 76 | dragging: false, 77 | }; 78 | this.dragEventCounter = 0; 79 | this.jsonEditor = null; 80 | 81 | this.downloadConfig = this.downloadConfig.bind(this); 82 | this.handleSubmit = this.handleSubmit.bind(this); 83 | this.dragenterListener = this.dragenterListener.bind(this); 84 | this.dragleaveListener = this.dragleaveListener.bind(this); 85 | this.dropListener = this.dropListener.bind(this); 86 | this.setEditorRef = this.setEditorRef.bind(this); 87 | } 88 | 89 | dragenterListener(event) { 90 | overrideEventDefaults(event); 91 | this.dragEventCounter++; 92 | if (event.dataTransfer.items && event.dataTransfer.items[0]) { 93 | this.setState({ dragging: true }); 94 | } 95 | } 96 | 97 | dragleaveListener(event) { 98 | overrideEventDefaults(event); 99 | this.dragEventCounter--; 100 | 101 | if (this.dragEventCounter === 0) { 102 | this.setState({ dragging: false }); 103 | } 104 | } 105 | 106 | dropListener(event) { 107 | overrideEventDefaults(event); 108 | this.dragEventCounter = 0; 109 | this.setState({ dragging: false }); 110 | 111 | if (event.dataTransfer.files && event.dataTransfer.files[0]) { 112 | const f = event.dataTransfer.files[0]; 113 | const reader = new FileReader(); 114 | 115 | reader.onload = e => { 116 | try { 117 | const config = JSON.parse(e.target.result); 118 | this.jsonEditor.update(config); 119 | } catch (error) { 120 | // TODO: Add notifications. Show notification for invalid json 121 | console.log(error); 122 | } 123 | }; 124 | reader.readAsText(f); 125 | } 126 | } 127 | 128 | componentDidMount() { 129 | window.addEventListener('dragover', event => { 130 | overrideEventDefaults(event); 131 | }); 132 | window.addEventListener('drop', event => { 133 | overrideEventDefaults(event); 134 | }); 135 | } 136 | 137 | componentWillUnmount() { 138 | window.removeEventListener('dragover', overrideEventDefaults); 139 | window.removeEventListener('drop', overrideEventDefaults); 140 | } 141 | 142 | downloadConfig() { 143 | const config = this.jsonEditor.get(); 144 | downloadFile(JSON.stringify(config, null, 2), 'zethus_config.json'); 145 | } 146 | 147 | handleSubmit() { 148 | const { updateConfiguration } = this.props; 149 | const config = this.jsonEditor.get(); 150 | updateConfiguration(config); 151 | } 152 | 153 | setEditorRef(instance) { 154 | if (instance) { 155 | const { jsonEditor } = instance; 156 | this.jsonEditor = jsonEditor; 157 | } else { 158 | this.jsonEditor = null; 159 | } 160 | } 161 | 162 | render() { 163 | const { closeModal, configuration } = this.props; 164 | const { dragging } = this.state; 165 | 166 | return ( 167 | 168 | 169 | Edit Configuration 170 | 179 | 189 | {dragging && ( 190 | 191 |

Drag & drop Configuration JSON file

192 |
193 | )} 194 |
195 | 196 | 197 | Download 198 | 199 | 200 | 201 | Update Configuration 202 | 203 | Cancel 204 | 205 |
206 |
207 | ); 208 | } 209 | } 210 | 211 | export default ConfigurationModal; 212 | -------------------------------------------------------------------------------- /src/panels/graphVisualizationModal/Tree.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import createAndPopulateGraph, { reposition } from './utils'; 3 | import { defaultGraph, graphWithTopicNodes } from '../../utils'; 4 | import { NODE_SELECT_VALUES } from './constants'; 5 | 6 | class Tree extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.graphBasedOnOptions = this.graphBasedOnOptions.bind(this); 10 | this.handleGraphResize = this.handleGraphResize.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.graphBasedOnOptions(); 15 | window.addEventListener('resize', this.handleGraphResize); 16 | } 17 | 18 | handleGraphResize() { 19 | reposition(this.graph); 20 | } 21 | 22 | graphBasedOnOptions() { 23 | const { graph, nodeSelect } = this.props; 24 | let newGraph = null; 25 | switch (nodeSelect.value) { 26 | case NODE_SELECT_VALUES.NODES_ONLY: { 27 | newGraph = defaultGraph(graph); 28 | break; 29 | } 30 | case NODE_SELECT_VALUES.NODES_AND_TOPICS: { 31 | newGraph = graphWithTopicNodes(graph); 32 | break; 33 | } 34 | } 35 | this.graph = createAndPopulateGraph(newGraph, 'graph'); 36 | } 37 | 38 | componentDidUpdate(prevProps) { 39 | const { graph, nodeSelect } = this.props; 40 | if ( 41 | JSON.stringify(prevProps.graph) !== JSON.stringify(graph) || 42 | JSON.stringify(prevProps.nodeSelect) !== JSON.stringify(nodeSelect) 43 | ) { 44 | this.graphBasedOnOptions(); 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default Tree; 58 | -------------------------------------------------------------------------------- /src/panels/graphVisualizationModal/constants.js: -------------------------------------------------------------------------------- 1 | export const NODE_SELECT_VALUES = { 2 | NODES_ONLY: 0, 3 | NODES_AND_TOPICS: 1, 4 | }; 5 | 6 | export const NODE_OPTIONS = [ 7 | { 8 | value: NODE_SELECT_VALUES.NODES_ONLY, 9 | label: 'Nodes only', 10 | }, 11 | { 12 | value: NODE_SELECT_VALUES.NODES_AND_TOPICS, 13 | label: 'Node/Topics (all)', 14 | }, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/panels/graphVisualizationModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Tree from './Tree'; 4 | 5 | import { ButtonPrimary } from '../../components/styled'; 6 | import { 7 | ModalWrapper, 8 | ModalContents, 9 | ModalTitle, 10 | } from '../../components/styled/modal'; 11 | import { 12 | stopPropagation, 13 | generateGraph, 14 | ROS_SOCKET_STATUSES, 15 | } from '../../utils'; 16 | import API_CALL_STATUS from '../../utils/constants'; 17 | import VisualizationHelperToolbar from './visualizationToolbar'; 18 | import { NODE_OPTIONS } from './constants'; 19 | 20 | const GraphContainer = styled.div` 21 | border: 1px solid red; 22 | display: flex; 23 | height: 90%; 24 | justify-content: center; 25 | align-items: center; 26 | position: relative; 27 | overflow: hidden; 28 | & > svg { 29 | width: 100%; 30 | height: 100%; 31 | z-index: 10 !important; 32 | } 33 | 34 | .node rect, 35 | .node circle, 36 | .node ellipse { 37 | stroke: #333; 38 | fill: #fff; 39 | stroke-width: 1px; 40 | } 41 | .edgePath path { 42 | stroke: #333; 43 | fill: #333; 44 | stroke-width: 1.5px; 45 | } 46 | .root-node { 47 | color: white; 48 | } 49 | 50 | /* This styles the title of the tooltip */ 51 | .name { 52 | font-size: 1.5em; 53 | font-weight: bold; 54 | color: #60b1fc; 55 | margin: 0; 56 | } 57 | 58 | /* This styles the body of the tooltip */ 59 | .description { 60 | font-size: 1.2em; 61 | } 62 | `; 63 | 64 | const ModalHeading = styled.div` 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: center; 68 | `; 69 | 70 | const StyledModalContents = styled(ModalContents)` 71 | height: 90%; 72 | width: 90%; 73 | margin: auto; 74 | margin-top: 5vh; 75 | `; 76 | 77 | class ConfigurationModal extends React.Component { 78 | constructor(props) { 79 | super(props); 80 | this.state = { 81 | graph: null, 82 | status: API_CALL_STATUS.FETCHING, 83 | visualizationToolbarSettings: { 84 | debug: true, 85 | nodeSelect: NODE_OPTIONS[0], 86 | }, 87 | }; 88 | this.graphContainerRef = React.createRef(); 89 | this.handleSubmit = this.handleSubmit.bind(this); 90 | this.createGraph = this.createGraph.bind(this); 91 | this.refreshGraph = this.refreshGraph.bind(this); 92 | this.returnContainerRef = this.returnContainerRef.bind(this); 93 | this.changeVisualizationToolbar = this.changeVisualizationToolbar.bind( 94 | this, 95 | ); 96 | this.selectHandler = this.selectHandler.bind(this); 97 | } 98 | 99 | createGraph() { 100 | const { ros } = this.props; 101 | const p = generateGraph(ros); 102 | p.then(graph => { 103 | this.setState({ graph, status: API_CALL_STATUS.SUCCESSFUL }); 104 | }).catch(err => { 105 | console.log(err); 106 | this.setState({ 107 | status: API_CALL_STATUS.ERROR, 108 | }); 109 | }); 110 | } 111 | 112 | selectHandler(selectedOption) { 113 | this.setState(function({ visualizationToolbarSettings }) { 114 | return { 115 | visualizationToolbarSettings: { 116 | ...visualizationToolbarSettings, 117 | nodeSelect: selectedOption, 118 | }, 119 | }; 120 | }); 121 | } 122 | 123 | changeVisualizationToolbar(e) { 124 | const { 125 | checked, 126 | dataset: { id }, 127 | } = e.target; 128 | this.setState(function({ visualizationToolbarSettings }) { 129 | return { 130 | visualizationToolbarSettings: { 131 | ...visualizationToolbarSettings, 132 | [id]: checked, 133 | }, 134 | }; 135 | }); 136 | } 137 | 138 | refreshGraph(e) { 139 | e.preventDefault(); 140 | this.createGraph(); 141 | } 142 | 143 | returnContainerRef() { 144 | return this.graphContainerRef; 145 | } 146 | 147 | componentDidMount() { 148 | this.createGraph(); 149 | } 150 | 151 | handleSubmit() { 152 | const { updateConfiguration } = this.props; 153 | const config = this.jsonEditor.get(); 154 | updateConfiguration(config); 155 | } 156 | 157 | render() { 158 | const { closeModal, rosStatus } = this.props; 159 | const { 160 | graph, 161 | status, 162 | visualizationToolbarSettings: { debug, nodeSelect }, 163 | } = this.state; 164 | 165 | let data = null; 166 | if (status === API_CALL_STATUS.SUCCESSFUL) { 167 | data = ( 168 | 174 | ); 175 | } else if (status === API_CALL_STATUS.ERROR) { 176 | data = ( 177 |

178 | Error{' '} 179 | Refresh 180 |

181 | ); 182 | } else { 183 | data =

Loading.

; 184 | } 185 | return ( 186 | 187 | 188 | 189 | Graph 190 | 194 | {rosStatus === ROS_SOCKET_STATUSES.CONNECTED 195 | ? 'Refresh' 196 | : 'Websocket disconnected.'} 197 | 198 | 199 | 205 | {data} 206 | 207 | 208 | ); 209 | } 210 | } 211 | 212 | export default ConfigurationModal; 213 | -------------------------------------------------------------------------------- /src/panels/graphVisualizationModal/utils.js: -------------------------------------------------------------------------------- 1 | import DagreD3 from 'dagre-d3'; 2 | import * as d3 from 'd3'; 3 | 4 | // NOTE: Make into a class/pure functions. 5 | 6 | function createAndPopulateGraph(graph, targetElementId) { 7 | const g = new DagreD3.graphlib.Graph().setGraph({ 8 | rankdir: 'LR', 9 | }); 10 | const initialScale = 0.75; 11 | const svg = d3.select(`#${targetElementId}`); 12 | const inner = svg.select('g'); 13 | // Create the renderer 14 | /* eslint-disable-next-line */ 15 | const render = new DagreD3.render(); 16 | 17 | // Set up zoom support 18 | const zoom = d3.zoom().on('zoom', function() { 19 | inner.attr('transform', d3.event.transform); 20 | }); 21 | svg.call(zoom); 22 | graph.nodes.forEach(function(node) { 23 | g.setNode(node.id, { label: node.label, shape: node.type }); 24 | }); 25 | 26 | graph.edges.forEach(function(edge) { 27 | g.setEdge(edge.source.id, edge.target.id, { label: edge.value }); 28 | }); 29 | 30 | // Set some general styles 31 | g.nodes().forEach(function(v) { 32 | const node = g.node(v); 33 | node.rx = 5; 34 | node.ry = 5; 35 | }); 36 | // Run the renderer. This is what draws the final graph. 37 | render(inner, g); 38 | svg.call( 39 | zoom.transform, 40 | d3.zoomIdentity 41 | .translate( 42 | (svg.attr('width') - g.graph().width * initialScale) / 2 + 43 | Number(svg.style('width').slice(0, -2)) / 2, 44 | Number(svg.style('height').slice(0, -2)) / 2 - 45 | (g.graph().height * initialScale) / 2, 46 | ) 47 | .scale(initialScale), 48 | ); 49 | svg.attr('height', g.graph().height * initialScale + 40); 50 | 51 | return { 52 | svg, 53 | g, 54 | initialScale, 55 | zoom, 56 | }; 57 | } 58 | 59 | export function reposition({ svg, zoom, g, initialScale }) { 60 | svg.call( 61 | zoom.transform, 62 | d3.zoomIdentity 63 | .translate( 64 | (svg.attr('width') - g.graph().width * initialScale) / 2 + 65 | Number(svg.style('width').slice(0, -2)) / 2, 66 | Number(svg.style('height').slice(0, -2)) / 2 - 67 | (g.graph().height * initialScale) / 2, 68 | ) 69 | .scale(initialScale), 70 | ); 71 | svg.attr('height', g.graph().height * initialScale + 40); 72 | } 73 | 74 | export default createAndPopulateGraph; 75 | -------------------------------------------------------------------------------- /src/panels/graphVisualizationModal/visualizationToolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import Select from 'react-select'; 5 | 6 | import { Container } from '../../components/styled'; 7 | import { NODE_OPTIONS } from './constants'; 8 | 9 | const SelectStyled = styled(Select)` 10 | width: 200px !important; 11 | z-index: 101; 12 | `; 13 | 14 | const Wrapper = styled(Container)` 15 | padding-left: 0; 16 | `; 17 | 18 | function visualizationToolbar({ 19 | changeVisualizationToolbar, 20 | debug, 21 | selectHandler, 22 | nodeSelect, 23 | }) { 24 | return ( 25 | 26 | 33 | 34 | {/* Could be included in a different pr with all filtering options. */} 35 | {/* */} 46 | 47 | ); 48 | } 49 | 50 | visualizationToolbar.propTypes = { 51 | debug: PropTypes.bool, 52 | }; 53 | 54 | visualizationToolbar.defaultProps = { 55 | debug: true, 56 | }; 57 | 58 | export default visualizationToolbar; 59 | -------------------------------------------------------------------------------- /src/panels/header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyledHeader, StyledLogo } from '../../components/styled'; 3 | import Logo from '../../components/logo'; 4 | import Toolbar from './toolbar'; 5 | 6 | export default class Header extends React.PureComponent { 7 | render() { 8 | const { activeTool, selectTool } = this.props; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/panels/header/tool.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyledTool, ToolHeading } from '../../components/styled'; 3 | 4 | export default class Tool extends React.PureComponent { 5 | render() { 6 | const { active, data, selectTool } = this.props; 7 | const { icon, name, type } = data; 8 | return ( 9 | selectTool(name, type)}> 10 | {icon(active)} {name} 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/panels/header/toolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { map } from 'lodash'; 3 | import { StyledToolbar } from '../../components/styled'; 4 | import Tool from './tool'; 5 | import { toolOptions } from '../../utils/toolbar'; 6 | 7 | export default class Toolbar extends React.PureComponent { 8 | render() { 9 | const { activeTool, selectTool } = this.props; 10 | return ( 11 | 12 | {map(toolOptions, option => ( 13 | 19 | ))} 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/panels/info/addInfoPanelModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { filter, get, isNil, map, size } from 'lodash'; 3 | import TagsInput from 'react-tagsinput'; 4 | import { 5 | AddInfoPanelModalTopics, 6 | ButtonPrimary, 7 | FlexGrow, 8 | } from '../../components/styled'; 9 | 10 | import { 11 | ModalActions, 12 | ModalContents, 13 | ModalTitle, 14 | ModalWrapper, 15 | TopicRow, 16 | } from '../../components/styled/modal'; 17 | import { stopPropagation } from '../../utils'; 18 | 19 | class AddInfoPanelModal extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | selected: null, 24 | keys: [], 25 | }; 26 | 27 | this.selectTopic = this.selectTopic.bind(this); 28 | this.closeModal = this.closeModal.bind(this); 29 | this.addInfoPanel = this.addInfoPanel.bind(this); 30 | this.handleFilterKeysChange = this.handleFilterKeysChange.bind(this); 31 | } 32 | 33 | handleFilterKeysChange(keys) { 34 | this.setState({ keys }); 35 | } 36 | 37 | selectTopic(selected) { 38 | this.setState({ selected }); 39 | } 40 | 41 | closeModal() { 42 | const { closeModal } = this.props; 43 | this.setState({ selected: null, keys: [] }, () => { 44 | closeModal(); 45 | }); 46 | } 47 | 48 | addInfoPanel(topic, keys) { 49 | const { closeModal, onAdd } = this.props; 50 | this.setState({ selected: null, keys: [] }, () => { 51 | onAdd(topic, keys); 52 | closeModal(); 53 | }); 54 | } 55 | 56 | render() { 57 | const { allTopics, open, topics } = this.props; 58 | const { keys, selected } = this.state; 59 | 60 | const topicsNamesSet = new Set(map(topics, topic => topic.name)); 61 | const filteredTopics = filter( 62 | allTopics, 63 | topic => !topicsNamesSet.has(topic.name), 64 | ); 65 | 66 | return open ? ( 67 | 68 | 69 | Add Info Panel 70 | 71 | {size(allTopics) === 0 72 | ? 'No topics available' 73 | : map(filteredTopics, ({ name, messageType }) => ( 74 | { 79 | this.selectTopic({ name, messageType }); 80 | }} 81 | > 82 | {name} 83 | ({messageType}) 84 | 85 | ))} 86 | 87 | 88 | 96 | 97 | 98 | 99 | this.addInfoPanel(selected, keys)} 102 | > 103 | Confirm 104 | 105 | Cancel 106 | 107 | 108 | 109 | ) : null; 110 | } 111 | } 112 | 113 | export default AddInfoPanelModal; 114 | -------------------------------------------------------------------------------- /src/panels/info/content.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isNil, omit, size, map } from 'lodash'; 3 | import FormattedContent from './formattedContent'; 4 | import { FilteredKeys, InfoPanelNoMessage } from '../../components/styled'; 5 | import RawContent from './rawContent'; 6 | 7 | const CONTENT_MANUAL_UPDATE_RATE = 500; 8 | 9 | class Content extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.forceUpdateId = null; 13 | } 14 | 15 | componentDidMount() { 16 | this.forceUpdateId = setInterval(() => { 17 | this.forceUpdate(); 18 | }, CONTENT_MANUAL_UPDATE_RATE); 19 | } 20 | 21 | componentWillUnmount() { 22 | clearInterval(this.forceUpdateId); 23 | } 24 | 25 | shouldComponentUpdate(nextProps) { 26 | const { raw, selected } = this.props; 27 | return nextProps.selected !== selected || nextProps.raw !== raw; 28 | } 29 | 30 | render() { 31 | const { messageBuffers, openAddInfoPanel, raw, selected } = this.props; 32 | 33 | const lastMessage = 34 | size(messageBuffers[selected.name]) === 0 35 | ? null 36 | : messageBuffers[selected.name][0]; 37 | 38 | if (isNil(selected.name)) { 39 | return ( 40 | 41 | Add an info panel to receive messages. 42 | 43 | ); 44 | } 45 | 46 | if (size(messageBuffers[selected.name]) === 0) { 47 | return waiting for messages...; 48 | } 49 | 50 | const bufferClone = raw ? [...messageBuffers[selected.name]] : []; 51 | return ( 52 | <> 53 | {size(selected.keys) > 0 && ( 54 | 55 | {map(selected.keys, key => ( 56 | {key} 57 | ))} 58 | 59 | )} 60 | {raw ? ( 61 | 62 | ) : ( 63 | 64 | )} 65 | 66 | ); 67 | } 68 | } 69 | 70 | export default Content; 71 | -------------------------------------------------------------------------------- /src/panels/info/formattedContent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { JsonEditor } from 'jsoneditor-react'; 3 | import { isNil } from 'lodash'; 4 | 5 | class FormattedContent extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.jsonEditor = null; 10 | this.setRef = this.setRef.bind(this); 11 | } 12 | 13 | setRef(instance) { 14 | if (instance) { 15 | const { jsonEditor } = instance; 16 | this.jsonEditor = jsonEditor; 17 | } 18 | } 19 | 20 | componentDidUpdate(prevProps) { 21 | const { message } = this.props; 22 | if (!isNil(message) && prevProps.message !== message && this.jsonEditor) { 23 | this.jsonEditor.update(message); 24 | } 25 | } 26 | 27 | render() { 28 | const { message } = this.props; 29 | if (isNil(message)) { 30 | return null; 31 | } 32 | return ( 33 | 42 | ); 43 | } 44 | } 45 | 46 | export default FormattedContent; 47 | -------------------------------------------------------------------------------- /src/panels/info/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ROSLIB from 'roslib'; 3 | import { 4 | find, 5 | findIndex, 6 | forEach, 7 | get, 8 | isEqual, 9 | isNil, 10 | map, 11 | size, 12 | } from 'lodash'; 13 | import { 14 | InfoPanel, 15 | InfoPanelAddButton, 16 | InfoPanelContentWrapper, 17 | InfoPanelHeader, 18 | InfoPanelHeaderControls, 19 | InfoPanelNoMessage, 20 | InfoPanelTab, 21 | InfoPanelTabsWrapper, 22 | } from '../../components/styled'; 23 | import Content from './content'; 24 | import { sanitizeMessage } from '../../utils/sanitize'; 25 | import AddInfoPanelModal from './addInfoPanelModal'; 26 | 27 | const MESSAGE_BUFFER_MAX_LENGTH = 1000; 28 | const compressionTypes = new Set([ 29 | 'sensor_msgs/Image', 30 | 'sensor_msgs/PointCloud2', 31 | 'sensor_msgs/PointCloud', 32 | 'sensor_msgs/LaserScan', 33 | 'nav_msgs/Path', 34 | 'nav_msgs/OccupancyGrid', 35 | 'visualization_msgs/MarkerArray', 36 | 'geometry_msgs/Polygon', 37 | 'geometry_msgs/PolygonStamped', 38 | 'geometry_msgs/PoseArray', 39 | ]); 40 | const getTopicOptions = messageType => { 41 | if (compressionTypes.has(messageType)) { 42 | return { 43 | queue_length: 1, 44 | compression: 'cbor', 45 | }; 46 | } 47 | 48 | return { 49 | queue_length: 1, 50 | }; 51 | }; 52 | 53 | class Info extends React.PureComponent { 54 | constructor(props) { 55 | super(props); 56 | 57 | const { topics } = props; 58 | this.topicInstances = {}; 59 | this.messageBuffers = {}; 60 | 61 | this.state = { 62 | selected: get(topics, '[0]', {}), 63 | raw: false, 64 | addModalOpen: false, 65 | }; 66 | 67 | this.onMessage = this.onMessage.bind(this); 68 | this.onTabChange = this.onTabChange.bind(this); 69 | this.onRawClick = this.onRawClick.bind(this); 70 | this.toggleAddModal = this.toggleAddModal.bind(this); 71 | this.addInfoPanel = this.addInfoPanel.bind(this); 72 | } 73 | 74 | componentDidMount() { 75 | const { ros, topics } = this.props; 76 | 77 | forEach(topics, topic => { 78 | const topicInstance = new ROSLIB.Topic({ 79 | ...topic, 80 | ros, 81 | ...getTopicOptions(topic.messageType), 82 | }); 83 | topicInstance.subscribe(message => this.onMessage(topic, message)); 84 | this.messageBuffers[topic.name] = []; 85 | this.topicInstances[topic.name] = topicInstance; 86 | }); 87 | } 88 | 89 | static getDerivedStateFromProps(nextProps, prevState) { 90 | let { selected } = prevState; 91 | if (isNil(find(nextProps.topics, t => t.name === selected.name))) { 92 | selected = size(nextProps.topics) > 0 ? nextProps.topics[0] : {}; 93 | } 94 | return { 95 | ...prevState, 96 | selected, 97 | topics: nextProps.topics, 98 | }; 99 | } 100 | 101 | componentDidUpdate(prevProps) { 102 | const { ros, topics } = this.props; 103 | 104 | if (isEqual(prevProps.topics, topics)) { 105 | return; 106 | } 107 | 108 | const newTopicNames = new Set(map(topics, t => t.name)); 109 | const oldTopicNames = new Set(map(prevProps.topics, t => t.name)); 110 | 111 | forEach(prevProps.topics, topic => { 112 | if (!newTopicNames.has(topic.name)) { 113 | this.topicInstances[topic.name].unsubscribe(); 114 | delete this.messageBuffers[topic.name]; 115 | delete this.topicInstances[topic.name]; 116 | } 117 | }); 118 | 119 | forEach(topics, topic => { 120 | if (!oldTopicNames.has(topic.name)) { 121 | const topicInstance = new ROSLIB.Topic({ 122 | ...topic, 123 | ros, 124 | ...getTopicOptions(topic.messageType), 125 | }); 126 | topicInstance.subscribe(message => this.onMessage(topic, message)); 127 | this.messageBuffers[topic.name] = []; 128 | this.topicInstances[topic.name] = topicInstance; 129 | } 130 | }); 131 | } 132 | 133 | onMessage(topic, message) { 134 | const { name } = topic; 135 | const buffer = this.messageBuffers[name]; 136 | if (size(buffer) === MESSAGE_BUFFER_MAX_LENGTH) { 137 | buffer.pop(); 138 | } 139 | 140 | const sanitizedMessage = sanitizeMessage(topic, message); 141 | sanitizedMessage.timestamp = performance.now(); 142 | buffer.unshift(sanitizedMessage); 143 | } 144 | 145 | onTabChange(e, topic) { 146 | const { 147 | collapsed, 148 | togglePanelCollapse, 149 | topics, 150 | updateInfoTabs, 151 | } = this.props; 152 | const action = e.target.getAttribute('data-action'); 153 | 154 | if (action === 'close') { 155 | const topicsShallowClone = [...topics]; 156 | const index = findIndex(topicsShallowClone, x => x.name === topic.name); 157 | topicsShallowClone.splice(index, 1); 158 | updateInfoTabs(topicsShallowClone); 159 | } else { 160 | this.setState({ selected: topic }, () => { 161 | if (collapsed) { 162 | togglePanelCollapse('info'); 163 | } 164 | }); 165 | } 166 | } 167 | 168 | onRawClick(e) { 169 | this.setState({ raw: e.target.checked || false }); 170 | } 171 | 172 | toggleAddModal(addModalOpen) { 173 | this.props.refreshRosData(); 174 | this.setState({ addModalOpen }); 175 | } 176 | 177 | addInfoPanel(topic, keys) { 178 | const { 179 | collapsed, 180 | togglePanelCollapse, 181 | topics, 182 | updateInfoTabs, 183 | } = this.props; 184 | const topicsShallowClone = [...topics]; 185 | topic.keys = keys; 186 | topicsShallowClone.push(topic); 187 | if (collapsed) { 188 | togglePanelCollapse('info'); 189 | } 190 | setTimeout(() => updateInfoTabs(topicsShallowClone), 0); 191 | this.toggleAddModal(false); 192 | } 193 | 194 | render() { 195 | const { addModalOpen, raw, selected } = this.state; 196 | const { 197 | collapsed, 198 | rosTopics: allTopics, 199 | toggleGraphModal, 200 | togglePanelCollapse, 201 | topics, 202 | } = this.props; 203 | 204 | return ( 205 | <> 206 | 207 | 208 | 209 | {map(topics, t => ( 210 | this.onTabChange(e, t)} 214 | > 215 | {t.name} 216 | 217 | 218 | ))} 219 | this.toggleAddModal(true)}> 220 | + 221 | 222 | 223 | 224 | 225 | RQT Graph 226 | 227 | 231 | togglePanelCollapse('info')}> 232 | {collapsed ? 'Expand' : 'Collapse'} {collapsed ? '▲' : '▼'} 233 | 234 | 235 | 236 | 237 | this.toggleAddModal(true)} 242 | /> 243 | 244 | 245 | this.toggleAddModal(false)} 250 | onAdd={this.addInfoPanel} 251 | /> 252 | 253 | ); 254 | } 255 | } 256 | 257 | export default Info; 258 | -------------------------------------------------------------------------------- /src/panels/info/rawContent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isNil, omit, size } from 'lodash'; 3 | import { AutoSizer, List } from 'react-virtualized'; 4 | import { RawContentRow, RawContentWrapper } from '../../components/styled'; 5 | 6 | class RawContent extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.rowRenderer = this.rowRenderer.bind(this); 11 | } 12 | 13 | static noRowsRenderer() { 14 | return null; 15 | } 16 | 17 | rowRenderer({ index }) { 18 | const { messages } = this.props; 19 | if (isNil(messages[index])) { 20 | return null; 21 | } 22 | 23 | return ( 24 | 25 | {JSON.stringify(omit(messages[index], ['timestamp']))} 26 | 27 | ); 28 | } 29 | 30 | render() { 31 | const { messages } = this.props; 32 | const rowHeight = 200; 33 | return ( 34 | 35 | 36 | {({ width }) => ( 37 | 47 | )} 48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | export default RawContent; 55 | -------------------------------------------------------------------------------- /src/panels/sidebar/globalOptions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import styled from 'styled-components'; 4 | 5 | import OptionRow from '../../components/optionRow'; 6 | import { Container, Input, Select, TextButton } from '../../components/styled'; 7 | 8 | const EditConfigButton = styled(TextButton)` 9 | font-size: 14px; 10 | `; 11 | 12 | class GlobalOptions extends React.PureComponent { 13 | constructor(props) { 14 | super(props); 15 | this.updateOptions = this.updateOptions.bind(this); 16 | } 17 | 18 | updateOptions(e) { 19 | const { updateGlobalOptions } = this.props; 20 | const { 21 | dataset: { id: optionId }, 22 | value, 23 | } = e.target; 24 | updateGlobalOptions(optionId, value); 25 | } 26 | 27 | render() { 28 | const { 29 | framesList, 30 | globalOptions: { 31 | backgroundColor: { 32 | display: displayBackgroundColor, 33 | value: valueBackgroundColor, 34 | }, 35 | display: displayOptions, 36 | fixedFrame: { display: displayFixedFrame, value: valueFixedFrame }, 37 | grid: { display: displayGrid, size: valueGrid }, 38 | }, 39 | toggleConfigurationModal, 40 | } = this.props; 41 | if (!displayOptions) { 42 | return null; 43 | } 44 | return ( 45 | 46 | 47 | Edit Configuration 48 | 49 | {displayBackgroundColor && ( 50 | 51 | 57 | 58 | )} 59 | {displayGrid && {valueGrid}} 60 | {displayFixedFrame && ( 61 | 62 | 73 | 74 | )} 75 | 76 | ); 77 | } 78 | } 79 | 80 | export default GlobalOptions; 81 | -------------------------------------------------------------------------------- /src/panels/sidebar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import { ROS_SOCKET_STATUSES } from '../../utils'; 5 | import { vizOptions } from '../../utils/vizOptions'; 6 | import GlobalOptions from './globalOptions'; 7 | import { 8 | ButtonPrimary, 9 | Container, 10 | Separator, 11 | SidebarCollapse, 12 | SidebarWrapper, 13 | StyledSidebar, 14 | } from '../../components/styled'; 15 | import ConnectionDot from '../../components/connectionDot'; 16 | import RosReconnectHandler from './rosReconnectHandler'; 17 | import VizOptions from './vizOptions'; 18 | import { RosStatus, SidebarVizContainer } from '../../components/styled/viz'; 19 | 20 | class Sidebar extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | rosInput: props.rosEndpoint, 25 | }; 26 | this.updateRosInput = this.updateRosInput.bind(this); 27 | this.onSubmit = this.onSubmit.bind(this); 28 | this.toggleSidebarOpen = this.toggleSidebarOpen.bind(this); 29 | } 30 | 31 | updateRosInput(e) { 32 | this.setState({ 33 | rosInput: e.target.value, 34 | }); 35 | } 36 | 37 | toggleSidebarOpen() { 38 | const { togglePanelCollapse } = this.props; 39 | togglePanelCollapse('sidebar'); 40 | } 41 | 42 | onSubmit(e) { 43 | const { 44 | connectRos, 45 | disconnectRos, 46 | rosEndpoint, 47 | rosStatus, 48 | updateRosEndpoint, 49 | } = this.props; 50 | const { rosInput } = this.state; 51 | e.preventDefault(); 52 | if (rosInput !== rosEndpoint) { 53 | updateRosEndpoint(rosInput); 54 | } else if ( 55 | _.includes( 56 | [ROS_SOCKET_STATUSES.CONNECTED, ROS_SOCKET_STATUSES.CONNECTING], 57 | rosStatus, 58 | ) 59 | ) { 60 | disconnectRos(); 61 | } else { 62 | connectRos(); 63 | } 64 | } 65 | 66 | render() { 67 | const { 68 | collapsedSidebar, 69 | connectRos, 70 | framesList, 71 | globalOptions, 72 | removeVisualization, 73 | rosInstance, 74 | rosStatus, 75 | rosTopics, 76 | toggleAddModal, 77 | toggleConfigurationModal, 78 | toggleVisibility, 79 | updateGlobalOptions, 80 | updateVizOptions, 81 | viewer, 82 | visualizations, 83 | vizInstances: vizInstancesSet, 84 | } = this.props; 85 | 86 | const vizInstances = [...vizInstancesSet]; 87 | 88 | const { rosInput } = this.state; 89 | return ( 90 | 91 | 92 | 93 | 94 | 95 | 96 | {rosStatus}.{' '} 97 | 101 | 102 | 103 | 109 | 110 | 111 | {rosStatus === ROS_SOCKET_STATUSES.CONNECTED && ( 112 | <> 113 | 114 | 115 | 116 | Add Visualization 117 | 118 | {_.size(visualizations) === 0 && ( 119 |

No visualizations added to the scene

120 | )} 121 | {_.map(visualizations, vizItem => { 122 | const vizObject = _.find( 123 | vizOptions, 124 | v => v.type === vizItem.vizType, 125 | ); 126 | if (!vizObject) { 127 | return null; 128 | } 129 | const topics = _.filter(rosTopics, t => 130 | _.includes(vizObject.messageTypes, t.messageType), 131 | ); 132 | const relatedTopics = _.filter(rosTopics, t => 133 | _.includes(vizObject.additionalMessageTypes, t.messageType), 134 | ); 135 | let vizInstance = _.filter( 136 | vizInstances, 137 | v => v.key === vizItem.key, 138 | ); 139 | // TODO: This seems like a HACK but it was necessary to get the joints stuff to work when loading a fresh robot model 140 | if (vizInstance.length === 0) { 141 | vizInstance = [vizInstances[vizInstances.length - 1]]; 142 | } 143 | return ( 144 | 157 | ); 158 | })} 159 |
160 | 161 | )} 162 |
163 | 164 | {collapsedSidebar ? '▸' : '◂'} 165 | 166 |
167 | ); 168 | } 169 | } 170 | 171 | export default Sidebar; 172 | -------------------------------------------------------------------------------- /src/panels/sidebar/rosReconnectHandler.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { includes } from 'lodash'; 3 | 4 | import { ROS_SOCKET_STATUSES } from '../../utils'; 5 | 6 | // In seconds 7 | const MIN_TIMER_TIME = 5; 8 | const MAX_TIMER_TIME = 60; 9 | const TIMER_INCREMENT = 0; 10 | const ONE_SECOND = 1000; 11 | 12 | class RosReconnectHandler extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | timer: 0, 17 | }; 18 | this.retryTime = MIN_TIMER_TIME; 19 | this.timerInstance = null; 20 | 21 | this.onTick = this.onTick.bind(this); 22 | this.startTimer = this.startTimer.bind(this); 23 | this.stopTimer = this.stopTimer.bind(this); 24 | } 25 | 26 | componentDidUpdate() { 27 | const { rosStatus } = this.props; 28 | switch (rosStatus) { 29 | case ROS_SOCKET_STATUSES.CONNECTED: 30 | this.retryTime = MIN_TIMER_TIME; 31 | break; 32 | case ROS_SOCKET_STATUSES.CONNECTING: 33 | this.stopTimer(); 34 | break; 35 | case ROS_SOCKET_STATUSES.CONNECTION_ERROR: 36 | case ROS_SOCKET_STATUSES.INITIAL: 37 | default: 38 | if (!this.timerInstance && this.retryTime < MAX_TIMER_TIME) { 39 | this.retryTime += TIMER_INCREMENT; 40 | this.startTimer(); 41 | } 42 | } 43 | } 44 | 45 | onTick() { 46 | const { timer } = this.state; 47 | const { connectRos } = this.props; 48 | if (timer === 0) { 49 | this.timerInstance = null; 50 | connectRos(); 51 | } else { 52 | this.setState({ 53 | timer: timer - 1, 54 | }); 55 | this.timerInstance = setTimeout(this.onTick, ONE_SECOND); 56 | } 57 | } 58 | 59 | startTimer() { 60 | this.setState({ 61 | timer: this.retryTime, 62 | }); 63 | this.timerInstance = setTimeout(this.onTick, ONE_SECOND); 64 | } 65 | 66 | stopTimer() { 67 | if (this.timerInstance) { 68 | clearTimeout(this.timerInstance); 69 | this.timerInstance = null; 70 | this.setState({ 71 | timer: 0, 72 | }); 73 | } 74 | } 75 | 76 | render() { 77 | const { timer } = this.state; 78 | const { rosStatus } = this.props; 79 | if ( 80 | includes( 81 | [ROS_SOCKET_STATUSES.CONNECTING, ROS_SOCKET_STATUSES.CONNECTED], 82 | rosStatus, 83 | ) 84 | ) { 85 | return null; 86 | } 87 | return timer > 0 ? `Reconnecting in ${timer} seconds` : null; 88 | } 89 | } 90 | 91 | export default RosReconnectHandler; 92 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/arrow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { Input } from '../../../components/styled'; 5 | 6 | const { DEFAULT_OPTIONS_ARROW } = CONSTANTS; 7 | 8 | class ArrowOptions extends React.PureComponent { 9 | render() { 10 | const { options: propsOptions, updateOptions } = this.props; 11 | 12 | const { alpha, color, headLength, headRadius, shaftLength, shaftRadius } = { 13 | ...DEFAULT_OPTIONS_ARROW, 14 | ...propsOptions, 15 | }; 16 | 17 | return ( 18 | <> 19 | 20 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 71 | 72 | 73 | 74 | 82 | 83 | 84 | ); 85 | } 86 | } 87 | 88 | export default ArrowOptions; 89 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/axes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { Input } from '../../../components/styled'; 5 | 6 | const { DEFAULT_OPTIONS_AXES } = CONSTANTS; 7 | 8 | class AxesOptions extends React.PureComponent { 9 | render() { 10 | const { options: propsOptions, updateOptions } = this.props; 11 | const { axesLength, axesRadius } = { 12 | ...DEFAULT_OPTIONS_AXES, 13 | ...propsOptions, 14 | }; 15 | return ( 16 | <> 17 | 18 | 26 | 27 | 28 | 29 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default AxesOptions; 44 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/colorTransformer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import _ from 'lodash'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import { Input, OptionContainer, Select } from '../../../components/styled'; 6 | 7 | const { AXES, COLOR_TRANSFORMERS, INTENSITY_CHANNEL_OPTIONS } = CONSTANTS; 8 | 9 | const Intensity = props => { 10 | const { 11 | options: { 12 | autocomputeIntensityBounds, 13 | channelName, 14 | maxColor, 15 | maxIntensity, 16 | minColor, 17 | minIntensity, 18 | useRainbow, 19 | }, 20 | updateOptions, 21 | } = props; 22 | 23 | return ( 24 | <> 25 | 26 | 40 | 41 | {!useRainbow && ( 42 | <> 43 | 44 | 51 | 52 | 53 | 60 | 61 | 62 | )} 63 | {!autocomputeIntensityBounds && ( 64 | <> 65 | 66 | 73 | 74 | 75 | 82 | 83 | 84 | )} 85 | 86 | ); 87 | }; 88 | 89 | const AxisColor = props => { 90 | const { 91 | options: { autocomputeValueBounds, axis, maxAxisValue, minAxisValue }, 92 | updateOptions, 93 | } = props; 94 | 95 | return ( 96 | <> 97 | 98 | 112 | 113 | {!autocomputeValueBounds && ( 114 | 115 | 116 | 123 | 124 | 125 | 126 | 133 | 134 | 135 | )} 136 | 137 | ); 138 | }; 139 | 140 | class ColorTransformer extends React.PureComponent { 141 | render() { 142 | const { 143 | options: { colorTransformer, flatColor }, 144 | options, 145 | updateOptions, 146 | } = this.props; 147 | 148 | switch (colorTransformer) { 149 | case COLOR_TRANSFORMERS.INTENSITY: 150 | return ; 151 | case COLOR_TRANSFORMERS.AXIS_COLOR: 152 | return ; 153 | case COLOR_TRANSFORMERS.FLAT_COLOR: 154 | return ( 155 | 156 | 163 | 164 | ); 165 | default: 166 | return null; 167 | } 168 | } 169 | } 170 | 171 | export default ColorTransformer; 172 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/flatArrow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | 4 | import OptionRow from '../../../components/optionRow'; 5 | import { Input } from '../../../components/styled'; 6 | 7 | const { DEFAULT_OPTIONS_FLATARROW } = CONSTANTS; 8 | 9 | class FlatArrowOptions extends React.PureComponent { 10 | render() { 11 | const { options: propsOptions, updateOptions } = this.props; 12 | const { alpha, arrowLength, color } = { 13 | ...DEFAULT_OPTIONS_FLATARROW, 14 | ...propsOptions, 15 | }; 16 | return ( 17 | <> 18 | 19 | 26 | 27 | 28 | 29 | 37 | 38 | 39 | 40 | 48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | export default FlatArrowOptions; 55 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import VizSpecificOptions from './vizSpecificOption'; 5 | import { Button, Select, StyledOptionRow } from '../../../components/styled'; 6 | import OptionRow from '../../../components/optionRow'; 7 | import { 8 | VizItem, 9 | VizItemActions, 10 | VizItemCollapse, 11 | VizItemContent, 12 | VizItemIcon, 13 | } from '../../../components/styled/viz'; 14 | import Chevron from '../../../components/chevron'; 15 | import { 16 | VIZ_TYPE_DEPTHCLOUD_STREAM, 17 | VIZ_TYPE_IMAGE_STREAM, 18 | } from '../../../utils/vizOptions'; 19 | 20 | const { 21 | VIZ_TYPE_INTERACTIVEMARKER, 22 | VIZ_TYPE_ROBOTMODEL, 23 | VIZ_TYPE_TF, 24 | } = CONSTANTS; 25 | 26 | const VizOptions = ({ 27 | options: { display, key, name, topicName, visible, vizType }, 28 | options, 29 | topics, 30 | relatedTopics, 31 | vizInstance, 32 | vizObject: { icon }, 33 | updateVizOptions, 34 | removeVisualization, 35 | toggleVisibility, 36 | }) => { 37 | const [collapsed, toggleCollapsed] = useState(false); 38 | 39 | if (_.isBoolean(display) && !display) { 40 | return null; 41 | } 42 | 43 | const updateVizOptionsWrapper = e => { 44 | if (vizType === VIZ_TYPE_INTERACTIVEMARKER) { 45 | updateVizOptions(key, { 46 | topicName: e.target.value, 47 | updateTopicName: undefined, 48 | feedbackTopicName: undefined, 49 | }); 50 | return; 51 | } 52 | updateVizOptions(key, { topicName: e.target.value }); 53 | }; 54 | 55 | return ( 56 | 57 | 58 | toggleCollapsed(!collapsed)} 61 | > 62 | 63 | 64 | {icon} 65 | {name} 66 | 67 | {!collapsed && ( 68 | 69 | {!_.includes( 70 | [ 71 | VIZ_TYPE_ROBOTMODEL, 72 | VIZ_TYPE_TF, 73 | VIZ_TYPE_DEPTHCLOUD_STREAM, 74 | VIZ_TYPE_IMAGE_STREAM, 75 | ], 76 | vizType, 77 | ) && ( 78 | 79 | 84 | 85 | )} 86 | 93 | 94 | 97 | 100 | 101 | 102 | )} 103 | 104 | ); 105 | }; 106 | 107 | export default VizOptions; 108 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/interactiveMarkerOptions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import { Select } from '../../../components/styled'; 6 | 7 | const { 8 | MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK, 9 | MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE, 10 | } = CONSTANTS; 11 | 12 | class InteractiveMarkerOptions extends React.PureComponent { 13 | constructor(props) { 14 | super(props); 15 | 16 | const { 17 | options: propsOptions, 18 | relatedTopics, 19 | updateVizOptions, 20 | } = this.props; 21 | 22 | const { key } = propsOptions; 23 | 24 | this.updateTopics = _.filter( 25 | relatedTopics, 26 | t => t.messageType === MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE, 27 | ); 28 | this.feedbackTopics = _.filter( 29 | relatedTopics, 30 | t => t.messageType === MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK, 31 | ); 32 | 33 | updateVizOptions(key, { 34 | updateTopicName: { 35 | name: this.updateTopics.length > 0 ? this.updateTopics[0].name : '', 36 | messageType: MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE, 37 | }, 38 | feedbackTopicName: { 39 | name: this.feedbackTopics.length > 0 ? this.feedbackTopics[0].name : '', 40 | messageType: MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK, 41 | }, 42 | }); 43 | } 44 | 45 | render() { 46 | const { options: propsOptions, updateVizOptions } = this.props; 47 | 48 | const { feedbackTopicName, key, updateTopicName } = propsOptions; 49 | 50 | return ( 51 | <> 52 | 53 | 71 | 72 | 73 | 91 | 92 | 93 | ); 94 | } 95 | } 96 | 97 | export default InteractiveMarkerOptions; 98 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/laserScan.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import ColorTransformer from './colorTransformer'; 6 | import { updateOptionsUtil } from '../../../utils'; 7 | import { Input, Select } from '../../../components/styled'; 8 | 9 | const { 10 | COLOR_TRANSFORMERS, 11 | DEFAULT_OPTIONS_LASERSCAN, 12 | LASERSCAN_STYLES, 13 | } = CONSTANTS; 14 | 15 | class LaserScanOptions extends React.PureComponent { 16 | constructor(props) { 17 | super(props); 18 | this.updateOptions = updateOptionsUtil.bind(this); 19 | } 20 | 21 | render() { 22 | const { options: propsOptions } = this.props; 23 | 24 | const options = { 25 | ...DEFAULT_OPTIONS_LASERSCAN, 26 | ...propsOptions, 27 | }; 28 | const { alpha, colorTransformer, size, style } = options; 29 | 30 | return ( 31 | <> 32 | 33 | 47 | 48 | 49 | 50 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 72 | 86 | 87 | 91 | 92 | ); 93 | } 94 | } 95 | 96 | export default LaserScanOptions; 97 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/map.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import { updateOptionsUtil } from '../../../utils'; 6 | import { Input, Select } from '../../../components/styled'; 7 | 8 | const { DEFAULT_OPTIONS_MAP, MAP_COLOR_SCHEMES } = CONSTANTS; 9 | 10 | class MapOptions extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.updateOptions = updateOptionsUtil.bind(this); 14 | } 15 | 16 | render() { 17 | const { options: propsOptions } = this.props; 18 | const options = { 19 | ...DEFAULT_OPTIONS_MAP, 20 | ...propsOptions, 21 | }; 22 | const { alpha, colorScheme, drawBehind } = options; 23 | return ( 24 | <> 25 | 26 | 34 | 35 | 36 | 50 | 51 | 52 | 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default MapOptions; 66 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/marker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import { CONSTANTS } from 'amphion'; 5 | import OptionRow from '../../../components/optionRow'; 6 | import { Input, OptionContainer } from '../../../components/styled'; 7 | 8 | const { DEFAULT_OPTIONS_MARKER } = CONSTANTS; 9 | 10 | class MarkerOptions extends React.PureComponent { 11 | constructor(props) { 12 | super(props); 13 | this.updateNamespaceVisibility = this.updateNamespaceVisibility.bind(this); 14 | } 15 | 16 | updateNamespaceVisibility(e) { 17 | const { 18 | options: { key, namespaces }, 19 | updateVizOptions, 20 | } = this.props; 21 | const { 22 | checked, 23 | dataset: { id: optionId }, 24 | } = e.target; 25 | updateVizOptions(key, { 26 | namespaces: { 27 | ...namespaces, 28 | [optionId]: checked, 29 | }, 30 | }); 31 | } 32 | 33 | render() { 34 | const { options: propsOptions } = this.props; 35 | 36 | const { namespaces } = { 37 | ...DEFAULT_OPTIONS_MARKER, 38 | ...propsOptions, 39 | }; 40 | 41 | if (_.size(_.compact(_.keys(namespaces))) === 0) { 42 | return null; 43 | } 44 | 45 | return ( 46 | <> 47 | Namespaces: 48 | 49 | {_.map(namespaces, (checked, key) => 50 | key ? ( 51 | 52 | 59 | 60 | ) : null, 61 | )} 62 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | export default MarkerOptions; 69 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/odometry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import ShapeOptions from './shape'; 6 | import { updateOptionsUtil } from '../../../utils'; 7 | import { Input, OptionContainer, Select } from '../../../components/styled'; 8 | 9 | const { 10 | DEFAULT_OPTIONS_ODOMETRY, 11 | OBJECT_TYPE_ARROW, 12 | OBJECT_TYPE_AXES, 13 | } = CONSTANTS; 14 | 15 | class OdometryOptions extends React.PureComponent { 16 | constructor(props) { 17 | super(props); 18 | this.updateOptions = updateOptionsUtil.bind(this); 19 | } 20 | 21 | render() { 22 | const { options: propsOptions } = this.props; 23 | const options = { 24 | ...DEFAULT_OPTIONS_ODOMETRY, 25 | ...propsOptions, 26 | }; 27 | const { 28 | angleTolerance, 29 | keep, 30 | positionTolerance, 31 | type: shapeType, 32 | } = options; 33 | 34 | return ( 35 | <> 36 | 37 | 45 | 46 | 47 | 55 | 56 | 57 | 65 | 66 | 67 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | } 89 | 90 | export default OdometryOptions; 91 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/path.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { updateOptionsUtil } from '../../../utils'; 5 | import { Input } from '../../../components/styled'; 6 | 7 | const { DEFAULT_OPTIONS_PATH } = CONSTANTS; 8 | 9 | class PathOptions extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.updateOptions = updateOptionsUtil.bind(this); 13 | } 14 | 15 | render() { 16 | const { options: propsOptions } = this.props; 17 | const { alpha, color } = { 18 | ...DEFAULT_OPTIONS_PATH, 19 | ...propsOptions, 20 | }; 21 | return ( 22 | <> 23 | 24 | 31 | 32 | 33 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default PathOptions; 48 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/point.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { updateOptionsUtil } from '../../../utils'; 5 | import { Input } from '../../../components/styled'; 6 | 7 | const { DEFAULT_OPTIONS_POINT } = CONSTANTS; 8 | 9 | class PointOptions extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.updateOptions = updateOptionsUtil.bind(this); 13 | } 14 | 15 | render() { 16 | const { options: propsOptions } = this.props; 17 | const { alpha, color, radius } = { 18 | ...DEFAULT_OPTIONS_POINT, 19 | ...propsOptions, 20 | }; 21 | return ( 22 | <> 23 | 24 | 32 | 33 | 34 | 41 | 42 | 43 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default PointOptions; 58 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/pointcloud.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import { updateOptionsUtil } from '../../../utils'; 6 | import { Input, Select } from '../../../components/styled'; 7 | 8 | const { DEFAULT_OPTIONS_POINTCLOUD, POINTCLOUD_COLOR_CHANNELS } = CONSTANTS; 9 | 10 | class PointCloudOptions extends React.PureComponent { 11 | constructor(props) { 12 | super(props); 13 | this.updateOptions = updateOptionsUtil.bind(this); 14 | } 15 | 16 | render() { 17 | const { options: propsOptions } = this.props; 18 | 19 | const options = { 20 | ...DEFAULT_OPTIONS_POINTCLOUD, 21 | ...propsOptions, 22 | }; 23 | const { colorChannel, size, useRainbow } = options; 24 | 25 | return ( 26 | <> 27 | 28 | 42 | 43 | 44 | 45 | 53 | 54 | 55 | {colorChannel === POINTCLOUD_COLOR_CHANNELS.INTENSITY && ( 56 | 57 | 64 | 65 | )} 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default PointCloudOptions; 72 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/pose.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { CONSTANTS } from 'amphion'; 4 | import OptionRow from '../../../components/optionRow'; 5 | import ShapeOptions from './shape'; 6 | import { updateOptionsUtil } from '../../../utils'; 7 | import { OptionContainer, Select } from '../../../components/styled'; 8 | 9 | const { 10 | DEFAULT_OPTIONS_POSE, 11 | OBJECT_TYPE_ARROW, 12 | OBJECT_TYPE_AXES, 13 | OBJECT_TYPE_FLAT_ARROW, 14 | VIZ_TYPE_POSE, 15 | VIZ_TYPE_POSEARRAY, 16 | } = CONSTANTS; 17 | 18 | const dropdownOptions = { 19 | [VIZ_TYPE_POSE]: [OBJECT_TYPE_ARROW, OBJECT_TYPE_AXES], 20 | [VIZ_TYPE_POSEARRAY]: [ 21 | OBJECT_TYPE_ARROW, 22 | OBJECT_TYPE_AXES, 23 | OBJECT_TYPE_FLAT_ARROW, 24 | ], 25 | }; 26 | 27 | class PoseOptions extends React.PureComponent { 28 | constructor(props) { 29 | super(props); 30 | this.updateOptions = updateOptionsUtil.bind(this); 31 | } 32 | 33 | render() { 34 | const { options: propsOptions } = this.props; 35 | const options = { 36 | ...DEFAULT_OPTIONS_POSE, 37 | ...propsOptions, 38 | }; 39 | const { type: shapeType, vizType } = options; 40 | 41 | return ( 42 | <> 43 | 44 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default PoseOptions; 66 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/range.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { updateOptionsUtil } from '../../../utils'; 5 | import { Input } from '../../../components/styled'; 6 | 7 | const { DEFAULT_OPTIONS_RANGE } = CONSTANTS; 8 | 9 | class RangeOptions extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.updateOptions = updateOptionsUtil.bind(this); 13 | } 14 | 15 | render() { 16 | const { options: propsOptions } = this.props; 17 | const { alpha, color } = { 18 | ...DEFAULT_OPTIONS_RANGE, 19 | ...propsOptions, 20 | }; 21 | return ( 22 | <> 23 | 24 | 31 | 32 | 33 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default RangeOptions; 48 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/robotModel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { keys, map } from 'lodash'; 3 | import { VizItem, VizItemCollapse } from '../../../components/styled/viz'; 4 | import Chevron from '../../../components/chevron'; 5 | import { Input, StyledOptionRow } from '../../../components/styled'; 6 | import OptionRow from '../../../components/optionRow'; 7 | 8 | class RobotModelLinksJoints extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | linksCollapsed: true, 13 | jointsCollapsed: true, 14 | }; 15 | 16 | this.toggleCollapsed = this.toggleCollapsed.bind(this); 17 | } 18 | 19 | toggleCollapsed(name) { 20 | // eslint-disable-next-line react/destructuring-assignment 21 | const current = this.state[name]; 22 | this.setState({ [name]: !current }); 23 | } 24 | 25 | render() { 26 | const { jointsCollapsed, linksCollapsed } = this.state; 27 | const { vizInstance } = this.props; 28 | 29 | let joints = null; 30 | let links = null; 31 | 32 | if (vizInstance) { 33 | let v = vizInstance; 34 | const rest = null; 35 | if (Array.isArray(vizInstance)) [v] = vizInstance; 36 | 37 | const urdfObject = v ? v.urdfObject : null; 38 | joints = urdfObject ? urdfObject.joints : null; 39 | links = urdfObject ? urdfObject.links : null; 40 | } 41 | 42 | return ( 43 | <> 44 | 45 | 46 | this.toggleCollapsed('linksCollapsed')} 49 | > 50 | 51 | 52 | Links 53 | 54 | {!linksCollapsed && 55 | links && 56 | map(keys(links), (name, index) => { 57 | const link = links[name]; 58 | return ( 59 | 60 | { 66 | const { checked } = e.target; 67 | if (checked) { 68 | link.show(); 69 | } else { 70 | link.hide(); 71 | } 72 | }} 73 | /> 74 | 75 | ); 76 | })} 77 | 78 | 79 | this.toggleCollapsed('jointsCollapsed')} 82 | > 83 | 84 | 85 | Joints 86 | 87 | {!jointsCollapsed && 88 | joints && 89 | map(keys(joints), (name, index) => { 90 | return ; 91 | })} 92 | 93 | ); 94 | } 95 | } 96 | 97 | export default RobotModelLinksJoints; 98 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/shape.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import Arrow from './arrow'; 4 | import FlatArrow from './flatArrow'; 5 | import Axes from './axes'; 6 | 7 | const { 8 | OBJECT_TYPE_ARROW, 9 | OBJECT_TYPE_AXES, 10 | OBJECT_TYPE_FLAT_ARROW, 11 | } = CONSTANTS; 12 | 13 | class ShapeOptions extends React.PureComponent { 14 | render() { 15 | const { 16 | options, 17 | options: { type: shapeType }, 18 | updateOptions, 19 | } = this.props; 20 | 21 | switch (shapeType) { 22 | case OBJECT_TYPE_ARROW: 23 | return ; 24 | case OBJECT_TYPE_FLAT_ARROW: 25 | return ; 26 | case OBJECT_TYPE_AXES: 27 | return ; 28 | default: 29 | return null; 30 | } 31 | } 32 | } 33 | 34 | export default ShapeOptions; 35 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/vizSpecificOption.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import LaserScanOptions from './laserScan'; 4 | import MapOptions from './map'; 5 | import MarkerOptions from './marker'; 6 | import OdometryOptions from './odometry'; 7 | import PathOptions from './path'; 8 | import PoseOptions from './pose'; 9 | import PointCloudOptions from './pointcloud'; 10 | import RangeOptions from './range'; 11 | import PointOptions from './point'; 12 | import InteractiveMarkerOptions from './interactiveMarkerOptions'; 13 | import WrenchOptions from './wrench'; 14 | import RobotModelLinksJoints from './robotModel'; 15 | 16 | const { 17 | VIZ_TYPE_IMAGE, 18 | VIZ_TYPE_INTERACTIVEMARKER, 19 | VIZ_TYPE_LASERSCAN, 20 | VIZ_TYPE_MAP, 21 | VIZ_TYPE_MARKER, 22 | VIZ_TYPE_MARKERARRAY, 23 | VIZ_TYPE_ODOMETRY, 24 | VIZ_TYPE_PATH, 25 | VIZ_TYPE_POINT, 26 | VIZ_TYPE_POINTCLOUD, 27 | VIZ_TYPE_POLYGON, 28 | VIZ_TYPE_POSE, 29 | VIZ_TYPE_POSEARRAY, 30 | VIZ_TYPE_RANGE, 31 | VIZ_TYPE_ROBOTMODEL, 32 | VIZ_TYPE_TF, 33 | VIZ_TYPE_WRENCH, 34 | } = CONSTANTS; 35 | 36 | const VizSpecificOptions = ({ 37 | options: { vizType }, 38 | options, 39 | topics, 40 | vizInstance, 41 | relatedTopics, 42 | updateVizOptions, 43 | }) => { 44 | switch (vizType) { 45 | case VIZ_TYPE_IMAGE: 46 | return null; 47 | case VIZ_TYPE_INTERACTIVEMARKER: 48 | return ( 49 | 55 | ); 56 | case VIZ_TYPE_LASERSCAN: 57 | return ( 58 | 62 | ); 63 | case VIZ_TYPE_MAP: 64 | return ( 65 | 66 | ); 67 | case VIZ_TYPE_MARKER: 68 | return ( 69 | 70 | ); 71 | case VIZ_TYPE_MARKERARRAY: 72 | return null; 73 | case VIZ_TYPE_ODOMETRY: 74 | return ( 75 | 79 | ); 80 | case VIZ_TYPE_PATH: 81 | return ( 82 | 83 | ); 84 | case VIZ_TYPE_POINT: 85 | return ( 86 | 87 | ); 88 | case VIZ_TYPE_POINTCLOUD: 89 | return ( 90 | 94 | ); 95 | case VIZ_TYPE_POLYGON: 96 | return null; 97 | case VIZ_TYPE_POSE: 98 | return ( 99 | 100 | ); 101 | case VIZ_TYPE_POSEARRAY: 102 | return null; 103 | case VIZ_TYPE_RANGE: 104 | return ( 105 | 106 | ); 107 | case VIZ_TYPE_ROBOTMODEL: 108 | return ; 109 | case VIZ_TYPE_TF: 110 | return null; 111 | case VIZ_TYPE_WRENCH: 112 | return ( 113 | 114 | ); 115 | default: 116 | return null; 117 | } 118 | }; 119 | 120 | export default VizSpecificOptions; 121 | -------------------------------------------------------------------------------- /src/panels/sidebar/vizOptions/wrench.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CONSTANTS } from 'amphion'; 3 | import OptionRow from '../../../components/optionRow'; 4 | import { updateOptionsUtil } from '../../../utils'; 5 | import { Input } from '../../../components/styled'; 6 | 7 | const { DEFAULT_OPTIONS_WRENCH } = CONSTANTS; 8 | 9 | class WrenchOptions extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.updateOptions = updateOptionsUtil.bind(this); 13 | } 14 | 15 | render() { 16 | const { options: propsOptions } = this.props; 17 | const { 18 | alpha, 19 | arrowWidth, 20 | forceArrowScale, 21 | forceColor, 22 | torqueArrowScale, 23 | torqueColor, 24 | } = { 25 | ...DEFAULT_OPTIONS_WRENCH, 26 | ...propsOptions, 27 | }; 28 | return ( 29 | <> 30 | 31 | 38 | 39 | 40 | 47 | 48 | 49 | 57 | 58 | 59 | 67 | 68 | 69 | 77 | 78 | 79 | 87 | 88 | 89 | ); 90 | } 91 | } 92 | 93 | export default WrenchOptions; 94 | -------------------------------------------------------------------------------- /src/panels/sources/index.js: -------------------------------------------------------------------------------- 1 | import Amphion from 'amphion'; 2 | 3 | const rosTopicDataSources = {}; 4 | 5 | export const getOrCreateRosTopicDataSource = options => { 6 | const { topicName } = options; 7 | const existingSource = rosTopicDataSources[topicName]; 8 | if (existingSource) { 9 | return existingSource; 10 | } 11 | rosTopicDataSources[existingSource] = new Amphion.RosTopicDataSource(options); 12 | return rosTopicDataSources[existingSource]; 13 | }; 14 | -------------------------------------------------------------------------------- /src/panels/tools/index.jsx: -------------------------------------------------------------------------------- 1 | const Tools = () => {}; 2 | 3 | export default Tools; 4 | -------------------------------------------------------------------------------- /src/panels/viewer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledViewport = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | position: relative; 8 | 9 | #viewportStats { 10 | position: absolute !important; 11 | top: auto !important; 12 | left: auto !important; 13 | right: 0 !important; 14 | bottom: 0 !important; 15 | } 16 | `; 17 | 18 | class Viewport extends React.PureComponent { 19 | constructor(props) { 20 | super(props); 21 | this.container = React.createRef(); 22 | 23 | this.updateViewerOptions = this.updateViewerOptions.bind(this); 24 | } 25 | 26 | componentDidUpdate() { 27 | this.updateViewerOptions(); 28 | } 29 | 30 | updateViewerOptions() { 31 | const { 32 | globalOptions: { 33 | backgroundColor: { value: backgroundColor }, 34 | fixedFrame: { value: selectedFrame }, 35 | grid: { 36 | centerlineColor: gridCenterlineColor, 37 | color: gridColor, 38 | divisions: gridDivisions, 39 | size: gridSize, 40 | }, 41 | }, 42 | viewer, 43 | } = this.props; 44 | viewer.updateOptions({ 45 | backgroundColor, 46 | gridSize, 47 | gridDivisions, 48 | gridColor, 49 | gridCenterlineColor, 50 | selectedFrame, 51 | }); 52 | } 53 | 54 | componentDidMount() { 55 | const { viewer } = this.props; 56 | const container = this.container.current; 57 | viewer.setContainer(container); 58 | this.updateViewerOptions(); 59 | viewer.scene.stats.dom.id = 'viewportStats'; 60 | container.appendChild(viewer.scene.stats.dom); 61 | } 62 | 63 | componentWillUnmount() { 64 | const { viewer } = this.props; 65 | viewer.destroy(); 66 | } 67 | 68 | render() { 69 | return ; 70 | } 71 | } 72 | 73 | export default Viewport; 74 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | export const iconLineStyle = { 2 | fill: 'none', 3 | stroke: '#dc1d30', 4 | strokeLinecap: 'round', 5 | strokeLinejoin: 'round', 6 | strokeWidth: '1px', 7 | }; 8 | 9 | export const iconFillStyle = { 10 | fill: '#dc1d30', 11 | }; 12 | 13 | export const TOOL_TYPE_CONTROLS = 'TOOL_TYPE_CONTROLS'; 14 | export const TOOL_TYPE_POINT = 'TOOL_TYPE_POINT'; 15 | export const TOOL_TYPE_NAV_GOAL = 'TOOL_TYPE_NAV_GOAL'; 16 | export const TOOL_TYPE_POSE_ESTIMATE = 'TOOL_TYPE_POSE_ESTIMATE'; 17 | 18 | export const MESSAGE_TYPE_TOOL_POINT = 'geometry_msgs/PointStamped'; 19 | export const MESSAGE_TYPE_TOOL_NAV_GOAL = 'geometry_msgs/PoseStamped'; 20 | export const MESSAGE_TYPE_TOOL_POSE_ESTIMATE = 21 | 'geometry_msgs/PoseWithCovarianceStamped'; 22 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | const API_CALL_STATUS = { 2 | IDLE: 0, 3 | FETCHING: 1, 4 | SUCCESSFUL: 2, 5 | ERROR: 3, 6 | }; 7 | 8 | export default API_CALL_STATUS; 9 | -------------------------------------------------------------------------------- /src/utils/editorControls.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * @author qiao / https://github.com/qiao 4 | * @author mrdoob / http://mrdoob.com 5 | * @author alteredq / http://alteredqualia.com/ 6 | * @author WestLangley / http://github.com/WestLangley 7 | */ 8 | 9 | import { 10 | Box3, 11 | EventDispatcher, 12 | Matrix3, 13 | Sphere, 14 | Spherical, 15 | Vector2, 16 | Vector3, 17 | } from 'three/build/three.module'; 18 | 19 | const EditorControls = function(object, domElement) { 20 | domElement = domElement !== undefined ? domElement : document; 21 | 22 | // API 23 | 24 | this.enabled = true; 25 | this.center = new Vector3(); 26 | this.panSpeed = 0.002; 27 | this.zoomSpeed = 0.1; 28 | this.rotationSpeed = 0.005; 29 | 30 | // internals 31 | 32 | const scope = this; 33 | const vector = new Vector3(); 34 | const delta = new Vector3(); 35 | const box = new Box3(); 36 | 37 | const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 }; 38 | let state = STATE.NONE; 39 | 40 | const { center } = this; 41 | const normalMatrix = new Matrix3(); 42 | const pointer = new Vector2(); 43 | const pointerOld = new Vector2(); 44 | const spherical = new Spherical(); 45 | const sphere = new Sphere(); 46 | 47 | // events 48 | 49 | const changeEvent = { type: 'change' }; 50 | 51 | this.focus = function(target) { 52 | let distance; 53 | 54 | box.setFromObject(target); 55 | 56 | if (box.isEmpty() === false) { 57 | box.getCenter(center); 58 | distance = box.getBoundingSphere(sphere).radius; 59 | } else { 60 | // Focusing on an Group, AmbientLight, etc 61 | 62 | center.setFromMatrixPosition(target.matrixWorld); 63 | distance = 0.1; 64 | } 65 | 66 | delta.set(0, 0, 1); 67 | delta.applyQuaternion(object.quaternion); 68 | delta.multiplyScalar(distance * 4); 69 | 70 | object.position.copy(center).add(delta); 71 | 72 | scope.dispatchEvent(changeEvent); 73 | }; 74 | 75 | this.pan = function(delta) { 76 | const distance = object.position.distanceTo(center); 77 | 78 | delta.multiplyScalar(distance * scope.panSpeed); 79 | delta.applyMatrix3(normalMatrix.getNormalMatrix(object.matrix)); 80 | 81 | object.position.add(delta); 82 | center.add(delta); 83 | 84 | scope.dispatchEvent(changeEvent); 85 | }; 86 | 87 | this.zoom = function(delta) { 88 | const distance = object.position.distanceTo(center); 89 | 90 | delta.multiplyScalar(distance * scope.zoomSpeed); 91 | 92 | if (delta.length() > distance) return; 93 | 94 | delta.applyMatrix3(normalMatrix.getNormalMatrix(object.matrix)); 95 | 96 | object.position.add(delta); 97 | 98 | scope.dispatchEvent(changeEvent); 99 | }; 100 | 101 | this.rotate = function(delta) { 102 | vector.copy(object.position).sub(center); 103 | 104 | // spherical.setFromVector3(vector); 105 | spherical.setFromCartesianCoords(-1 * vector.x, vector.z, vector.y); 106 | 107 | spherical.theta += delta.x * scope.rotationSpeed; 108 | spherical.phi += delta.y * scope.rotationSpeed; 109 | 110 | spherical.makeSafe(); 111 | 112 | vector.setFromSpherical(spherical); 113 | 114 | object.position.copy(center).add(vector); 115 | 116 | object.lookAt(center); 117 | 118 | scope.dispatchEvent(changeEvent); 119 | }; 120 | 121 | // mouse 122 | 123 | function onMouseDown(event) { 124 | if (scope.enabled === false) return; 125 | 126 | if (event.button === 0) { 127 | state = STATE.ROTATE; 128 | } else if (event.button === 1) { 129 | state = STATE.ZOOM; 130 | } else if (event.button === 2) { 131 | state = STATE.PAN; 132 | } 133 | 134 | pointerOld.set(event.clientX, event.clientY); 135 | 136 | domElement.addEventListener('mousemove', onMouseMove, false); 137 | domElement.addEventListener('mouseup', onMouseUp, false); 138 | domElement.addEventListener('mouseout', onMouseUp, false); 139 | domElement.addEventListener('dblclick', onMouseUp, false); 140 | } 141 | 142 | function onMouseMove(event) { 143 | if (scope.enabled === false) return; 144 | 145 | pointer.set(event.clientX, event.clientY); 146 | 147 | const movementX = pointer.x - pointerOld.x; 148 | const movementY = pointer.y - pointerOld.y; 149 | 150 | if (state === STATE.ROTATE) { 151 | scope.rotate(delta.set(-movementX, -movementY, 0)); 152 | } else if (state === STATE.ZOOM) { 153 | scope.zoom(delta.set(0, 0, movementY)); 154 | } else if (state === STATE.PAN) { 155 | scope.pan(delta.set(-movementX, movementY, 0)); 156 | } 157 | 158 | pointerOld.set(event.clientX, event.clientY); 159 | } 160 | 161 | function onMouseUp(event) { 162 | domElement.removeEventListener('mousemove', onMouseMove, false); 163 | domElement.removeEventListener('mouseup', onMouseUp, false); 164 | domElement.removeEventListener('mouseout', onMouseUp, false); 165 | domElement.removeEventListener('dblclick', onMouseUp, false); 166 | 167 | state = STATE.NONE; 168 | } 169 | 170 | function onMouseWheel(event) { 171 | event.preventDefault(); 172 | 173 | // Normalize deltaY due to https://bugzilla.mozilla.org/show_bug.cgi?id=1392460 174 | scope.zoom(delta.set(0, 0, event.deltaY > 0 ? 1 : -1)); 175 | } 176 | 177 | function contextmenu(event) { 178 | event.preventDefault(); 179 | } 180 | 181 | this.dispose = function() { 182 | domElement.removeEventListener('contextmenu', contextmenu, false); 183 | domElement.removeEventListener('mousedown', onMouseDown, false); 184 | domElement.removeEventListener('wheel', onMouseWheel, false); 185 | 186 | domElement.removeEventListener('mousemove', onMouseMove, false); 187 | domElement.removeEventListener('mouseup', onMouseUp, false); 188 | domElement.removeEventListener('mouseout', onMouseUp, false); 189 | domElement.removeEventListener('dblclick', onMouseUp, false); 190 | 191 | domElement.removeEventListener('touchstart', touchStart, false); 192 | domElement.removeEventListener('touchmove', touchMove, false); 193 | }; 194 | 195 | domElement.addEventListener('contextmenu', contextmenu, false); 196 | domElement.addEventListener('mousedown', onMouseDown, false); 197 | domElement.addEventListener('wheel', onMouseWheel, false); 198 | 199 | // touch 200 | 201 | const touches = [new Vector3(), new Vector3(), new Vector3()]; 202 | const prevTouches = [new Vector3(), new Vector3(), new Vector3()]; 203 | 204 | let prevDistance = null; 205 | 206 | function touchStart(event) { 207 | if (scope.enabled === false) return; 208 | 209 | switch (event.touches.length) { 210 | case 1: 211 | touches[0] 212 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 213 | .divideScalar(window.devicePixelRatio); 214 | touches[1] 215 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 216 | .divideScalar(window.devicePixelRatio); 217 | break; 218 | 219 | case 2: 220 | touches[0] 221 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 222 | .divideScalar(window.devicePixelRatio); 223 | touches[1] 224 | .set(event.touches[1].pageX, event.touches[1].pageY, 0) 225 | .divideScalar(window.devicePixelRatio); 226 | prevDistance = touches[0].distanceTo(touches[1]); 227 | break; 228 | } 229 | 230 | prevTouches[0].copy(touches[0]); 231 | prevTouches[1].copy(touches[1]); 232 | } 233 | 234 | function touchMove(event) { 235 | if (scope.enabled === false) return; 236 | 237 | event.preventDefault(); 238 | event.stopPropagation(); 239 | 240 | function getClosest(touch, touches) { 241 | let closest = touches[0]; 242 | 243 | for (const i in touches) { 244 | if (closest.distanceTo(touch) > touches[i].distanceTo(touch)) 245 | closest = touches[i]; 246 | } 247 | 248 | return closest; 249 | } 250 | 251 | switch (event.touches.length) { 252 | case 1: 253 | touches[0] 254 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 255 | .divideScalar(window.devicePixelRatio); 256 | touches[1] 257 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 258 | .divideScalar(window.devicePixelRatio); 259 | scope.rotate( 260 | touches[0] 261 | .sub(getClosest(touches[0], prevTouches)) 262 | .multiplyScalar(-1), 263 | ); 264 | break; 265 | 266 | case 2: 267 | touches[0] 268 | .set(event.touches[0].pageX, event.touches[0].pageY, 0) 269 | .divideScalar(window.devicePixelRatio); 270 | touches[1] 271 | .set(event.touches[1].pageX, event.touches[1].pageY, 0) 272 | .divideScalar(window.devicePixelRatio); 273 | var distance = touches[0].distanceTo(touches[1]); 274 | scope.zoom(delta.set(0, 0, prevDistance - distance)); 275 | prevDistance = distance; 276 | 277 | var offset0 = touches[0] 278 | .clone() 279 | .sub(getClosest(touches[0], prevTouches)); 280 | var offset1 = touches[1] 281 | .clone() 282 | .sub(getClosest(touches[1], prevTouches)); 283 | offset0.x = -offset0.x; 284 | offset1.x = -offset1.x; 285 | 286 | scope.pan(offset0.add(offset1)); 287 | 288 | break; 289 | } 290 | 291 | prevTouches[0].copy(touches[0]); 292 | prevTouches[1].copy(touches[1]); 293 | } 294 | 295 | domElement.addEventListener('touchstart', touchStart, false); 296 | domElement.addEventListener('touchmove', touchMove, false); 297 | }; 298 | 299 | EditorControls.prototype = Object.create(EventDispatcher.prototype); 300 | EditorControls.prototype.constructor = EditorControls; 301 | 302 | export { EditorControls }; 303 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { CONSTANTS } from 'amphion'; 2 | import _ from 'lodash'; 3 | import { TF_MESSAGE_TYPES } from './vizOptions'; 4 | 5 | const { DEFAULT_OPTIONS_SCENE } = CONSTANTS; 6 | 7 | export const ROS_SOCKET_STATUSES = { 8 | INITIAL: 'Not Connected', 9 | CONNECTING: 'Connecting', 10 | CONNECTED: 'Connected', 11 | CONNECTION_ERROR: 'Error in connection', 12 | }; 13 | 14 | export const getTfTopics = rosTopics => 15 | _.filter(rosTopics, t => _.includes(TF_MESSAGE_TYPES, t.messageType)); 16 | 17 | export const stopPropagation = e => e.stopPropagation(); 18 | 19 | export const downloadFile = (content, filename, options = {}) => { 20 | const element = document.createElement('a'); 21 | element.setAttribute( 22 | 'href', 23 | `data:${options.mimetype || 'text/json'};charset=utf-8,${encodeURIComponent( 24 | content, 25 | )}`, 26 | ); 27 | element.setAttribute('download', filename); 28 | element.style.display = 'none'; 29 | document.body.appendChild(element); 30 | element.click(); 31 | document.body.removeChild(element); 32 | }; 33 | 34 | const getURLEndpoint = type => { 35 | const urlSearchParams = new URLSearchParams(window.location.search); 36 | const urlParams = Object.fromEntries(urlSearchParams.entries()); 37 | 38 | if (type === 'bridge') { 39 | urlParams.bridge; 40 | } 41 | if (type === 'pkgs') { 42 | urlParams.pkgs; 43 | } 44 | return ''; 45 | }; 46 | 47 | export const DEFAULT_CONFIG = { 48 | panels: { 49 | sidebar: { 50 | display: true, 51 | collapsed: false, 52 | }, 53 | header: { 54 | display: true, 55 | }, 56 | info: { 57 | display: true, 58 | collapsed: true, 59 | }, 60 | }, 61 | ros: { 62 | endpoint: 63 | getURLEndpoint('bridge') || `ws://${window.location.host}/ros/bridge`, 64 | pkgsEndpoint: 65 | getURLEndpoint('pkgs') || `http://${window.location.host}/ros/pkgs`, 66 | }, 67 | infoTabs: [], 68 | visualizations: [], 69 | globalOptions: { 70 | display: true, 71 | backgroundColor: { 72 | display: true, 73 | value: DEFAULT_OPTIONS_SCENE.backgroundColor, 74 | }, 75 | fixedFrame: { 76 | display: true, 77 | value: 'world', 78 | }, 79 | grid: { 80 | display: true, 81 | size: DEFAULT_OPTIONS_SCENE.gridSize, 82 | divisions: DEFAULT_OPTIONS_SCENE.gridDivisions, 83 | color: DEFAULT_OPTIONS_SCENE.gridColor, 84 | centerlineColor: DEFAULT_OPTIONS_SCENE.gridCenterlineColor, 85 | }, 86 | }, 87 | tools: { 88 | mode: 'controls', 89 | controls: { 90 | display: false, 91 | enabled: true, 92 | }, 93 | measure: { 94 | display: false, 95 | }, 96 | custom: [], 97 | }, 98 | }; 99 | 100 | export function updateOptionsUtil(e) { 101 | const { 102 | options: { key }, 103 | updateVizOptions, 104 | } = this.props; 105 | const { 106 | checked, 107 | dataset: { id: optionId }, 108 | value, 109 | } = e.target; 110 | updateVizOptions(key, { 111 | [optionId]: _.has(e.target, 'checked') ? checked : value, 112 | }); 113 | } 114 | 115 | export function promisifyGetNodeDetails(ros, node) { 116 | return new Promise(function(res, rej) { 117 | try { 118 | ros.getNodeDetails(node, function({ publishing, subscribing }) { 119 | res({ publishing, subscribing, node }); 120 | }); 121 | } catch (err) { 122 | rej(err); 123 | } 124 | }); 125 | } 126 | 127 | /** 128 | * 129 | * @param {Array} topics - a list of topics 130 | * @param {Object} nodeDetails - List of node details with node name, publishing topics and subsribing topics 131 | * @returns {auxGraphData} - For creating graph later based on options. 132 | */ 133 | export function createAuxGraph(topics, nodeDetails) { 134 | const auxGraphData = {}; 135 | 136 | topics.forEach(topic => { 137 | auxGraphData[topic] = { publishers: [], subscribers: [] }; 138 | }); 139 | nodeDetails.forEach(function({ publishing: pubs, subscribing: subs, node }) { 140 | pubs.forEach(topic => { 141 | auxGraphData[topic].publishers.push(node); 142 | }); 143 | subs.forEach(topic => { 144 | auxGraphData[topic].subscribers.push(node); 145 | }); 146 | }); 147 | 148 | return auxGraphData; 149 | } 150 | 151 | export function defaultGraph(graph) { 152 | const edges = []; 153 | const { auxGraphData } = graph; 154 | _.each(_.keys(auxGraphData), t => { 155 | const { publishers } = auxGraphData[t]; 156 | const { subscribers } = auxGraphData[t]; 157 | 158 | _.each(publishers, pub => { 159 | _.each(subscribers, sub => { 160 | edges.push({ 161 | source: { id: pub, label: pub }, 162 | target: { id: sub, label: sub }, 163 | value: t, 164 | }); 165 | }); 166 | }); 167 | }); 168 | return { nodes: graph.nodes, edges }; 169 | } 170 | 171 | export function graphWithTopicNodes(graph) { 172 | const newNodes = [...graph.nodes]; 173 | const edges = []; 174 | const { auxGraphData } = graph; 175 | // Adding topic as nodes 176 | _.keys(graph.auxGraphData).forEach(topicName => { 177 | newNodes.push({ 178 | id: topicName + topicName, 179 | label: topicName, 180 | type: 'rect', 181 | }); 182 | }); 183 | 184 | _.each(_.keys(auxGraphData), t => { 185 | const { publishers } = auxGraphData[t]; 186 | const { subscribers } = auxGraphData[t]; 187 | 188 | _.each(publishers, pub => { 189 | edges.push({ 190 | source: { id: pub, label: pub }, 191 | target: { id: t + t, label: t }, 192 | value: '', 193 | }); 194 | }); 195 | 196 | _.each(subscribers, sub => { 197 | edges.push({ 198 | source: { id: t + t, label: t }, 199 | target: { id: sub, label: sub }, 200 | value: '', 201 | }); 202 | }); 203 | }); 204 | 205 | return { nodes: newNodes, edges }; 206 | } 207 | 208 | /** 209 | * 210 | * @param {*} ros - Ros reference 211 | * @returns {Promise} - graph object represents nodes and links as edges. 212 | */ 213 | export function generateGraph(ros) { 214 | const graph = {}; 215 | 216 | return new Promise(function(res, rej) { 217 | ros.getNodes(nodes => { 218 | graph.nodes = _.map(nodes, node => ({ 219 | id: node, 220 | label: node, 221 | type: 'ellipse', 222 | })); 223 | 224 | ros.getTopics(function({ topics }) { 225 | Promise.all( 226 | nodes.map(function(node) { 227 | return promisifyGetNodeDetails(ros, node); 228 | }), 229 | ) 230 | .then(function(data) { 231 | graph.auxGraphData = createAuxGraph(topics, data); 232 | res(graph); 233 | }) 234 | .catch(function(err) { 235 | rej(err); 236 | }); 237 | }); 238 | }); 239 | }); 240 | } 241 | -------------------------------------------------------------------------------- /src/utils/raycaster.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { TOOL_TYPE } from './toolbar'; 3 | 4 | export default class Raycaster extends THREE.Raycaster { 5 | constructor(camera, scene, domElement) { 6 | super(); 7 | this.fixedFrame = 'base_link'; 8 | this.mouse = new THREE.Vector2(); 9 | this.camera = camera; 10 | this.scene = scene; 11 | this.domElement = domElement; 12 | this.activePlane = new THREE.Plane(); 13 | this.intersection = new THREE.Vector3(); 14 | this.mouseDown = new THREE.Vector3(); 15 | this.tool = { name: 'Controls', type: TOOL_TYPE.TOOL_TYPE_CONTROLS }; 16 | this.eventListeners = {}; 17 | this.arrowHelper = new THREE.ArrowHelper( 18 | new THREE.Vector3(0, 1, 0), 19 | this.scene.position, 20 | 1, 21 | 0xffff00, 22 | 0.2, 23 | 0.2, 24 | ); 25 | this.dirv1Cache = new THREE.Vector3(0, 1, 0); 26 | this.dirv2Cache = new THREE.Vector3(); 27 | this.quaternionCache = new THREE.Quaternion(); 28 | this.arrowHelper.line.material.linewidth = 2; 29 | 30 | this.addOrReplaceEventListener = this.addOrReplaceEventListener.bind(this); 31 | this.mouseUpListener = this.mouseUpListener.bind(this); 32 | this.mouseMoveListener = this.mouseMoveListener.bind(this); 33 | this.mouseDownListener = this.mouseDownListener.bind(this); 34 | this.translateToFixedFrame = this.translateToFixedFrame.bind(this); 35 | 36 | this.domElement.addEventListener('mouseup', this.mouseUpListener, false); 37 | this.domElement.addEventListener( 38 | 'mousedown', 39 | this.mouseDownListener, 40 | false, 41 | ); 42 | } 43 | 44 | addOrReplaceEventListener(name, cb) { 45 | this.eventListeners[name] = cb; 46 | } 47 | 48 | setRayDirection(event) { 49 | const rect = this.domElement.getBoundingClientRect(); 50 | const { clientHeight, clientWidth } = this.domElement; 51 | this.mouse.x = ((event.clientX - rect.left) / clientWidth) * 2 - 1; 52 | this.mouse.y = -((event.clientY - rect.top) / clientHeight) * 2 + 1; 53 | this.setFromCamera(this.mouse, this.camera); 54 | } 55 | 56 | mouseDownListener(event) { 57 | this.setRayDirection(event); 58 | this.activePlane.setFromNormalAndCoplanarPoint( 59 | this.camera.up, 60 | this.scene.position, 61 | ); 62 | this.ray.intersectPlane(this.activePlane, this.intersection); 63 | if (!(this.intersection && this.eventListeners[this.tool.name])) { 64 | return; 65 | } 66 | this.translateToFixedFrame(this.intersection); 67 | this.mouseDown.copy(this.intersection); 68 | 69 | switch (this.tool.type) { 70 | case TOOL_TYPE.TOOL_TYPE_POSE_ESTIMATE: 71 | case TOOL_TYPE.TOOL_TYPE_NAV_GOAL: { 72 | this.arrowHelper.position.copy(this.intersection); 73 | this.arrowHelper.quaternion.set(0, 0, 0, 1); 74 | this.scene.add(this.arrowHelper); 75 | this.domElement.addEventListener( 76 | 'mousemove', 77 | this.mouseMoveListener, 78 | false, 79 | ); 80 | break; 81 | } 82 | default: 83 | } 84 | } 85 | 86 | mouseMoveListener(event) { 87 | this.setRayDirection(event); 88 | this.ray.intersectPlane(this.activePlane, this.intersection); 89 | if (!(this.intersection && this.eventListeners[this.tool.name])) { 90 | return; 91 | } 92 | this.translateToFixedFrame(this.intersection); 93 | this.dirv2Cache 94 | .copy(this.intersection) 95 | .sub(this.mouseDown) 96 | .normalize(); 97 | this.quaternionCache.setFromUnitVectors(this.dirv1Cache, this.dirv2Cache); 98 | 99 | this.arrowHelper.quaternion.copy(this.quaternionCache); 100 | } 101 | 102 | mouseUpListener() { 103 | this.domElement.removeEventListener( 104 | 'mousemove', 105 | this.mouseMoveListener, 106 | false, 107 | ); 108 | this.scene.remove(this.arrowHelper); 109 | 110 | switch (this.tool.type) { 111 | case TOOL_TYPE.TOOL_TYPE_POINT: { 112 | this.eventListeners[this.tool.name](this.intersection, this.fixedFrame); 113 | break; 114 | } 115 | case TOOL_TYPE.TOOL_TYPE_POSE_ESTIMATE: 116 | case TOOL_TYPE.TOOL_TYPE_NAV_GOAL: { 117 | const { position, quaternion } = this.arrowHelper; 118 | const quaternionTransform = new THREE.Quaternion().setFromAxisAngle( 119 | this.camera.up, 120 | Math.PI / 2, 121 | ); 122 | quaternion.premultiply(quaternionTransform); 123 | this.eventListeners[this.tool.name]( 124 | position, 125 | quaternion, 126 | this.fixedFrame, 127 | ); 128 | break; 129 | } 130 | case TOOL_TYPE.TOOL_TYPE_CONTROLS: 131 | default: 132 | } 133 | } 134 | 135 | translateToFixedFrame(point) { 136 | const frame = this.scene.getObjectByName(this.fixedFrame); 137 | if (frame) { 138 | frame.worldToLocal(point); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/utils/sanitize.js: -------------------------------------------------------------------------------- 1 | import { get, isNil, map, set, size } from 'lodash'; 2 | 3 | const replacementMap = { 4 | 'sensor_msgs/Image': { 5 | key: 'data', 6 | }, 7 | 'visualization_msgs/MarkerArray': { 8 | key: 'markers', 9 | validation: it => size(it) <= 1000, 10 | }, 11 | 'nav_msgs/OccupancyGrid': { 12 | key: 'data', 13 | }, 14 | 'nav_msgs/Path': { 15 | key: 'poses', 16 | validation: it => size(it) <= 1000, 17 | }, 18 | 'sensor_msgs/PointCloud': { 19 | key: 'points', 20 | }, 21 | 'sensor_msgs/PointCloud2': { 22 | key: 'data', 23 | }, 24 | 'geometry_msgs/Polygon': { 25 | key: 'points', 26 | }, 27 | 'geometry_msgs/PolygonStamped': { 28 | key: 'polygon.points', 29 | }, 30 | 'geometry_msgs/PoseArray': { 31 | key: 'poses', 32 | validation: it => size(it) <= 1000, 33 | }, 34 | }; 35 | 36 | export const sanitizeMessage = (topic, message) => { 37 | // message is mutated for speed 38 | const { keys, messageType } = topic; 39 | const keysSet = new Set(keys); 40 | const filteredMessage = message; 41 | if (size(keys) !== 0) { 42 | map(filteredMessage, (value, key) => { 43 | if (!keysSet.has(key)) { 44 | delete filteredMessage[key]; 45 | } 46 | }); 47 | } 48 | if (isNil(replacementMap[messageType])) { 49 | return filteredMessage; 50 | } 51 | const { key, validation } = replacementMap[messageType]; 52 | const value = get(filteredMessage, key); 53 | if (validation && value && validation(value)) { 54 | return filteredMessage; 55 | } 56 | // only set if already not filtered 57 | if (value) { 58 | set(message, key, '...'); 59 | } 60 | return message; 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils/toolPublisher.js: -------------------------------------------------------------------------------- 1 | import ROSLIB from 'roslib'; 2 | import { 3 | TOOL_TYPE_NAV_GOAL, 4 | TOOL_TYPE_POINT, 5 | TOOL_TYPE_POSE_ESTIMATE, 6 | } from './common'; 7 | 8 | export default class ToolPublisher { 9 | constructor(ros) { 10 | this.ros = ros; 11 | this.seq = { 12 | [TOOL_TYPE_POINT]: 0, 13 | [TOOL_TYPE_NAV_GOAL]: 0, 14 | [TOOL_TYPE_POSE_ESTIMATE]: 0, 15 | }; 16 | 17 | this.pointToolPublisher = new ROSLIB.Topic({ 18 | ros: this.ros, 19 | name: '/clicked_point', 20 | messageType: 'geometry_msgs/PointStamped', 21 | }); 22 | this.navGoalToolPublisher = new ROSLIB.Topic({ 23 | ros: this.ros, 24 | name: '/move_base_simple/goal', 25 | messageType: 'geometry_msgs/PoseStamped', 26 | }); 27 | this.poseEstimateToolPublisher = new ROSLIB.Topic({ 28 | ros: this.ros, 29 | name: '/initialpose', 30 | messageType: 'geometry_msgs/PoseWithCovarianceStamped', 31 | }); 32 | 33 | this.pointToolPublisher.advertise(); 34 | this.navGoalToolPublisher.advertise(); 35 | this.poseEstimateToolPublisher.advertise(); 36 | 37 | this.publishPointToolMessage = this.publishPointToolMessage.bind(this); 38 | this.publishNavGoalToolMessage = this.publishNavGoalToolMessage.bind(this); 39 | this.publishPoseEstimateToolMessage = this.publishPoseEstimateToolMessage.bind( 40 | this, 41 | ); 42 | } 43 | 44 | publishPointToolMessage(point, frameId) { 45 | const message = new ROSLIB.Message({ 46 | header: { 47 | seq: this.seq[TOOL_TYPE_POINT], 48 | stamp: { 49 | secs: Math.floor(Date.now() / 1000), 50 | nsecs: 0, 51 | }, 52 | frame_id: frameId, 53 | }, 54 | point, 55 | }); 56 | this.pointToolPublisher.publish(message); 57 | this.seq[TOOL_TYPE_POINT]++; 58 | } 59 | 60 | publishNavGoalToolMessage(pose, frameId) { 61 | const message = new ROSLIB.Message({ 62 | header: { 63 | seq: this.seq[TOOL_TYPE_NAV_GOAL], 64 | stamp: { 65 | secs: Math.floor(Date.now() / 1000), 66 | nsecs: 0, 67 | }, 68 | frame_id: frameId, 69 | }, 70 | pose, 71 | }); 72 | this.navGoalToolPublisher.publish(message); 73 | this.seq[TOOL_TYPE_NAV_GOAL]++; 74 | } 75 | 76 | publishPoseEstimateToolMessage(pose, frameId) { 77 | // covariance being published here is meaningless 78 | // but we keep the same covariance as rviz does 79 | // for compatibility 80 | const covariance = new Array(36).fill(0); 81 | covariance[0] = 0.5 * 0.5; 82 | covariance[6 + 1] = 0.5 * 0.5; 83 | covariance[6 * 6 - 1] = ((Math.PI / 12.0) * Math.PI) / 12.0; 84 | 85 | const message = new ROSLIB.Message({ 86 | header: { 87 | seq: this.seq[TOOL_TYPE_POSE_ESTIMATE], 88 | stamp: { 89 | secs: Math.floor(Date.now() / 1000), 90 | nsecs: 0, 91 | }, 92 | frame_id: frameId, 93 | }, 94 | pose: { 95 | pose, 96 | covariance, 97 | }, 98 | }); 99 | this.poseEstimateToolPublisher.publish(message); 100 | this.seq[TOOL_TYPE_POSE_ESTIMATE]++; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/toolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | iconLineStyle, 4 | MESSAGE_TYPE_TOOL_NAV_GOAL, 5 | MESSAGE_TYPE_TOOL_POINT, 6 | MESSAGE_TYPE_TOOL_POSE_ESTIMATE, 7 | TOOL_TYPE_CONTROLS, 8 | TOOL_TYPE_NAV_GOAL, 9 | TOOL_TYPE_POINT, 10 | TOOL_TYPE_POSE_ESTIMATE, 11 | } from './common'; 12 | 13 | const activeStyle = { 14 | ...iconLineStyle, 15 | strokeWidth: '1px', 16 | }; 17 | const inactiveStyle = { 18 | ...activeStyle, 19 | stroke: '#000000', 20 | }; 21 | 22 | export const TOOL_TYPE = { 23 | [TOOL_TYPE_CONTROLS]: TOOL_TYPE_CONTROLS, 24 | [TOOL_TYPE_POINT]: TOOL_TYPE_POINT, 25 | [TOOL_TYPE_NAV_GOAL]: TOOL_TYPE_NAV_GOAL, 26 | [TOOL_TYPE_POSE_ESTIMATE]: TOOL_TYPE_POSE_ESTIMATE, 27 | }; 28 | 29 | export const toolOptions = [ 30 | { 31 | name: 'Controls', 32 | type: TOOL_TYPE_CONTROLS, 33 | icon: active => ( 34 | 35 | 36 | 40 | 41 | 42 | ), 43 | }, 44 | { 45 | name: 'Pose Estimate', 46 | type: TOOL_TYPE_POSE_ESTIMATE, 47 | icon: active => ( 48 | 49 | 50 | 57 | 61 | 62 | 63 | ), 64 | messageType: MESSAGE_TYPE_TOOL_POSE_ESTIMATE, 65 | }, 66 | { 67 | name: 'Nav Goal', 68 | type: TOOL_TYPE_NAV_GOAL, 69 | icon: active => ( 70 | 71 | 72 | 79 | 83 | 84 | 85 | ), 86 | messageType: MESSAGE_TYPE_TOOL_NAV_GOAL, 87 | }, 88 | { 89 | name: 'Point', 90 | type: TOOL_TYPE_POINT, 91 | icon: active => ( 92 | 93 | 94 | 98 | 99 | 100 | ), 101 | messageType: MESSAGE_TYPE_TOOL_POINT, 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /src/zethus.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import shortid from 'shortid'; 4 | import withGracefulUnmount from 'react-graceful-unmount'; 5 | import store from 'store'; 6 | 7 | import Panels from './panels'; 8 | 9 | import { DEFAULT_CONFIG } from './utils'; 10 | import ErrorBoundary from './components/errorBoundary'; 11 | 12 | class Zethus extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | const urlSearchParams = new URLSearchParams(window.location.search); 17 | const urlParams = Object.fromEntries(urlSearchParams.entries()); 18 | const urlConfig = urlParams.config 19 | ? JSON.parse(urlParams.config) 20 | : undefined; 21 | this.zethusId = urlParams.zethusId ? urlParams.zethusId : undefined; 22 | 23 | const providedConfig = 24 | props.configuration || urlConfig || store.get('zethus_config') || {}; 25 | 26 | // Empty object is required or the merge function mutates default config 27 | window.document.addEventListener('SetConfig', e => { 28 | this.updateConfiguration(e.config, e.replaceOnExisting || false); 29 | }); 30 | 31 | this.state = { 32 | configuration: _.merge({}, DEFAULT_CONFIG, providedConfig), 33 | }; 34 | this.updateVizOptions = this.updateVizOptions.bind(this); 35 | this.updateRosEndpoint = this.updateRosEndpoint.bind(this); 36 | this.updateGlobalOptions = this.updateGlobalOptions.bind(this); 37 | this.addVisualization = this.addVisualization.bind(this); 38 | this.removeVisualization = this.removeVisualization.bind(this); 39 | this.toggleVisibility = this.toggleVisibility.bind(this); 40 | this.updateConfiguration = this.updateConfiguration.bind(this); 41 | this.resetReload = this.resetReload.bind(this); 42 | } 43 | 44 | updateConfiguration(configuration, replaceOnExisting) { 45 | const { configuration: oldConfiguration } = this.state; 46 | let newConfiguration; 47 | if (replaceOnExisting) { 48 | newConfiguration = { 49 | ...oldConfiguration, 50 | ...configuration, 51 | }; 52 | } else { 53 | newConfiguration = { 54 | ..._.merge(oldConfiguration, configuration), 55 | }; 56 | } 57 | 58 | if (window.parent && this.zethusId) { 59 | const event = new CustomEvent(`ZethusUpdateConfig${this.zethusId}`, { 60 | detail: { config: newConfiguration }, 61 | }); 62 | window.parent.document.dispatchEvent(event); 63 | } 64 | 65 | this.setState({ configuration: newConfiguration }); 66 | } 67 | 68 | updateVizOptions(key, options) { 69 | const { 70 | configuration: { visualizations }, 71 | } = this.state; 72 | this.updateConfiguration( 73 | { 74 | visualizations: _.map(visualizations, v => 75 | v.key === key ? { ...v, ...options } : v, 76 | ), 77 | }, 78 | true, 79 | ); 80 | } 81 | 82 | updateRosEndpoint(endpoint) { 83 | const { 84 | configuration: { ros }, 85 | } = this.state; 86 | this.updateConfiguration({ 87 | ros: { 88 | ...ros, 89 | endpoint, 90 | }, 91 | }); 92 | } 93 | 94 | componentWillUnmount() { 95 | const { configuration } = this.state; 96 | store.set('zethus_config', configuration); 97 | } 98 | 99 | updateGlobalOptions(path, option) { 100 | const { 101 | configuration: { globalOptions }, 102 | } = this.state; 103 | const clonedGlobalOptions = _.cloneDeep(globalOptions); 104 | _.set(clonedGlobalOptions, path, option); 105 | this.updateConfiguration( 106 | { 107 | globalOptions: clonedGlobalOptions, 108 | }, 109 | true, 110 | ); 111 | } 112 | 113 | removeVisualization(e) { 114 | const { 115 | dataset: { id: vizId }, 116 | } = e.target; 117 | const { 118 | configuration: { visualizations }, 119 | } = this.state; 120 | this.updateConfiguration( 121 | { 122 | visualizations: _.filter(visualizations, v => v.key !== vizId), 123 | }, 124 | true, 125 | ); 126 | } 127 | 128 | toggleVisibility(e) { 129 | const { 130 | dataset: { id: vizId }, 131 | } = e.target; 132 | const { 133 | configuration: { visualizations }, 134 | } = this.state; 135 | this.updateConfiguration( 136 | { 137 | visualizations: _.map(visualizations, v => 138 | v.key === vizId 139 | ? { 140 | ...v, 141 | visible: !!(_.isBoolean(v.visible) && !v.visible), 142 | } 143 | : v, 144 | ), 145 | }, 146 | true, 147 | ); 148 | } 149 | 150 | resetReload() { 151 | this.setState( 152 | { 153 | configuration: DEFAULT_CONFIG, 154 | }, 155 | () => { 156 | window.location.reload(); 157 | }, 158 | ); 159 | } 160 | 161 | addVisualization(vizOptions) { 162 | const { 163 | configuration: { visualizations }, 164 | } = this.state; 165 | this.updateConfiguration({ 166 | visualizations: [ 167 | ...visualizations, 168 | { 169 | ...vizOptions, 170 | key: shortid.generate(), 171 | }, 172 | ], 173 | }); 174 | } 175 | 176 | render() { 177 | const { configuration } = this.state; 178 | return ( 179 | 183 | 193 | 194 | ); 195 | } 196 | } 197 | 198 | export default withGracefulUnmount(Zethus); 199 | -------------------------------------------------------------------------------- /webpack.app.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: './src/index.jsx', 8 | mode: 'development', 9 | plugins: [ 10 | new HtmlWebpackPlugin({ 11 | template: './public/index.html', 12 | filename: 'index.html', 13 | templateParameters: { 14 | PUBLIC_URL: '', 15 | }, 16 | }), 17 | new CopyPlugin([{ from: './public' }]), 18 | new CleanWebpackPlugin(), 19 | ], 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.css$/i, 24 | use: ['style-loader', 'css-loader'], 25 | }, 26 | { 27 | test: /\.s[ac]ss$/i, 28 | use: ['style-loader', 'css-loader', 'sass-loader'], 29 | }, 30 | { 31 | test: /\.(png|jpe?g|gif|svg)$/i, 32 | use: [ 33 | { 34 | loader: 'file-loader', 35 | }, 36 | ], 37 | }, 38 | { 39 | test: /\.(js|jsx)$/i, 40 | exclude: /node_modules/, 41 | loader: 'babel-loader', 42 | options: { 43 | presets: ['@babel/preset-env'], 44 | }, 45 | }, 46 | ], 47 | }, 48 | resolve: { 49 | extensions: ['.js', '.jsx'], 50 | alias: { 51 | three: path.resolve('./node_modules/three'), 52 | }, 53 | }, 54 | devServer: { 55 | compress: true, 56 | hot: true, 57 | port: 3000, 58 | quiet: false, 59 | noInfo: false, 60 | stats: { 61 | assets: false, 62 | children: false, 63 | chunks: false, 64 | chunkModules: false, 65 | colors: true, 66 | entrypoints: false, 67 | hash: false, 68 | modules: false, 69 | timings: false, 70 | version: false, 71 | }, 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /webpack.app.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | 7 | const PUBLIC_URL = require('./package').homepage; 8 | 9 | module.exports = { 10 | entry: './src/index.jsx', 11 | mode: 'production', 12 | devtool: 'source-map', 13 | optimization: { 14 | splitChunks: { 15 | chunks: 'async', 16 | minSize: 30000, 17 | maxSize: 0, 18 | minChunks: 1, 19 | maxAsyncRequests: 5, 20 | maxInitialRequests: 3, 21 | automaticNameDelimiter: '~', 22 | automaticNameMaxLength: 30, 23 | name: true, 24 | cacheGroups: { 25 | vendors: { 26 | test: /[\\/]node_modules[\\/]/, 27 | priority: -10, 28 | }, 29 | default: { 30 | minChunks: 2, 31 | priority: -20, 32 | reuseExistingChunk: true, 33 | }, 34 | }, 35 | }, 36 | }, 37 | plugins: [ 38 | new webpack.HashedModuleIdsPlugin(), 39 | new HtmlWebpackPlugin({ 40 | template: './public/index.html', 41 | filename: 'index.html', 42 | templateParameters: { 43 | PUBLIC_URL, 44 | }, 45 | }), 46 | new CopyPlugin([{ from: './public' }]), 47 | new CleanWebpackPlugin(), 48 | ], 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.css$/i, 53 | use: ['style-loader', 'css-loader'], 54 | }, 55 | { 56 | test: /\.s[ac]ss$/i, 57 | use: ['style-loader', 'css-loader', 'sass-loader'], 58 | }, 59 | { 60 | test: /\.(png|jpe?g|gif|svg)$/i, 61 | use: [ 62 | { 63 | loader: 'file-loader', 64 | }, 65 | ], 66 | }, 67 | { 68 | test: /\.(js|jsx)$/i, 69 | exclude: /node_modules/, 70 | loader: 'babel-loader', 71 | options: { 72 | presets: ['@babel/preset-env'], 73 | }, 74 | }, 75 | ], 76 | }, 77 | resolve: { 78 | extensions: ['.js', '.jsx'], 79 | alias: { 80 | three: path.resolve('./node_modules/three'), 81 | }, 82 | }, 83 | output: { 84 | filename: 'index.[hash].js', 85 | path: path.resolve(__dirname, 'build'), 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /webpack.lib.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | 7 | const PUBLIC_URL = require('./package').homepage; 8 | 9 | module.exports = { 10 | entry: { 11 | zethus: './src/zethus.jsx', 12 | panels: './src/panels/index.jsx', 13 | }, 14 | mode: 'production', 15 | devtool: 'source-map', 16 | optimization: { 17 | splitChunks: { 18 | chunks: 'async', 19 | minSize: 30000, 20 | maxSize: 0, 21 | minChunks: 1, 22 | maxAsyncRequests: 5, 23 | maxInitialRequests: 3, 24 | automaticNameDelimiter: '~', 25 | automaticNameMaxLength: 30, 26 | name: true, 27 | cacheGroups: { 28 | vendors: { 29 | test: /[\\/]node_modules[\\/]/, 30 | priority: -10, 31 | }, 32 | default: { 33 | minChunks: 2, 34 | priority: -20, 35 | reuseExistingChunk: true, 36 | }, 37 | }, 38 | }, 39 | }, 40 | plugins: [ 41 | new webpack.HashedModuleIdsPlugin(), 42 | new HtmlWebpackPlugin({ 43 | template: './public/index.html', 44 | filename: 'index.html', 45 | templateParameters: { 46 | PUBLIC_URL, 47 | }, 48 | }), 49 | new CopyPlugin([{ from: './public' }]), 50 | new CleanWebpackPlugin(), 51 | ], 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.css$/i, 56 | use: ['style-loader', 'css-loader'], 57 | }, 58 | { 59 | test: /\.s[ac]ss$/i, 60 | use: ['style-loader', 'css-loader', 'sass-loader'], 61 | }, 62 | { 63 | test: /\.(png|jpe?g|gif|svg)$/i, 64 | use: [ 65 | { 66 | loader: 'file-loader', 67 | }, 68 | ], 69 | }, 70 | { 71 | test: /\.(js|jsx)$/i, 72 | exclude: /node_modules/, 73 | loader: 'babel-loader', 74 | options: { 75 | presets: ['@babel/preset-env'], 76 | }, 77 | }, 78 | ], 79 | }, 80 | resolve: { 81 | extensions: ['.js', '.jsx'], 82 | alias: { 83 | three: path.resolve('./node_modules/three'), 84 | }, 85 | }, 86 | output: { 87 | filename: '[name].umd.js', 88 | libraryTarget: 'umd', 89 | path: path.resolve(__dirname, 'build-lib'), 90 | }, 91 | }; 92 | --------------------------------------------------------------------------------