├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── docs.yml │ ├── ironpython.yml │ ├── pr-checks.yml │ └── release.yml ├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── conftest.py ├── docs ├── _images │ ├── Application_1.png │ ├── Application_2.png │ ├── Application_3.png │ ├── Application_Overview_1.png │ ├── Application_Overview_2.png │ ├── Application_Settings.png │ ├── Assembly_BuildingPlan.png │ ├── Building_Plan_Multi_User_Interaction.png │ ├── Building_Plan_Priority_Illustration.png │ ├── Building_Plan_Step.png │ ├── Building_Plan_Structure_Example.png │ ├── Built_Human_Robot.png │ ├── Data_Structure_Example.png │ ├── Docker_1.png │ ├── Docker_2.png │ ├── Docker_3.png │ ├── Docker_4.png │ ├── Docker_5.png │ ├── Docker_6.png │ ├── Docker_7.png │ ├── Docker_8.png │ ├── Firebase_Data_Management.png │ ├── Frame.png │ ├── Frames_Wall.png │ ├── Grasshopper_2.png │ ├── Grasshopper_3.png │ ├── Grasshopper_4.png │ ├── Grasshopper_5.png │ ├── Grasshopper_6.png │ ├── Grasshopper_7.png │ ├── Services_Overview.png │ ├── android_1.png │ ├── android_2.png │ ├── android_3.png │ ├── android_4.png │ ├── android_5.png │ ├── android_6.png │ ├── android_7.png │ ├── android_8.png │ ├── android_9.png │ ├── app_01.png │ ├── compas_xr_lead_image.png │ ├── example_1.jpg │ ├── example_2.jpg │ ├── example_3.jpg │ ├── firebase_1.png │ ├── firebase_10.png │ ├── firebase_11.png │ ├── firebase_12.png │ ├── firebase_13.png │ ├── firebase_14.png │ ├── firebase_15.png │ ├── firebase_16.png │ ├── firebase_17.png │ ├── firebase_18.png │ ├── firebase_2.png │ ├── firebase_3.png │ ├── firebase_4.png │ ├── firebase_5.png │ ├── firebase_6.png │ ├── firebase_7.png │ ├── firebase_8.png │ ├── firebase_9.png │ ├── firebase_packages.png │ ├── grasshopper_1.png │ ├── ios_1.png │ ├── ios_10.png │ ├── ios_11.png │ ├── ios_12.png │ ├── ios_13.png │ ├── ios_14.png │ ├── ios_15.png │ ├── ios_2.png │ ├── ios_3.png │ ├── ios_4.png │ ├── ios_5.png │ ├── ios_6.png │ ├── ios_7.png │ ├── ios_8.png │ ├── ios_9.png │ ├── software_architecture.png │ ├── ui_01.png │ ├── ui_02.png │ ├── ui_03.png │ └── workflow.png ├── _static │ └── qr_codes │ │ └── compas_xr_qrs.pdf ├── api.rst ├── api │ ├── compas_xr.ghpython.rst │ ├── compas_xr.mqtt.rst │ ├── compas_xr.project.rst │ ├── compas_xr.realtime_database.rst │ ├── compas_xr.rst │ ├── compas_xr.storage.rst │ └── compas_xr_unity.rst ├── citing.rst ├── conf.py ├── examples │ └── scripts │ │ ├── docker │ │ └── docker-compose.yml │ │ ├── ex1_assembly_definition_and_firebaseupload.ghx │ │ ├── ex2_robotic_trajectory_visualization_example.ghx │ │ └── firebase_config │ │ └── compas_xr_example.json ├── index.rst ├── installation.rst ├── license.rst └── userguide.rst ├── environment.yml ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── src └── compas_xr │ ├── __init__.py │ ├── __main__.py │ ├── dependencies │ ├── Firebase.Auth.dll │ ├── Firebase.Auth.xml │ ├── Firebase.Storage.dll │ ├── Firebase.dll │ ├── Firebase.xml │ ├── LiteDB.dll │ ├── LiteDB.xml │ ├── Newtonsoft.Json.dll │ ├── Newtonsoft.Json.xml │ ├── System.Reactive.dll │ └── System.Reactive.xml │ ├── ghpython │ ├── __init__.py │ ├── app_settings.py │ ├── components │ │ ├── Cx_AppSettings │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_Firebase_Config │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_GetTrajectoryRequest │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_MqttTrajectoryResult │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_PlanningServiceResponse │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_SendTrajectory │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ ├── Cx_XrOptions │ │ │ ├── code.py │ │ │ ├── icon.png │ │ │ └── metadata.json │ │ └── ___init__.py │ ├── firebase_config.py │ ├── options.py │ └── trajectory_manager.py │ ├── mqtt │ ├── __init__.py │ └── messages.py │ ├── project │ ├── __init__.py │ ├── assembly_extensions.py │ ├── buildingplan_extensions.py │ └── project_manager.py │ ├── realtime_database │ ├── __init__.py │ ├── realtime_database_cli.py │ ├── realtime_database_interface.py │ └── realtime_database_pyrebase.py │ ├── rhino │ ├── __init__.py │ └── install.py │ └── storage │ ├── __init__.py │ ├── storage_cli.py │ ├── storage_interface.py │ └── storage_pyrebase.py ├── tasks.py ├── temp └── PLACEHOLDER └── tests ├── compas_xr └── project │ └── test_project_manager.py └── ipy_test_runner.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | charset = utf-8 12 | max_line_length = 179 13 | 14 | [*.{bat,cmd,ps1}] 15 | end_of_line = crlf 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | indent_size = 4 23 | 24 | [*.yml] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [LICENSE] 29 | insert_final_newline = false 30 | -------------------------------------------------------------------------------- /.github/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 both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | 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 brg@arch.ethz.ch. 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | 13 | Steps to reproduce the behavior: 14 | 15 | 1. Context [e.g. ST3, Rhino, Blender, ...] 16 | 2. Sample script 17 | 3. Sample data 18 | 4. See error 19 | 20 | **Expected behavior** 21 | 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | 30 | - OS: [e.g. iOS] 31 | - Python version [e.g. 2.7] 32 | - Python package manager [e.g. macports, pip, conda] 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | # Feature Request 7 | 8 | As a [role], I want [something] so that [benefit]. 9 | 10 | ## Details 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | 14 | A clear and concise description of what the problem is. 15 | 16 | **Describe the solution you'd like.** 17 | 18 | A clear and concise description of what you want to happen. 19 | 20 | **Describe alternatives you've considered.** 21 | 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | 24 | **Additional context.** 25 | 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### What type of change is this? 6 | 7 | - [ ] Bug fix in a **backwards-compatible** manner. 8 | - [ ] New feature in a **backwards-compatible** manner. 9 | - [ ] Breaking change: bug fix or new feature that involve incompatible API changes. 10 | - [ ] Other (e.g. doc update, configuration, etc) 11 | 12 | ### Checklist 13 | 14 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 15 | 16 | - [ ] I added a line to the `CHANGELOG.md` file in the `Unreleased` section under the most fitting heading (e.g. `Added`, `Changed`, `Removed`). 17 | - [ ] I ran all tests on my computer and it's all green (i.e. `invoke test`). 18 | - [ ] I ran lint on my computer and there are no errors (i.e. `invoke lint`). 19 | - [ ] I added new functions/classes and made them available on a second-level import, e.g. `compas.datastructures.Mesh`. 20 | - [ ] I have added tests that prove my fix is effective or that my feature works. 21 | - [ ] I have added necessary documentation (if appropriate) 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | if: "!contains(github.event.pull_request.labels.*.name, 'docs-only')" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | python: ["3.9", "3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - uses: compas-dev/compas-actions.build@v4 22 | with: 23 | invoke_lint: true 24 | invoke_test: true 25 | python: ${{ matrix.python }} 26 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request_review: 10 | types: [submitted] 11 | 12 | jobs: 13 | docs: 14 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: compas-dev/compas-actions.docs@v3 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | doc_url: https://compas.dev/compas_xr 21 | -------------------------------------------------------------------------------- /.github/workflows/ironpython.yml: -------------------------------------------------------------------------------- 1 | name: ironpython 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: windows-ironpython 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: | 17 | echo "Installing IronPython..." 18 | choco install ironpython --version=2.7.8.1 19 | 20 | echo "Downloading ironpython-pytest..." 21 | curl -o ironpython-pytest.tar.gz -LJO https://pypi.debian.net/ironpython-pytest/latest 22 | 23 | echo "Downloading COMPAS..." 24 | curl -o compas.tar.gz -LJO https://pypi.debian.net/COMPAS/COMPAS-2.1.0.tar.gz 25 | 26 | echo "Downloading compas_robots..." 27 | curl -o compas_robots.tar.gz -LJO https://pypi.debian.net/compas_robots/latest 28 | 29 | echo "Downloading compas_fab..." 30 | curl -o compas_fab.tar.gz -LJO https://pypi.debian.net/compas_fab/latest 31 | 32 | echo "Downloading compas_eve..." 33 | curl -o compas_eve.tar.gz -LJO https://pypi.debian.net/compas_eve/latest 34 | 35 | echo "Downloading compas_timber..." 36 | curl -o compas_timber.tar.gz -LJO https://pypi.debian.net/compas_timber/compas_timber-0.7.0.tar.gz 37 | 38 | echo "Setting up IronPython environment..." 39 | ipy -X:Frames -m ensurepip 40 | 41 | echo "Installing ironpython-pytest..." 42 | ipy -X:Frames -m pip install --no-deps ironpython-pytest.tar.gz 43 | 44 | echo "Installing COMPAS..." 45 | ipy -X:Frames -m pip install --no-deps compas.tar.gz 46 | 47 | echo "Installing compas_robots..." 48 | ipy -X:Frames -m pip install --no-deps compas_robots.tar.gz 49 | 50 | echo "Installing compas_fab..." 51 | ipy -X:Frames -m pip install --no-deps compas_fab.tar.gz 52 | 53 | echo "Installing compas_eve..." 54 | ipy -X:Frames -m pip install --no-deps compas_eve.tar.gz 55 | 56 | echo "Installing compas_timber..." 57 | ipy -X:Frames -m pip install --no-deps compas_timber.tar.gz 58 | 59 | - uses: NuGet/setup-nuget@v1.0.5 60 | - uses: compas-dev/compas-actions.ghpython_components@v5 61 | with: 62 | source: src/compas_xr/ghpython/components 63 | target: src/compas_xr/ghpython/components/ghuser 64 | - name: Test import 65 | run: | 66 | echo "Testing import of compas_xr..." 67 | ipy -m compas_xr 68 | env: 69 | IRONPYTHONPATH: ./src 70 | - name: Run tests 71 | run: | 72 | echo "Running tests..." 73 | ipy tests/ipy_test_runner.py 74 | env: 75 | IRONPYTHONPATH: ./src 76 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: verify-pr-checklist 2 | on: 3 | pull_request: 4 | types: [assigned, opened, synchronize, reopened, labeled, unlabeled] 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Check Actions 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Changelog check 16 | uses: Zomzog/changelog-checker@v1.2.0 17 | with: 18 | fileName: CHANGELOG.md 19 | checkNotification: Simple 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [macos-latest, windows-latest, ubuntu-latest] 14 | python: ["3.10", "3.11"] 15 | 16 | steps: 17 | - uses: compas-dev/compas-actions.build@v4 18 | with: 19 | python: ${{ matrix.python }} 20 | invoke_lint: true 21 | check_import: true 22 | 23 | publish: 24 | needs: build 25 | runs-on: windows-latest 26 | steps: 27 | - uses: compas-dev/compas-actions.publish@v3 28 | with: 29 | pypi_token: ${{ secrets.PYPI }} 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | build_ghpython_components: true 32 | gh_source: src/compas_xr/ghpython/components 33 | gh_target: src/compas_xr/ghpython/components/ghuser 34 | gh_prefix: "COMPAS XR: " 35 | gh_interpreter: "ironpython" 36 | release_name_prefix: COMPAS XR 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # ============================================================================== 104 | # compas_xr 105 | # ============================================================================== 106 | 107 | *.3dmbak 108 | *.rhl 109 | *.rui_bak 110 | 111 | temp/** 112 | !temp/PLACEHOLDER 113 | 114 | .DS_Store 115 | 116 | .vscode 117 | 118 | docs/api/generated/ 119 | 120 | conda.recipe/ 121 | 122 | scripts/firebase_config.json 123 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | - Joseph Kenny <> [@jckenny59](https://github.com/jckenny59) 4 | - Daniela Mitterberger <> [@dmitterberger](https://github.com/dmitterberger) 5 | - Gonzalo Casas <> [@gonzalocasas](https://github.com/gonzalocasas) 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ### Added 11 | 12 | ### Changed 13 | 14 | ### Removed 15 | 16 | 17 | ## [1.0.0] 2024-06-26 18 | 19 | ### Added 20 | 21 | ### Changed 22 | 23 | ### Removed 24 | 25 | 26 | ## [0.9.5] 2024-06-24 27 | 28 | ### Added 29 | 30 | ### Changed 31 | 32 | ### Removed 33 | 34 | 35 | ## [0.9.4] 2024-06-24 36 | 37 | ### Changed 38 | 39 | * Fixed missing dependency for GH. 40 | 41 | ## [0.9.3] 2024-06-24 42 | 43 | ### Added 44 | 45 | * Add GH components to release. 46 | 47 | 48 | ## [0.9.2] 2024-06-24 49 | 50 | ### Added 51 | 52 | ### Changed 53 | 54 | ### Removed 55 | 56 | 57 | ## [0.9.1] 2024-06-24 58 | 59 | * Zenodo DOI generation. 60 | 61 | ## [0.9.0] 2024-06-24 62 | 63 | * Initial release. 64 | 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and very much appreciated! 4 | 5 | ## Code contributions 6 | 7 | We accept code contributions through pull requests. 8 | In short, this is how that works. 9 | 10 | 1. Fork [the repository](https://github.com/compas-dev/compas_xr) and clone the fork. 11 | 2. Create a virtual environment using your tool of choice (e.g. `virtualenv`, `conda`, etc). 12 | 3. Install development dependencies: 13 | 14 | ```bash 15 | pip install -e .[dev] 16 | ``` 17 | 18 | 4. Make sure all tests pass: 19 | 20 | ```bash 21 | invoke test 22 | ``` 23 | 24 | 5. Start making your changes to the **master** branch (or branch off of it). 25 | 6. Make sure all tests still pass: 26 | 27 | ```bash 28 | invoke test 29 | ``` 30 | 31 | 7. Add yourself to the *Contributors* section of `AUTHORS.md`. 32 | 8. Commit your changes and push your branch to GitHub. 33 | 9. Create a [pull request](https://help.github.com/articles/about-pull-requests/) through the GitHub website. 34 | 35 | During development, use [pyinvoke](http://docs.pyinvoke.org/) tasks on the 36 | command line to ease recurring operations: 37 | 38 | * `invoke clean`: Clean all generated artifacts. 39 | * `invoke check`: Run various code and documentation style checks. 40 | * `invoke docs`: Generate documentation. 41 | * `invoke test`: Run all tests and checks in one swift command. 42 | * `invoke`: Show available tasks. 43 | 44 | ## Bug reports 45 | 46 | When [reporting a bug](https://github.com/compas-dev/compas_xr/issues) please include: 47 | 48 | * Operating system name and version. 49 | * Any details about your local setup that might be helpful in troubleshooting. 50 | * Detailed steps to reproduce the bug. 51 | 52 | ## Feature requests 53 | 54 | When [proposing a new feature](https://github.com/compas-dev/compas_xr/issues) please include: 55 | 56 | * Explain in detail how it would work. 57 | * Keep the scope as narrow as possible, to make it easier to implement. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 ETH Zurich, Princeton University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src 2 | 3 | prune .github 4 | prune data 5 | prune docs 6 | prune scripts 7 | prune tests 8 | prune temp 9 | 10 | include LICENSE 11 | include README.md 12 | include AUTHORS.md 13 | include CHANGELOG.md 14 | include requirements.txt 15 | 16 | exclude requirements-dev.txt 17 | exclude pytest.ini .bumpversion.cfg .editorconfig 18 | exclude tasks.py 19 | exclude CONTRIBUTING.md 20 | exclude conftest.py 21 | 22 | global-exclude *.py[cod] __pycache__ *.dylib *.nb[ic] .DS_Store 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COMPAS XR 2 | 3 | [![Github Actions Build Status](https://github.com/compas-dev/compas_xr/workflows/build/badge.svg)](https://github.com/compas-dev/compas_xr/actions) 4 | [![License](https://img.shields.io/github/license/compas-dev/compas_xr.svg)](https://pypi.python.org/pypi/compas_xr) 5 | [![pip downloads](https://img.shields.io/pypi/dm/compas_xr)](https://pypi.python.org/project/compas_xr) 6 | [![PyPI Package latest release](https://img.shields.io/pypi/v/compas_xr.svg)](https://pypi.python.org/pypi/compas_xr) 7 | [![Supported implementations](https://img.shields.io/pypi/implementation/compas_xr.svg)](https://pypi.python.org/pypi/compas_xr) 8 | [![DOI](https://zenodo.org/badge/247674503.svg)](https://zenodo.org/doi/10.5281/zenodo.12514526) 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/compas_dev?style=social)](https://twitter.com/compas_dev) 10 | 11 | ![COMPAS XR](https://raw.githubusercontent.com/compas-dev/compas_xr/main/docs/_images/compas_xr_lead_image.png) 12 | 13 | `COMPAS XR` streamlines extended reality workflows to ease the implementation of human-machine collaborative applications in architectural research and educational environments. 14 | 15 | ## Installation 16 | 17 | > It is recomended you install `compas_xr` inside a virtual environment. 18 | 19 | ```bash 20 | pip install compas_xr 21 | ``` 22 | 23 | To install `compas_xr` to Rhino run 24 | 25 | ```bash 26 | python -m compas_rhino.install 27 | ``` 28 | 29 | ## First Steps 30 | 31 | * [Documentation](https://compas.dev/compas_xr/) 32 | * [User guide](https://compas.dev/compas_xr/latest/userguide.html) 33 | * [API Reference](https://compas.dev/compas_xr/latest/api.html) 34 | 35 | ## Questions and feedback 36 | 37 | We encourage the use of the [COMPAS framework forum](https://forum.compas-framework.org/) 38 | for questions and discussions. 39 | 40 | ## Issue tracker 41 | 42 | If you found an issue or have a suggestion for a dandy new feature, please file a new issue in our [issue tracker](https://github.com/compas-dev/compas_xr/issues). 43 | 44 | ## Contributing 45 | 46 | We love contributions! 47 | 48 | Check the [Contributor's Guide](https://github.com/compas-dev/compas_xr/blob/main/CONTRIBUTING.md) 49 | for more details. 50 | 51 | ## Credits 52 | 53 | `compas_xr` is currently developed by ETH Zurich (Gramazio Kohler Research) and Princeton University. See the [list of authors](https://github.com/compas-dev/compas_xr/blob/main/AUTHORS.md) for a complete overview. 54 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import compas 4 | import numpy 5 | import pytest 6 | 7 | import compas_xr 8 | 9 | 10 | def pytest_ignore_collect(collection_path): 11 | if "rhino" in str(collection_path): 12 | return True 13 | 14 | if "blender" in str(collection_path): 15 | return True 16 | 17 | if "ghpython" in str(collection_path): 18 | return True 19 | 20 | 21 | @pytest.fixture(autouse=True) 22 | def add_compas(doctest_namespace): 23 | doctest_namespace["compas"] = compas 24 | 25 | 26 | @pytest.fixture(autouse=True) 27 | def add_compas_xr(doctest_namespace): 28 | doctest_namespace["compas_xr"] = compas_xr 29 | 30 | 31 | @pytest.fixture(autouse=True) 32 | def add_math(doctest_namespace): 33 | doctest_namespace["math"] = math 34 | 35 | 36 | @pytest.fixture(autouse=True) 37 | def add_np(doctest_namespace): 38 | doctest_namespace["np"] = numpy 39 | -------------------------------------------------------------------------------- /docs/_images/Application_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_1.png -------------------------------------------------------------------------------- /docs/_images/Application_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_2.png -------------------------------------------------------------------------------- /docs/_images/Application_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_3.png -------------------------------------------------------------------------------- /docs/_images/Application_Overview_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_Overview_1.png -------------------------------------------------------------------------------- /docs/_images/Application_Overview_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_Overview_2.png -------------------------------------------------------------------------------- /docs/_images/Application_Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Application_Settings.png -------------------------------------------------------------------------------- /docs/_images/Assembly_BuildingPlan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Assembly_BuildingPlan.png -------------------------------------------------------------------------------- /docs/_images/Building_Plan_Multi_User_Interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Building_Plan_Multi_User_Interaction.png -------------------------------------------------------------------------------- /docs/_images/Building_Plan_Priority_Illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Building_Plan_Priority_Illustration.png -------------------------------------------------------------------------------- /docs/_images/Building_Plan_Step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Building_Plan_Step.png -------------------------------------------------------------------------------- /docs/_images/Building_Plan_Structure_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Building_Plan_Structure_Example.png -------------------------------------------------------------------------------- /docs/_images/Built_Human_Robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Built_Human_Robot.png -------------------------------------------------------------------------------- /docs/_images/Data_Structure_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Data_Structure_Example.png -------------------------------------------------------------------------------- /docs/_images/Docker_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_1.png -------------------------------------------------------------------------------- /docs/_images/Docker_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_2.png -------------------------------------------------------------------------------- /docs/_images/Docker_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_3.png -------------------------------------------------------------------------------- /docs/_images/Docker_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_4.png -------------------------------------------------------------------------------- /docs/_images/Docker_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_5.png -------------------------------------------------------------------------------- /docs/_images/Docker_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_6.png -------------------------------------------------------------------------------- /docs/_images/Docker_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_7.png -------------------------------------------------------------------------------- /docs/_images/Docker_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Docker_8.png -------------------------------------------------------------------------------- /docs/_images/Firebase_Data_Management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Firebase_Data_Management.png -------------------------------------------------------------------------------- /docs/_images/Frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Frame.png -------------------------------------------------------------------------------- /docs/_images/Frames_Wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Frames_Wall.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_2.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_3.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_4.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_5.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_6.png -------------------------------------------------------------------------------- /docs/_images/Grasshopper_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Grasshopper_7.png -------------------------------------------------------------------------------- /docs/_images/Services_Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/Services_Overview.png -------------------------------------------------------------------------------- /docs/_images/android_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_1.png -------------------------------------------------------------------------------- /docs/_images/android_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_2.png -------------------------------------------------------------------------------- /docs/_images/android_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_3.png -------------------------------------------------------------------------------- /docs/_images/android_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_4.png -------------------------------------------------------------------------------- /docs/_images/android_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_5.png -------------------------------------------------------------------------------- /docs/_images/android_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_6.png -------------------------------------------------------------------------------- /docs/_images/android_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_7.png -------------------------------------------------------------------------------- /docs/_images/android_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_8.png -------------------------------------------------------------------------------- /docs/_images/android_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/android_9.png -------------------------------------------------------------------------------- /docs/_images/app_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/app_01.png -------------------------------------------------------------------------------- /docs/_images/compas_xr_lead_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/compas_xr_lead_image.png -------------------------------------------------------------------------------- /docs/_images/example_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/example_1.jpg -------------------------------------------------------------------------------- /docs/_images/example_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/example_2.jpg -------------------------------------------------------------------------------- /docs/_images/example_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/example_3.jpg -------------------------------------------------------------------------------- /docs/_images/firebase_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_1.png -------------------------------------------------------------------------------- /docs/_images/firebase_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_10.png -------------------------------------------------------------------------------- /docs/_images/firebase_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_11.png -------------------------------------------------------------------------------- /docs/_images/firebase_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_12.png -------------------------------------------------------------------------------- /docs/_images/firebase_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_13.png -------------------------------------------------------------------------------- /docs/_images/firebase_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_14.png -------------------------------------------------------------------------------- /docs/_images/firebase_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_15.png -------------------------------------------------------------------------------- /docs/_images/firebase_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_16.png -------------------------------------------------------------------------------- /docs/_images/firebase_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_17.png -------------------------------------------------------------------------------- /docs/_images/firebase_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_18.png -------------------------------------------------------------------------------- /docs/_images/firebase_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_2.png -------------------------------------------------------------------------------- /docs/_images/firebase_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_3.png -------------------------------------------------------------------------------- /docs/_images/firebase_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_4.png -------------------------------------------------------------------------------- /docs/_images/firebase_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_5.png -------------------------------------------------------------------------------- /docs/_images/firebase_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_6.png -------------------------------------------------------------------------------- /docs/_images/firebase_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_7.png -------------------------------------------------------------------------------- /docs/_images/firebase_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_8.png -------------------------------------------------------------------------------- /docs/_images/firebase_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_9.png -------------------------------------------------------------------------------- /docs/_images/firebase_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/firebase_packages.png -------------------------------------------------------------------------------- /docs/_images/grasshopper_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/grasshopper_1.png -------------------------------------------------------------------------------- /docs/_images/ios_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_1.png -------------------------------------------------------------------------------- /docs/_images/ios_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_10.png -------------------------------------------------------------------------------- /docs/_images/ios_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_11.png -------------------------------------------------------------------------------- /docs/_images/ios_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_12.png -------------------------------------------------------------------------------- /docs/_images/ios_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_13.png -------------------------------------------------------------------------------- /docs/_images/ios_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_14.png -------------------------------------------------------------------------------- /docs/_images/ios_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_15.png -------------------------------------------------------------------------------- /docs/_images/ios_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_2.png -------------------------------------------------------------------------------- /docs/_images/ios_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_3.png -------------------------------------------------------------------------------- /docs/_images/ios_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_4.png -------------------------------------------------------------------------------- /docs/_images/ios_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_5.png -------------------------------------------------------------------------------- /docs/_images/ios_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_6.png -------------------------------------------------------------------------------- /docs/_images/ios_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_7.png -------------------------------------------------------------------------------- /docs/_images/ios_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_8.png -------------------------------------------------------------------------------- /docs/_images/ios_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ios_9.png -------------------------------------------------------------------------------- /docs/_images/software_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/software_architecture.png -------------------------------------------------------------------------------- /docs/_images/ui_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ui_01.png -------------------------------------------------------------------------------- /docs/_images/ui_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ui_02.png -------------------------------------------------------------------------------- /docs/_images/ui_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/ui_03.png -------------------------------------------------------------------------------- /docs/_images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_images/workflow.png -------------------------------------------------------------------------------- /docs/_static/qr_codes/compas_xr_qrs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/docs/_static/qr_codes/compas_xr_qrs.pdf -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | API Reference 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | api/compas_xr 9 | api/compas_xr_unity 10 | -------------------------------------------------------------------------------- /docs/api/compas_xr.ghpython.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: compas_xr.ghpython 3 | -------------------------------------------------------------------------------- /docs/api/compas_xr.mqtt.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: compas_xr.mqtt 3 | -------------------------------------------------------------------------------- /docs/api/compas_xr.project.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: compas_xr.project 3 | -------------------------------------------------------------------------------- /docs/api/compas_xr.realtime_database.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: compas_xr.realtime_database 3 | -------------------------------------------------------------------------------- /docs/api/compas_xr.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^ 2 | COMPAS XR 3 | ^^^^^^^^^ 4 | 5 | The ``compas_xr`` Python library allows -amongs other things- integration into CAD functionality. 6 | 7 | .. automodule:: compas_xr 8 | -------------------------------------------------------------------------------- /docs/api/compas_xr.storage.rst: -------------------------------------------------------------------------------- 1 | 2 | .. automodule:: compas_xr.storage 3 | -------------------------------------------------------------------------------- /docs/api/compas_xr_unity.rst: -------------------------------------------------------------------------------- 1 | 2 | ^^^^^^^^^^^^^^^ 3 | COMPAS XR Unity 4 | ^^^^^^^^^^^^^^^ 5 | 6 | The COMPAS XR Unity package contains an application & Unity file that work together with the ``compas_xr`` Python library. 7 | 8 | """""""""""""""" 9 | Initialize.unity 10 | """""""""""""""" 11 | 12 | - ``FirebaseInitializer.cs`` (initializes Firebase with configured settings and manages scene transitions upon successful initialization, utilizing MqttFirebaseConfigManager for disconnecting MQTT and HelpersExtensions.ChangeScene for scene navigation.) 13 | - ``FirebaseConfigSettings.cs`` (manages user input Firebase configuration settings, saving and loading them from Player Preferences, and updates a singleton instance FirebaseManager with these values.) 14 | - ``MqttFirebaseConfigManager.cs`` (manages MQTT connections, handles message events, and allows subscription to custom topics for sending Firebase configuration information.) 15 | - ``LogManager.cs`` (manages logging by creating a log directory, deleting old log files, and writing log messages with timestamps, scene names, log types, and stack traces to a specified log file.) 16 | 17 | """"""""""" 18 | Login.unity 19 | """"""""""" 20 | 21 | - ``UserManager.cs`` (manages user records and device configurations in Firebase, allowing creation of new users and updating existing ones based on unique identifiers.) 22 | 23 | 24 | """""""""""""" 25 | MainGame.unity 26 | """""""""""""" 27 | 28 | - ``AppModeControler.cs`` (defines a ModeControler class within the CompasXR.AppSettings namespace that manages two enums, VisulizationMode and TouchMode, to control visualization modes and touch interaction modes in an application.) 29 | - ``ApplicationSettings.cs`` (defines a ApplicationSettings class within the CompasXR.AppSettings namespace, which includes properties for managing the project name, storage folder path, and a boolean for z-to-y axis remapping configuration in an application.) 30 | - ``CheckFirebase.cs`` (defines a CheckFirebase class within the CompasXR.Database.FirebaseManagement namespace, which checks and initializes Firebase on the Start method, and triggers an event to confirm successful initialization or logs an error if it fails.) 31 | - ``CoreData.cs`` (defines several classes within the CompasXR.Core.Data namespace, handling data structures, data conversion, and deserialization. The classes represent parts of a building assembly, including nodes, parts, frames, attributes, building plans, steps, data, and user information.) 32 | - ``DatabaseManager.cs`` (manages a Unity application's integration with Firebase Realtime Database and Storage, handling data synchronization, event-driven updates, and deserialization of complex data structures for a construction planning and tracking system.) 33 | - ``Eventmanager.cs`` (orchestrates the setup, initialization, and event-driven interactions between various components and Firebase services within the CompasXR Unity application.) 34 | - ``Extentions.cs`` (extends Unity engine functionality with methods for scene management, object finding, value remapping, UI interaction checks, data type printing, camera-facing behavior, and object position storage in the CompasXR.Core.Extentions namespace.) 35 | - ``FirebaseManager.cs`` (implements a sealed singleton class using the Singleton Pattern to manage Firebase configuration settings such as appId, apiKey, databaseUrl, storageBucket, and projectId, with initialization based on the current operating system detected by OperatingSystemManager.GetCurrentOS().) 36 | - ``InstantiateObjects.cs`` (manages the AR space instantiation and control of objects based on building plan data, handling object visualization, user indicators, and interaction with materials and textures.) 37 | - ``MQTTDataCompasXR.cs`` (define classes and data structures for managing MQTT communication, service states, and custom message formats within the Compas XR system, ensuring standardized messaging and service management across robotic applications.) 38 | - ``MqttTrajectoryManager.cs`` (implements an MQTT client in Unity for managing robot trajectory requests and approvals, featuring message handling, connection management, and UI interactions.) 39 | - ``ObjectTransformations.cs`` ( provides static methods for converting object positions and rotations between Unity and Rhino coordinate systems, utilizing various rotation and transformation algorithms.) 40 | - ``OperatingSystemManager.cs`` (identifies and logs the current operating system (Android, iOS, or Unknown) based on the Unity platform, providing a static method to retrieve this information.) 41 | - ``QRLocalization.cs`` (manages the real-time localization of objects in a scene based on QR code data, updating their positions and rotations dynamically.) 42 | - ``RosConnectionManager.cs`` ( handles the connection to a ROSBridge server, manages connection status, and updates UI elements based on communication toggle state in the CompasXR Application.) 43 | - ``ScrollSearchManager.cs`` (manages scrollable UI functionality for searching and interacting with elements, including dynamic cell creation, scrolling control, and object coloring based on user selection in the CompasXR Application.) 44 | - ``TrajectoryVisualizer.cs`` (namespace manages robot visualization and interaction in Unity, facilitating tasks such as setting up robots in the scene, instantiating trajectories, configuring robot configurations from dictionaries, and attaching elements to end effectors based on received data.) 45 | - ``UIFunctionalities.cs`` (manages various UI elements and functionalities, including primary UI controls, visualizer menus, additional menu functionalities, and communication settings such as MQTT and ROS connections.) 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/citing.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Citing 3 | ******************************************************************************** 4 | 5 | CITING COMPAS XR 6 | 7 | If you are using COMPAS XR, please cite us. 8 | 9 | This citation is for a conference poster; the associated paper is in press and will soon replace it. 10 | 11 | .. code-block:: none 12 | 13 | @misc{compas-xr, 14 | title={{COMPAS~XR}: extended reality workflows for the COMPAS Framework}, 15 | author={ 16 | Kenny, J. and 17 | Mitterberger, D. and 18 | Casas, G. and 19 | Alexi, E. and 20 | Gramazio, F. and 21 | Kohler, M. 22 | }, 23 | howpublished={https://github.com/compas-dev/compas\_xr/}, 24 | note={ETH Z\"{u}rich, Princeton University}, 25 | year={2024}, 26 | doi={10.5281/zenodo.12514526}, 27 | url={https://doi.org/10.5281/zenodo.12514526}, 28 | } 29 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | 4 | from sphinx.writers import html, html5 5 | import sphinx_compas2_theme 6 | 7 | # -- General configuration ------------------------------------------------ 8 | 9 | project = "COMPAS XR" 10 | copyright = "ETH Zurich, Princeton University" 11 | author = "Joseph Kenny" 12 | organization = "compas-dev" 13 | package = "compas_xr" 14 | 15 | master_doc = "index" 16 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"} 17 | templates_path = sphinx_compas2_theme.get_autosummary_templates_path() 18 | exclude_patterns = sphinx_compas2_theme.default_exclude_patterns 19 | add_module_names = True 20 | language = "en" 21 | 22 | latest_version = sphinx_compas2_theme.get_latest_version() 23 | 24 | if latest_version == "Unreleased": 25 | release = "Unreleased" 26 | version = "latest" 27 | else: 28 | release = latest_version 29 | version = ".".join(release.split(".")[0:2]) # type: ignore 30 | 31 | # -- Extension configuration ------------------------------------------------ 32 | 33 | extensions = sphinx_compas2_theme.default_extensions 34 | 35 | # numpydoc options 36 | 37 | numpydoc_show_class_members = False 38 | numpydoc_class_members_toctree = False 39 | numpydoc_attributes_as_param_list = True 40 | numpydoc_show_inherited_class_members = False 41 | 42 | # bibtex options 43 | 44 | # autodoc options 45 | 46 | autodoc_type_aliases = {} 47 | autodoc_typehints_description_target = "documented" 48 | autodoc_mock_imports = sphinx_compas2_theme.default_mock_imports 49 | autodoc_default_options = { 50 | "undoc-members": True, 51 | "show-inheritance": True, 52 | } 53 | autodoc_member_order = "groupwise" 54 | autodoc_typehints = "description" 55 | autodoc_class_signature = "separated" 56 | 57 | autoclass_content = "class" 58 | 59 | 60 | def setup(app): 61 | app.connect("autodoc-skip-member", sphinx_compas2_theme.skip) 62 | 63 | 64 | # autosummary options 65 | 66 | autosummary_generate = True 67 | autosummary_mock_imports = sphinx_compas2_theme.default_mock_imports 68 | 69 | # graph options 70 | 71 | # plot options 72 | 73 | plot_include_source = False 74 | plot_html_show_source_link = False 75 | plot_html_show_formats = False 76 | plot_formats = ["png"] 77 | 78 | # intersphinx options 79 | 80 | intersphinx_mapping = { 81 | "python": ("https://docs.python.org/", None), 82 | "compas": ("https://compas.dev/compas/latest/", None), 83 | "compas_timber": ("https://gramaziokohler.github.io/compas_timber/latest/", None), 84 | } 85 | 86 | # linkcode 87 | 88 | linkcode_resolve = sphinx_compas2_theme.get_linkcode_resolve(organization, package) 89 | 90 | # extlinks 91 | 92 | extlinks = { 93 | "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"), 94 | "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s"), 95 | } 96 | 97 | # from pytorch 98 | 99 | sphinx_compas2_theme.replace(html.HTMLTranslator) 100 | sphinx_compas2_theme.replace(html5.HTML5Translator) 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | html_theme = "sidebaronly" 105 | html_title = project 106 | html_sidebars = {"index": []} 107 | 108 | favicons = [ 109 | { 110 | "rel": "icon", 111 | "href": "compas.ico", 112 | } 113 | ] 114 | 115 | html_theme_options = { 116 | "external_links": [ 117 | {"name": "COMPAS Framework", "url": "https://compas.dev"}, 118 | ], 119 | "icon_links": [ 120 | { 121 | "name": "GitHub", 122 | "url": f"https://github.com/{organization}/{package}", 123 | "icon": "fa-brands fa-github", 124 | "type": "fontawesome", 125 | }, 126 | { 127 | "name": "Discourse", 128 | "url": "http://forum.compas-framework.org/", 129 | "icon": "fa-brands fa-discourse", 130 | "type": "fontawesome", 131 | }, 132 | { 133 | "name": "PyPI", 134 | "url": f"https://pypi.org/project/{package}/", 135 | "icon": "fa-brands fa-python", 136 | "type": "fontawesome", 137 | }, 138 | ], 139 | "switcher": { 140 | "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", 141 | "version_match": version, 142 | }, 143 | "logo": { 144 | "image_light": "_static/compas_icon_white.png", 145 | "image_dark": "_static/compas_icon_white.png", 146 | "text": "COMPAS XR", 147 | }, 148 | "navigation_depth": 3, 149 | } 150 | 151 | html_context = { 152 | "github_url": "https://github.com", 153 | "github_user": organization, 154 | "github_repo": package, 155 | "github_version": "main", 156 | "doc_path": "docs", 157 | } 158 | 159 | html_static_path = sphinx_compas2_theme.get_html_static_path() + ["_static"] 160 | html_css_files = [] 161 | html_extra_path = [] 162 | html_last_updated_fmt = "" 163 | html_copy_source = False 164 | html_show_sourcelink = True 165 | html_permalinks = False 166 | html_permalinks_icon = "" 167 | html_compact_lists = True 168 | -------------------------------------------------------------------------------- /docs/examples/scripts/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | moveit-demo: 4 | image: gramaziokohler/ros-noetic-moveit 5 | container_name: moveit-demo 6 | environment: 7 | - ROS_HOSTNAME=moveit-demo 8 | - ROS_MASTER_URI=http://ros-core:11311 9 | # GUI Option 1: To forward the GUI to an external X11 server (eg. XMing), uncomment the following line 10 | # - DISPLAY=host.docker.internal:0.0 11 | # GUI Option 2: To use the web-based GUI, uncomment the following line 12 | # - DISPLAY=gui:0.0 13 | depends_on: 14 | - ros-core 15 | # To use the web-based GUI, uncomment the following line 16 | # - gui 17 | command: 18 | - roslaunch 19 | - --wait 20 | # To change the robot, select the corresponding package name here, eg. `ur10e_moveit_config` 21 | - ur10e_moveit_config 22 | - demo.launch 23 | # To launch the RVIZ GUI, change the following to true and activate one of the two GUI options above 24 | - use_rviz:=false 25 | 26 | ros-core: 27 | image: gramaziokohler/ros-noetic-moveit 28 | container_name: ros-core 29 | ports: 30 | - "11311:11311" 31 | command: 32 | - roscore 33 | 34 | ros-bridge: 35 | image: gramaziokohler/ros-noetic-moveit 36 | container_name: ros-bridge 37 | environment: 38 | - "ROS_HOSTNAME=ros-bridge" 39 | - "ROS_MASTER_URI=http://ros-core:11311" 40 | ports: 41 | - "9090:9090" 42 | depends_on: 43 | - ros-core 44 | command: 45 | - roslaunch 46 | - --wait 47 | - rosbridge_server 48 | - rosbridge_websocket.launch 49 | 50 | ros-fileserver: 51 | image: gramaziokohler/ros-noetic-moveit 52 | container_name: ros-fileserver 53 | environment: 54 | - ROS_HOSTNAME=ros-fileserver 55 | - ROS_MASTER_URI=http://ros-core:11311 56 | depends_on: 57 | - ros-core 58 | command: 59 | - roslaunch 60 | - --wait 61 | - file_server 62 | - file_server.launch 63 | 64 | # To use the web-based GUI, uncomment the following lines 65 | # gui: 66 | # image: gramaziokohler/novnc:latest 67 | # ports: 68 | # - "8080:8080" 69 | -------------------------------------------------------------------------------- /docs/examples/scripts/firebase_config/compas_xr_example.json: -------------------------------------------------------------------------------- 1 | {"apiKey": "AIzaSyDm-KDwiar0NyfHfjTHFtF-tr2tgzKS3bM", "authDomain": "test-project-94f41.firebaseapp.com", "databaseURL": "https://test-project-94f41-default-rtdb.europe-west1.firebasedatabase.app", "storageBucket": "test-project-94f41.appspot.com"} -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | COMPAS XR 3 | ******************************************************************************** 4 | 5 | .. rst-class:: lead 6 | 7 | COMPAS XR streamlines extended reality workflows to ease the implementation of human-machine collaborative applications in architectural research and educational environments. 8 | 9 | .. figure:: /_images/compas_xr_lead_image.png 10 | :figclass: figure 11 | :class: figure-img img-fluid 12 | 13 | Introducing **COMPAS XR**, an open-source framework designed to streamline extended 14 | reality (XR) workflows. COMPAS XR facilitates the integration of human-machine 15 | collaboration in architectural research and educational environments, enhancing 16 | productivity and innovation in these fields. Additionally, it provides implementation 17 | of communication protocols for both CAD softwares and Unity Based phone applications 18 | required for the visualization, reading, writing, and storage of COMPAS data 19 | across multiple cloud-based data storage formats. 20 | 21 | .. figure:: /_images/software_architecture.png 22 | :figclass: figure 23 | :class: figure-img img-fluid 24 | 25 | **Key Features:** 26 | 27 | * **Multi-User/Device Connectivity:** COMPAS XR features a distributed system that enables numerous users to connect simultaneously and actively share information across all devices. This capability facilitates the implementation of real-time building processes in CAD software, ensuring seamless collaboration and data exchange. 28 | * **Human-Machine Building Assistance:** COMPAS XR enhances assembly tasks through extended reality visualization and interaction features, allowing users to instruct, modify, and record task completions. Additionally, it supports the remote visualization, review, and approval of robotic actions in assembly processes from multiple user devices. 29 | * **Versatility:** COMPAS XR offers robust capabilities for visualizing, planning, and tracking of COMPAS assemblies across a variety of geometries. It supports the extended reality instruction of diverse construction assembly types, catering to a wide range of building procedures. 30 | * **Project-Specific and Low-Code:** COMPAS XR empowers users to tailor assembly processes to meet specific project needs, offering flexibility and ease of use with low-code customization options. 31 | 32 | Join us in unlocking the potential of COMPAS XR – where extended reality transforms collaborative 33 | architectural design and research, seamlessly integrating human-machine interaction with 34 | cutting-edge cloud-based solutions and assembly processes. 35 | 36 | 37 | Example Projects 38 | ================= 39 | 40 | .. figure:: /_images/example_1.jpg 41 | :figclass: figure 42 | :class: figure-img img-fluid 43 | **AR Timber Assembly** | 44 | Enabling Human-Robot Cooperation for Reconfigurable Timber Assembly 45 | 46 | https://robarch2024.org/Collaborative-Augmented-Assembly 47 | https://huma-labforadvancedtechnologyinarch.github.io/robarch24/ 48 | 49 | In this workshop, participants explored a cooperative human-robot design-to-assembly 50 | workflow, creating a complex timber structure that neither could achieve alone. Using 51 | a digital design tool guided by human input, they modeled the timber assembly, considering 52 | fabrication constraints, structural stability, and task distribution between humans and 53 | robots. During assembly, two mobile robots positioned timber members and provided temporary 54 | support at critical points. Human participants manually closed the reciprocal frames and 55 | added mechanical connectors. Both shared a digital-physical workspace, with humans receiving 56 | instructions via a mobile AR interface, utilizing COMPAS XR features. 57 | 58 | *Princeton University: Mitterberger, Alexi, Kenny in collaboration with TU Munich: Dörfler, Atanasova, Saral* 59 | 60 | 61 | .. figure:: /_images/example_2.jpg 62 | :figclass: figure 63 | :class: figure-img img-fluid 64 | **Cooperative Augmented Assembly** | 65 | Augmented Reality for On-Site Cooperative Robotic Fabrication 66 | 67 | https://www.arc.ed.tum.de/df/teaching/archive/cdf-ss-2023/ 68 | https://drive.google.com/file/d/1sUCggAQAHKByWS5JAix2g8qEch174QuH/view 69 | https://www.masdfab.arch.ethz.ch/program/studentwork/2022-23-t3-work 70 | 71 | 72 | The interdisciplinary seminar "Computational Design and Digital Fabrication" aims to bridge fundamental 73 | principles of geometric computation and structural design and use these insights to develop new algorithms 74 | and tools for robotic fabrication in architecture. At the interface of the disciplines of architecture, 75 | structural design and engineering, and digital fabrication, students are taught innovative computational 76 | design solutions for advanced digital construction techniques at various scales. 77 | 78 | *TU Munich: Dörfler, Atanasova in collaboration with Princeton University: Mitterberger, Alexi, Kenny* 79 | 80 | 81 | .. figure:: /_images/example_3.jpg 82 | :figclass: figure 83 | :class: figure-img img-fluid 84 | **ARC 595** | Embodied Computation 85 | 86 | https://huma-labforadvancedtechnologyinarch.github.io/ARC596_Embodied-Computatio 87 | 88 | The seminar Embodied Computation at Princeton University delves into the intersection of architecture and 89 | computation, focusing on augmented design and fabrication processes. The course is based on lectures and 90 | practical programming exercises in which students learn concepts and methods of computer-aided design, 91 | augmented reality (AR) and digital fabrication. The students learn new programming skills to develop augmented 92 | reality applications for architects to support adaptive and interactive design and fabrication methodologies. 93 | 94 | *Princeton University: Mitterberger* 95 | 96 | 97 | 98 | Table of Contents 99 | ================= 100 | 101 | .. toctree:: 102 | :maxdepth: 3 103 | :titlesonly: 104 | 105 | Introduction 106 | installation 107 | userguide 108 | api 109 | license 110 | citing 111 | 112 | Indices and tables 113 | ================== 114 | 115 | * :ref:`genindex` 116 | * :ref:`modindex` 117 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Installation 3 | ******************************************************************************** 4 | 5 | This chapter provides a step-by-step guide for installing compas_xr on your system. The library can be 6 | installed using either pip or conda, which are widely-used package managers for Python. The following 7 | instructions will guide you through each method. Alternatively, you can clone the ``compas_xr`` library 8 | directly from our `repository `_. 9 | 10 | Installation using conda 11 | ======================== 12 | 13 | Conda is an open-source package management system and environment management system that runs on Windows, 14 | macOS, and Linux. It is very popular in the realm of scientific computing. 15 | 16 | Step 1: Create a conda environment (Optional) 17 | ============================================= 18 | 19 | It's often beneficial to create a new environment for your project. This can be done using the following command: 20 | :: 21 | conda create --name my_environment_name 22 | 23 | Replace my_environment_name with your desired environment name. 24 | 25 | Activate the new environment by running: 26 | :: 27 | conda activate my_environment_name 28 | 29 | Step 2: Update pip 30 | ================== 31 | 32 | It is good practice to ensure that you are using the latest version of pip. To update pip, run the following command: 33 | :: 34 | python -m pip install --upgrade pip 35 | 36 | Step 3: Install compas_xr 37 | ========================= 38 | 39 | To install compas_xr using pip, execute the following command: 40 | :: 41 | pip install compas_xr 42 | 43 | Verify installation 44 | =================== 45 | 46 | After installation, you can verify that the compas_xr has been successfully installed by running: 47 | :: 48 | python -c "import compas_xr; print(compas_xr.__version__)" 49 | 50 | 51 | If everything worked out correctly, the version of the installed package will be printed on the screen, and you can 52 | start using the toolkit into your projects. 53 | 54 | Installing COMPAS packages for Rhino environments 55 | ================================================= 56 | 57 | After verification of installation you can run the command below to install all COMPAS packages within your Rhino Environment: 58 | :: 59 | python -m compas_rhino.install 60 | 61 | COMPAS XR Unity - Phone Based AR Application 62 | ============================================ 63 | 64 | This chapter provides a step-by-step guide for installing compas_xr_unity on your device. The use and installation of 65 | the application is supported by both Android and ios devices. If you would like to install the application without 66 | functionality or code modifications Android .apk file, and ios xcode build can be found `here `_. 67 | 68 | However, if you would like to modify any application functionalities or anything the entire code base for the application 69 | can be found and cloned from our `repository `_. 70 | 71 | Additionally, both Android and device installation procedures can be found in the Release Procedures chapter of the documentation. 72 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | License 3 | ******************************************************************************** 4 | :: 5 | 6 | MIT License 7 | 8 | Copyright (c) 2023-2024 ETH Zurich, Princeton University 9 | 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 12 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 14 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: compas_xr_docs 2 | channels: 3 | - conda-forge 4 | - defaults 5 | 6 | dependencies: 7 | - python=3.11 8 | - pip 9 | - compas=2.* 10 | - compas_timber=0.7.* 11 | - compas_eve=1.* 12 | - compas_fab=1.* 13 | - pip: 14 | - pyrebase4>4.7.1 15 | # dev requirements 16 | - attrs >=17.4 17 | - black >=22.12.0 18 | - bump-my-version 19 | - compas_invocations2 20 | - invoke >=0.14 21 | - ruff 22 | - sphinx_compas2_theme 23 | - twine 24 | - wheel 25 | 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # ============================================================================ 6 | # project info 7 | # ============================================================================ 8 | 9 | [project] 10 | name = "compas_xr" 11 | description = "COMPAS XR streamlines extended reality workflows to ease the implementation of human-machine collaborative applications in architectural research and educational environments." 12 | keywords = ["compas", "xr", "aec", "robotic", "human-machine"] 13 | authors = [{ name = "Joseph Kenny", email = "kenny@arch.ethz.ch" }, { name = "Daniela Mitterberger", email = "mitterberger@princeton.edu" }] 14 | license = { file = "LICENSE" } 15 | readme = "README.md" 16 | requires-python = ">=3.9" 17 | dynamic = ['dependencies', 'optional-dependencies', 'version'] 18 | classifiers = [ 19 | "Development Status :: 4 - Beta", 20 | "Topic :: Scientific/Engineering", 21 | "Operating System :: Unix", 22 | "Operating System :: POSIX", 23 | "Operating System :: Microsoft :: Windows", 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | ] 30 | 31 | [project.urls] 32 | Homepage = "https://github.com/compas-dev/compas_xr" 33 | Documentation = "https://compas.dev/compas_xr/" 34 | Repository = "https://github.com/compas-dev/compas_xr.git" 35 | Changelog = "https://github.com/compas-dev/compas_xr/blob/main/CHANGELOG.md" 36 | Issues = "https://github.com/compas-dev/compas_xr/issues" 37 | Forum = "https://forum.compas-framework.org/" 38 | 39 | # ============================================================================ 40 | # setuptools config 41 | # ============================================================================ 42 | 43 | [tool.setuptools] 44 | package-dir = { "" = "src" } 45 | include-package-data = true 46 | zip-safe = false 47 | 48 | [tool.setuptools.dynamic] 49 | version = { attr = "compas_xr.__version__" } 50 | dependencies = { file = "requirements.txt" } 51 | optional-dependencies = { dev = { file = "requirements-dev.txt" } } 52 | 53 | [tool.setuptools.packages.find] 54 | where = ["src"] 55 | 56 | [tool.setuptools.package-data] 57 | 58 | # ============================================================================ 59 | # replace pytest.ini 60 | # ============================================================================ 61 | [tool.pytest.ini_options] 62 | minversion = "6.0" 63 | testpaths = ["tests", "src/compas_xr"] 64 | python_files = ["test_*.py", "tests.py"] 65 | addopts = ["-ra", "--strict-markers", "--doctest-glob=*.rst", "--tb=short"] 66 | doctest_optionflags = [ 67 | "NORMALIZE_WHITESPACE", 68 | "IGNORE_EXCEPTION_DETAIL", 69 | "ALLOW_UNICODE", 70 | "ALLOW_BYTES", 71 | "NUMBER", 72 | ] 73 | 74 | # ============================================================================ 75 | # replace bumpversion.cfg 76 | # ============================================================================ 77 | 78 | [tool.bumpversion] 79 | current_version = "1.0.0" 80 | message = "Bump version to {new_version}" 81 | commit = true 82 | tag = true 83 | 84 | [[tool.bumpversion.files]] 85 | filename = "src/compas_xr/__init__.py" 86 | search = "{current_version}" 87 | replace = "{new_version}" 88 | 89 | [[tool.bumpversion.files]] 90 | filename = "CHANGELOG.md" 91 | search = "Unreleased" 92 | replace = "[{new_version}] {now:%Y-%m-%d}" 93 | 94 | [[tool.bumpversion.files]] 95 | glob = "src/compas_xr/ghpython/components/**/code.py" 96 | search = "v{current_version}" 97 | replace = "v{new_version}" 98 | 99 | # ============================================================================ 100 | # replace setup.cfg 101 | # ============================================================================ 102 | 103 | [tool.black] 104 | line-length = 179 105 | 106 | [tool.ruff] 107 | line-length = 179 108 | indent-width = 4 109 | target-version = "py39" 110 | 111 | [tool.ruff.lint] 112 | select = ["E", "F", "I"] 113 | 114 | [tool.ruff.lint.per-file-ignores] 115 | "__init__.py" = ["I001"] 116 | "tests/*" = ["I001"] 117 | "tasks.py" = ["I001"] 118 | 119 | [tool.ruff.lint.isort] 120 | force-single-line = true 121 | known-first-party = [ 122 | "compas_xr", 123 | ] 124 | 125 | [tool.ruff.lint.pydocstyle] 126 | convention = "numpy" 127 | 128 | [tool.ruff.lint.pycodestyle] 129 | max-doc-length = 179 130 | 131 | [tool.ruff.format] 132 | docstring-code-format = true 133 | docstring-code-line-length = "dynamic" 134 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | attrs >=17.4 2 | black >=22.12.0 3 | bump-my-version 4 | compas_invocations2 5 | invoke >=0.14 6 | ruff 7 | sphinx_compas2_theme 8 | twine 9 | wheel 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy >= 1.15.4, < 2 2 | compas >=2.0.4,<3 3 | compas_timber ~=0.7.0 4 | compas_eve >=1,<2 5 | compas_fab >=1.0.2,<2 6 | pyrebase4 >=4.7.1 7 | -------------------------------------------------------------------------------- /src/compas_xr/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr 4 | ******************************************************************************** 5 | 6 | .. currentmodule:: compas_xr 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | compas_xr.ghpython 13 | compas_xr.mqtt 14 | compas_xr.project 15 | compas_xr.realtime_database 16 | compas_xr.storage 17 | 18 | """ 19 | 20 | from __future__ import print_function 21 | 22 | import os 23 | 24 | __author__ = ["Joseph Kenny"] 25 | __copyright__ = "ETH Zurich, Princeton University" 26 | __license__ = "MIT License" 27 | __email__ = "kenny@arch.ethz.ch" 28 | __version__ = "1.0.0" 29 | 30 | 31 | HERE = os.path.dirname(__file__) 32 | DATA = os.path.abspath(os.path.join(HERE, "data")) 33 | 34 | __all_plugins__ = ["compas_xr.rhino.install"] 35 | __all__ = ["HERE", "DATA"] 36 | -------------------------------------------------------------------------------- /src/compas_xr/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | pass 3 | -------------------------------------------------------------------------------- /src/compas_xr/dependencies/Firebase.Auth.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/Firebase.Auth.dll -------------------------------------------------------------------------------- /src/compas_xr/dependencies/Firebase.Storage.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/Firebase.Storage.dll -------------------------------------------------------------------------------- /src/compas_xr/dependencies/Firebase.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/Firebase.dll -------------------------------------------------------------------------------- /src/compas_xr/dependencies/LiteDB.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/LiteDB.dll -------------------------------------------------------------------------------- /src/compas_xr/dependencies/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /src/compas_xr/dependencies/System.Reactive.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/dependencies/System.Reactive.dll -------------------------------------------------------------------------------- /src/compas_xr/ghpython/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr.ghpython 4 | ******************************************************************************** 5 | 6 | This package contains classes to ease the usage of COMPAS XR from within Grasshopper. 7 | 8 | .. currentmodule:: compas_xr.ghpython 9 | 10 | Classes 11 | ------- 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | :nosignatures: 16 | 17 | AppSettings 18 | FirebaseConfig 19 | MqttMessageOptionsXR 20 | TrajectoryResultManager 21 | 22 | """ 23 | 24 | from .app_settings import AppSettings 25 | from .firebase_config import FirebaseConfig 26 | from .options import MqttMessageOptionsXR 27 | from .trajectory_manager import TrajectoryResultManager 28 | 29 | __all__ = ["AppSettings", "FirebaseConfig", "MqttMessageOptionsXR", "TrajectoryResultManager"] 30 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/app_settings.py: -------------------------------------------------------------------------------- 1 | class AppSettings(object): 2 | def __init__(self, project_name, storage_folder=None, z_to_y_remap=None): 3 | self.project_name = project_name 4 | self.storage_folder = storage_folder or "None" 5 | self.z_to_y_remap = z_to_y_remap or False 6 | 7 | def ToString(self): 8 | return str(self) 9 | 10 | def __str__(self): 11 | return "AppSettings, project_name={}, storage_folder={}, z_to_y_remap={}".format(self.project_name, self.storage_folder, self.z_to_y_remap) 12 | 13 | def __data__(self): 14 | return { 15 | "project_name": self.project_name, 16 | "storage_folder": self.storage_folder, 17 | "z_to_y_remap": self.z_to_y_remap, 18 | } 19 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_AppSettings/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Application Settings. 3 | 4 | COMPAS XR v1.0.0 5 | """ 6 | 7 | from ghpythonlib.componentbase import executingcomponent as component 8 | 9 | from compas_xr.ghpython.app_settings import AppSettings 10 | from compas_xr.project import ProjectManager 11 | 12 | 13 | class ApplicationSettingsComponent(component): 14 | def RunScript(self, config_filepath, project_name, storage_folder, z_to_y_remap, write): 15 | if not (config_filepath): 16 | self.Message = "Missing Config" 17 | 18 | elif not (project_name): 19 | self.Message = "Missing Settings Data" 20 | 21 | else: 22 | app_settings = AppSettings(project_name, storage_folder, z_to_y_remap) 23 | pm = ProjectManager(config_filepath) 24 | self.Message = None 25 | 26 | if write: 27 | pm.application_settings_writer(app_settings.project_name, app_settings.storage_folder, app_settings.z_to_y_remap) 28 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_AppSettings/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_AppSettings/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_AppSettings/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App Settings", 3 | "nickname": "App Settings", 4 | "category": "COMPAS XR", 5 | "subcategory": "2 Options", 6 | "description": "Application settings", 7 | "exposure": 4, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "config_filepath", 15 | "description": "Directory where you wish to store the Firebase settings.", 16 | "typeHintID": "str", 17 | "wireDisplay": 2 18 | }, 19 | { 20 | "name": "project_name", 21 | "description": "Name of the project where the app will look for information.", 22 | "typeHintID": "str" 23 | }, 24 | { 25 | "name": "storage_folder", 26 | "description": "Name of the storage folder.", 27 | "typeHintID": "str" 28 | }, 29 | { 30 | "name": "z_to_y_remap", 31 | "description": "Remap Z and Y axes.", 32 | "typeHintID": "bool" 33 | }, 34 | { 35 | "name": "write", 36 | "description": "True to write the settings to disc.", 37 | "typeHintID": "bool" 38 | } 39 | ], 40 | "outputParameters": [] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_Firebase_Config/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for Firebase. 3 | 4 | COMPAS XR v1.0.0 5 | """ 6 | 7 | import json 8 | import os 9 | 10 | from ghpythonlib.componentbase import executingcomponent as component 11 | 12 | from compas_xr.ghpython.firebase_config import FirebaseConfig 13 | 14 | 15 | class FirebaseConfigComponent(component): 16 | def RunScript(self, filepath, filename, api_key, auth_domain, database_url, storage_bucket): 17 | if not (api_key and auth_domain and database_url and storage_bucket): 18 | self.Message = "You are missing some config information" 19 | raise Exception("Missing Config Info") 20 | 21 | config_path = None 22 | firebase_config = FirebaseConfig(api_key, auth_domain, database_url, storage_bucket) 23 | config = firebase_config.__data__() 24 | 25 | if config and filepath: 26 | if os.path.exists(filepath): 27 | if not filename: 28 | filename = os.path.join(filepath, "firebase_config.json") 29 | if filename: 30 | filename = os.path.join(filepath, filename) 31 | 32 | config = dict(config) 33 | with open(filename, "w") as f: 34 | json.dump(config, f) 35 | config_path = filename 36 | self.Message = "Config Written" 37 | else: 38 | self.Message = "You filepath does not exist" 39 | raise Exception("Path does not exist {}".format(filepath)) 40 | else: 41 | self.Message = "You are missing your filepath" 42 | raise Exception("Missing Filepath") 43 | 44 | return config_path 45 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_Firebase_Config/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_Firebase_Config/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_Firebase_Config/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firebase Options", 3 | "nickname": "Firebase", 4 | "category": "COMPAS XR", 5 | "subcategory": "2 Options", 6 | "description": "Settings for Firebase database", 7 | "exposure": 4, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "filepath", 15 | "description": "Directory where you wish to store the Firebase settings.", 16 | "typeHintID": "str" 17 | }, 18 | { 19 | "name": "filename", 20 | "description": "Name of the Firebase settings file.", 21 | "typeHintID": "str" 22 | }, 23 | { 24 | "name": "api_key", 25 | "description": "API Key to access Firebase.", 26 | "typeHintID": "str" 27 | }, 28 | { 29 | "name": "auth_domain", 30 | "description": "Auth domain for Firebase.", 31 | "typeHintID": "str" 32 | }, 33 | { 34 | "name": "database_url", 35 | "description": "URL of the Firebase database.", 36 | "typeHintID": "str" 37 | }, 38 | { 39 | "name": "storage_bucket", 40 | "description": "Name of the storage bucket.", 41 | "typeHintID": "str" 42 | } 43 | 44 | ], 45 | "outputParameters": [ 46 | { 47 | "name": "config_path", 48 | "description": "Path where the configuration was stored." 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_GetTrajectoryRequest/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get Trajectory Request Component. 3 | 4 | COMPAS XR v1.0.0 5 | """ 6 | 7 | from compas_eve import Subscriber 8 | from compas_eve import Topic 9 | from compas_eve.ghpython import BackgroundWorker 10 | from compas_eve.mqtt import MqttTransport 11 | from ghpythonlib.componentbase import executingcomponent as component 12 | 13 | from compas_xr.mqtt import GetTrajectoryRequest 14 | 15 | 16 | def start_server(worker, options): 17 | topic_name_request = "compas_xr/get_trajectory_request/" + options.project_name 18 | 19 | worker.count = 0 20 | 21 | def get_trajectory_requested(request_message): 22 | worker.count += 1 23 | worker.display_message("Request #{} started".format(worker.count)) 24 | worker.update_result(request_message, 10) 25 | 26 | tx = MqttTransport(options.host) 27 | topic = Topic(topic_name_request, GetTrajectoryRequest) 28 | worker.subscriber = Subscriber(topic, callback=get_trajectory_requested, transport=tx) 29 | worker.subscriber.subscribe() 30 | worker.display_message("Subscribed") 31 | 32 | 33 | def stop_server(worker): 34 | if hasattr(worker, "subscriber"): 35 | worker.subscriber.unsubscribe() 36 | worker.display_message("Stopped") 37 | 38 | 39 | class GetTrajectoryRequestComponent(component): 40 | def RunScript(self, options, reset, on): 41 | if not on: 42 | BackgroundWorker.stop_instance_by_component(ghenv) # noqa: F821 43 | return None 44 | 45 | self.worker = BackgroundWorker.instance_by_component( 46 | ghenv, # noqa: F821 47 | start_server, 48 | dispose_function=stop_server, 49 | force_new=reset, 50 | auto_set_done=False, 51 | args=(options,), 52 | ) 53 | 54 | if not self.worker.is_working() and not self.worker.is_done() and reset: 55 | self.worker.start_work() 56 | 57 | if hasattr(self.worker, "result"): 58 | element_id = self.worker.result.element_id 59 | robot_name = self.worker.result.robot_name 60 | return element_id, robot_name 61 | else: 62 | return None, None 63 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_GetTrajectoryRequest/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_GetTrajectoryRequest/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_GetTrajectoryRequest/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Planning Request", 3 | "nickname": "Planning Request", 4 | "category": "COMPAS XR", 5 | "subcategory": "1 Plan", 6 | "description": "Start a planning request", 7 | "exposure": 2, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "options", 15 | "description": "XR options.", 16 | "wireDisplay": 2 17 | }, 18 | { 19 | "name": "reset", 20 | "description": "Resets the component." 21 | }, 22 | { 23 | "name": "on", 24 | "description": "Turn ON or OFF the subscriber for planning requests.", 25 | "typeHintID": "bool" 26 | } 27 | ], 28 | "outputParameters": [ 29 | { 30 | "name": "requested_element_id", 31 | "description": "The request element ID." 32 | }, 33 | { 34 | "name": "requested_robot", 35 | "description": "The name of the robot." 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_MqttTrajectoryResult/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sync Results. 3 | 4 | The Sync Result component is used to consolidate all user-defined inputs for the resulting trajectory and coordinate them into a single, unified trajectory. 5 | 6 | COMPAS XR v1.0.0 7 | """ 8 | 9 | from ghpythonlib.componentbase import executingcomponent as component 10 | 11 | from compas_xr.ghpython import TrajectoryResultManager 12 | 13 | 14 | class SyncResultComponent(component): 15 | def RunScript(self, element_id, trajectory, robot_base_frame, pick_and_place, pick_index, ee_link_name, options): 16 | if element_id: 17 | result = TrajectoryResultManager() 18 | result.requested_element_id = element_id 19 | result.robot_base_frame = robot_base_frame 20 | result.trajectory = result.format_trajectory(trajectory) 21 | if pick_and_place is not None: 22 | result.pick_and_place = pick_and_place 23 | if pick_and_place: 24 | result.pick_index = pick_index 25 | result.end_effector_link_name = ee_link_name 26 | else: 27 | result.pick_and_place = False 28 | else: 29 | result = None 30 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_MqttTrajectoryResult/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_MqttTrajectoryResult/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_MqttTrajectoryResult/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sync Result", 3 | "nickname": "Sync", 4 | "category": "COMPAS XR", 5 | "subcategory": "1 Plan", 6 | "description": "Consolidates all user-defined inputs for the resulting trajectory and coordinate them into a single, unified trajectory.", 7 | "exposure": 4, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "element_id", 15 | "description": "The number of the BuildingPlan Step that the trajectory is intended for.", 16 | "typeHintID": "str" 17 | }, 18 | { 19 | "name": "trajectory", 20 | "description": " trajectory that is intended to be published." 21 | }, 22 | { 23 | "name": "robot_base_fame", 24 | "description": "The location of the robot in relation to the design object." 25 | }, 26 | { 27 | "name": "pick_and_place", 28 | "description": "Notifies the application if the trajectory is intended to attach a building element in the process.", 29 | "typeHintID": "bool" 30 | }, 31 | { 32 | "name": "pick_index", 33 | "description": "The index in the trajectory in which the element should be attached to the robot.", 34 | "typeHintID": "int" 35 | }, 36 | { 37 | "name": "ee_link_name", 38 | "description": "The link name in which the element should be attached to.", 39 | "typeHintID": "str" 40 | }, 41 | { 42 | "name": "options", 43 | "description": "Information passed from the COMPAS XR Options Component." 44 | } 45 | ], 46 | "outputParameters": [ 47 | { 48 | "name": "result", 49 | "description": "Resulting trajectory." 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_PlanningServiceResponse/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Planning service response. 3 | 4 | COMPAS XR v1.0.0 5 | """ 6 | 7 | from compas_eve import Publisher 8 | from compas_eve import Topic 9 | from compas_eve.mqtt import MqttTransport 10 | from ghpythonlib.componentbase import executingcomponent as component 11 | 12 | from compas_xr.mqtt import GetTrajectoryResult 13 | 14 | 15 | class PlanningServiceResponseComponent(component): 16 | def RunScript(self, options, result, publish): 17 | if not result: 18 | self.Message = "Null Result, unable to publish" 19 | return 20 | 21 | if publish: 22 | topic_name_result = "compas_xr/get_trajectory_result/" + options.project_name 23 | topic = Topic(topic_name_result, GetTrajectoryResult) 24 | tx = MqttTransport(options.host) 25 | publisher = Publisher(topic, transport=tx) 26 | message = GetTrajectoryResult( 27 | element_id=result.requested_element_id, 28 | robot_name=options.robot_name, 29 | robot_base_frame=result.robot_base_frame, 30 | trajectory=result.trajectory, 31 | pick_and_place=result.pick_and_place, 32 | pick_index=result.pick_index, 33 | end_effector_link_name=result.end_effector_link_name, 34 | ) 35 | publisher.publish(message) 36 | self.Message = "Send trajectory for #{}".format(result.requested_element_id) 37 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_PlanningServiceResponse/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_PlanningServiceResponse/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_PlanningServiceResponse/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Publish plan", 3 | "nickname": "Publish trajectory", 4 | "category": "COMPAS XR", 5 | "subcategory": "1 Plan", 6 | "description": "Publishes a planned trajectory for execution.", 7 | "exposure": 2, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "options", 15 | "description": "XR options.", 16 | "wireDisplay": 2 17 | }, 18 | { 19 | "name": "result", 20 | "description": "Resulting trajectory." 21 | }, 22 | { 23 | "name": "publish", 24 | "description": "Send the trajectory.", 25 | "typeHintID": "bool" 26 | } 27 | ], 28 | "outputParameters": [] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_SendTrajectory/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Component to handle execution of trajectory. 3 | 4 | Gets trajectories to be executed by a robot. 5 | 6 | COMPAS XR v1.0.0 7 | """ 8 | 9 | from compas_eve import Subscriber 10 | from compas_eve import Topic 11 | from compas_eve.ghpython import BackgroundWorker 12 | from compas_eve.mqtt import MqttTransport 13 | from ghpythonlib.componentbase import executingcomponent as component 14 | 15 | from compas_xr.mqtt import SendTrajectory 16 | 17 | 18 | def start_server(worker, options): 19 | topic_name_request = "compas_xr/send_trajectory/" + options.project_name 20 | 21 | worker.count = 0 22 | 23 | def execute_trajectory_requested(request_message): 24 | worker.count += 1 25 | worker.display_message("Request #{} started".format(worker.count)) 26 | worker.update_result(request_message, 10) 27 | 28 | tx = MqttTransport(options.host) 29 | topic = Topic(topic_name_request, SendTrajectory) 30 | worker.subscriber = Subscriber(topic, callback=execute_trajectory_requested, transport=tx) 31 | worker.subscriber.subscribe() 32 | worker.display_message("Subscribed") 33 | 34 | 35 | def stop_server(worker): 36 | if hasattr(worker, "subscriber"): 37 | worker.subscriber.unsubscribe() 38 | worker.display_message("Stopped") 39 | 40 | 41 | class ExecuteTrajectoryServiceComponent(component): 42 | def RunScript(self, options, reset, on): 43 | if not on: 44 | BackgroundWorker.stop_instance_by_component(ghenv) # noqa: F821 45 | return None 46 | 47 | self.worker = BackgroundWorker.instance_by_component( 48 | ghenv, # noqa: F821 49 | start_server, 50 | dispose_function=stop_server, 51 | force_new=reset, 52 | auto_set_done=False, 53 | args=(options,), 54 | ) 55 | 56 | if not self.worker.is_working() and not self.worker.is_done() and reset: 57 | self.worker.start_work() 58 | 59 | if hasattr(self.worker, "result"): 60 | element_id = self.worker.result.element_id 61 | robot_name = self.worker.result.robot_name 62 | return element_id, robot_name 63 | else: 64 | return None, None 65 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_SendTrajectory/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_SendTrajectory/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_SendTrajectory/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Execution Service", 3 | "nickname": "Execute", 4 | "category": "COMPAS XR", 5 | "subcategory": "1 Plan", 6 | "description": "Service that receives trajectories to be executed by the robot.", 7 | "exposure": 4, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "options", 15 | "description": "XR options.", 16 | "wireDisplay": 2 17 | }, 18 | { 19 | "name": "reset", 20 | "description": "Resets the trajectory execution service." 21 | }, 22 | { 23 | "name": "on", 24 | "description": "Turn ON or OFF the trajectory execution service.", 25 | "typeHintID": "bool" 26 | } 27 | ], 28 | "outputParameters": [ 29 | { 30 | "name": "execute_element_id", 31 | "description": "The element ID associated with the trajectory." 32 | }, 33 | { 34 | "name": "execution_robot", 35 | "description": "The name of the robot that executed the trajectory." 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_XrOptions/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Component to define COMPAS XR options. 3 | 4 | COMPAS XR v1.0.0 5 | """ 6 | 7 | from ghpythonlib.componentbase import executingcomponent as component 8 | 9 | from compas_xr.ghpython import MqttMessageOptionsXR 10 | 11 | 12 | class XrOptionsComponent(component): 13 | def RunScript(self, host, project_name, robot_name): 14 | return MqttMessageOptionsXR(host, project_name, robot_name) 15 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_XrOptions/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/Cx_XrOptions/icon.png -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/Cx_XrOptions/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XR Options", 3 | "nickname": "Options", 4 | "category": "COMPAS XR", 5 | "subcategory": "2 Options", 6 | "description": "Creates an instance of COMPAS XR MQTT Options", 7 | "exposure": 2, 8 | 9 | "ghpython": { 10 | "isAdvancedMode": true, 11 | "iconDisplay": 2, 12 | "inputParameters": [ 13 | { 14 | "name": "host", 15 | "description": "MQTT host to use for communication.", 16 | "typeHintID": "str" 17 | }, 18 | { 19 | "name": "project_name", 20 | "description": "Name of the project.", 21 | "typeHintID": "str" 22 | }, 23 | { 24 | "name": "robot_name", 25 | "description": "Name of the robot.", 26 | "typeHintID": "str" 27 | } 28 | ], 29 | "outputParameters": [ 30 | { 31 | "name": "options", 32 | "description": "XR Options." 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/components/___init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/ghpython/components/___init__.py -------------------------------------------------------------------------------- /src/compas_xr/ghpython/firebase_config.py: -------------------------------------------------------------------------------- 1 | class FirebaseConfig(object): 2 | def __init__(self, api_key, auth_domain, database_url, storage_bucket): 3 | self.api_key = api_key 4 | self.auth_domain = auth_domain 5 | self.database_url = database_url 6 | self.storage_bucket = storage_bucket 7 | 8 | def ToString(self): 9 | return str(self) 10 | 11 | def __str__(self): 12 | return "FirebaseConfig, api_key={}, auth_domain={}, database_url={}, storage_bucket={}".format(self.api_key, self.auth_domain, self.database_url, self.storage_bucket) 13 | 14 | def __data__(self): 15 | return { 16 | "apiKey": self.api_key, 17 | "authDomain": self.auth_domain, 18 | "databaseURL": self.database_url, 19 | "storageBucket": self.storage_bucket, 20 | } 21 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/options.py: -------------------------------------------------------------------------------- 1 | class MqttMessageOptionsXR(object): 2 | def __init__(self, host, project_name, robot_name): 3 | self.host = host 4 | self.project_name = project_name 5 | self.robot_name = robot_name 6 | 7 | def ToString(self): 8 | return str(self) 9 | 10 | def __str__(self): 11 | return "Options, host={}, project_name={}, robot_name={}".format(self.host, self.project_name, self.robot_name) 12 | -------------------------------------------------------------------------------- /src/compas_xr/ghpython/trajectory_manager.py: -------------------------------------------------------------------------------- 1 | class TrajectoryResultManager(object): 2 | trajectory = None 3 | requested_element_id = None 4 | robot_base_fame = None 5 | pick_and_place = None 6 | pick_index = None 7 | end_effector_link_name = None 8 | 9 | def ToString(self): 10 | return str(self) 11 | 12 | def __str__(self): 13 | return "Planning result for element {} with {} points".format(self.requested_element_id, len(self.trajectory.points)) 14 | 15 | def format_trajectory(self, trajectory): 16 | configs_dicts = [] 17 | if trajectory: 18 | for point in trajectory.points: 19 | # Merge trajectory point with start config to make sure they are all full configurations 20 | # In the past, this was hardcoded only for the RFL, but it makes sense to do it for all 21 | point = trajectory.start_configuration.merged(point) 22 | joints_dict = point.joint_dict 23 | configs_dicts.append(joints_dict) 24 | return configs_dicts 25 | -------------------------------------------------------------------------------- /src/compas_xr/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr.mqtt 4 | ******************************************************************************** 5 | 6 | This package contains classes for interfacing with MQTT protocol. 7 | 8 | .. currentmodule:: compas_xr.mqtt 9 | 10 | MQTT Messages 11 | ------------- 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | :nosignatures: 16 | 17 | GetTrajectoryRequest 18 | GetTrajectoryResult 19 | ApproveTrajectory 20 | SendTrajectory 21 | 22 | """ 23 | 24 | from .messages import ApproveTrajectory, GetTrajectoryRequest, GetTrajectoryResult, SendTrajectory 25 | 26 | __all__ = ["GetTrajectoryRequest", "GetTrajectoryResult", "ApproveTrajectory", "SendTrajectory"] 27 | -------------------------------------------------------------------------------- /src/compas_xr/mqtt/messages.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | import uuid 4 | from datetime import datetime 5 | 6 | from compas.geometry import Frame 7 | from compas_eve import Message 8 | 9 | 10 | class SequenceCounter(object): 11 | """An atomic, thread-safe sequence increament counter that increments with each message.""" 12 | 13 | ROLLOVER_THRESHOLD = sys.maxsize 14 | 15 | def __init__(self, start=0): 16 | """Initialize a new counter to given initial value.""" 17 | self._lock = threading.Lock() 18 | self._value = start 19 | 20 | def increment(self, num=1): 21 | """Atomically increment the counter by ``num`` and 22 | return the new value. 23 | """ 24 | with self._lock: 25 | self._value += num 26 | if self._value > SequenceCounter.ROLLOVER_THRESHOLD: 27 | self._value = 1 28 | return self._value 29 | 30 | def update_from_msg(self, value): 31 | """Method to compare value received and current value 32 | if it is greater then current value update my current value. 33 | """ 34 | with self._lock: 35 | if value > self._value: 36 | self._value = value 37 | 38 | 39 | class ResponseID(object): 40 | """An atomic, thread-safe counter that increments with each service routine.""" 41 | 42 | ROLLOVER_THRESHOLD = sys.maxsize 43 | 44 | def __init__(self, start=0): 45 | """Initialize a new counter to given initial value.""" 46 | self._lock = threading.Lock() 47 | self._value = start 48 | 49 | def increment(self, num=1): 50 | """Atomically increment the counter by ``num`` and 51 | return the new value. 52 | """ 53 | with self._lock: 54 | self._value += num 55 | if self._value > ResponseID.ROLLOVER_THRESHOLD: 56 | self._value = 1 57 | return self._value 58 | 59 | def update_from_msg(self, value): 60 | """Method to compare value received and current value 61 | if it is greater then current value update my current value. 62 | """ 63 | with self._lock: 64 | if value > self._value: 65 | self._value = value 66 | 67 | 68 | class Header(Message): 69 | """ 70 | The header class is responsible for coordinating and understanding messages between users. 71 | 72 | The Header class provides methods for parsing, updating, and accessing the header fields of a message, 73 | and provides a means of defining attributes of the message in order to accept or ignore specific messages. 74 | 75 | Parameters 76 | ---------- 77 | increment_response_ID : bool, optional 78 | Whether to increment the response ID when creating a new instance of Header. 79 | sequence_id : int, optional 80 | The sequence ID of the message. Optional for parsing. 81 | response_id : int, optional 82 | The response ID of the message. Optional for parsing. 83 | device_id : str, optional 84 | The device ID of the message. Optional for parsing. 85 | time_stamp : str, optional 86 | The timestamp of the message. Optional for parsing. 87 | 88 | Attributes 89 | ---------- 90 | increment_response_ID : bool 91 | Whether to increment the response ID when creating a new instance of Header. 92 | sequence_id : int 93 | Sequence ID is an atomic counter that increments with each message. 94 | response_id : int 95 | Response ID is an int that increments with request routine. 96 | device_id : str 97 | Device ID coresponds to the unique system identifier that send the message. 98 | time_stamp : str 99 | Timestamp is the time in which the message was sent. 100 | """ 101 | 102 | _shared_sequence_counter = None 103 | _shared_response_id_counter = None 104 | _device_id = None 105 | 106 | def __init__(self, increment_response_ID=False, sequence_id=None, response_id=None, device_id=None, time_stamp=None): 107 | super(Header, self).__init__() 108 | self["sequence_id"] = sequence_id or self._ensure_sequence_id() 109 | self["response_id"] = response_id or self._ensure_response_id(increment_response_ID) 110 | self["device_id"] = device_id or self._get_device_id() 111 | self["time_stamp"] = time_stamp or self._get_time_stamp() 112 | 113 | @classmethod 114 | def parse(cls, value): 115 | """Parse the header information 116 | from the input value 117 | """ 118 | instance = cls(value["sequence_id"], value["response_id"], value["device_id"], value["time_stamp"]) 119 | instance._update_sequence_counter_from_message(value["sequence_id"]) 120 | instance._update_response_id_from_message(value["response_id"]) 121 | return instance 122 | 123 | def _get_device_id(self): 124 | """Ensure device ID is set and return it. 125 | If not set, generate a new device ID. 126 | """ 127 | if not Header._device_id: 128 | Header._device_id = str(uuid.uuid4()) 129 | self.device_id = Header._device_id 130 | else: 131 | self.device_id = Header._device_id 132 | return self.device_id 133 | 134 | def _get_time_stamp(self): 135 | """Generate timestamp and return it.""" 136 | self.time_stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") 137 | return self.time_stamp 138 | 139 | def _ensure_sequence_id(self): 140 | """Ensure SequenceID is set and return it. 141 | If not set, generate new shared counter. 142 | """ 143 | if not Header._shared_sequence_counter: 144 | Header._shared_sequence_counter = SequenceCounter() 145 | self.sequence_id = Header._shared_sequence_counter._value 146 | else: 147 | self.sequence_id = Header._shared_sequence_counter.increment() 148 | return self.sequence_id 149 | 150 | def _ensure_response_id(self, increment_response_ID=False): 151 | """Ensure ResponseID is set and return it. 152 | If not set, generate new shared counter. 153 | """ 154 | if not Header._shared_response_id_counter: 155 | Header._shared_response_id_counter = ResponseID() 156 | self.response_id = Header._shared_response_id_counter._value 157 | else: 158 | if increment_response_ID: 159 | self.response_id = Header._shared_response_id_counter.increment() 160 | self.response_id = Header._shared_response_id_counter._value 161 | else: 162 | self.response_id = Header._shared_response_id_counter._value 163 | return self.response_id 164 | 165 | def _update_sequence_counter_from_message(self, sequence_id): 166 | """Update SequnceID Value if the message input is greater then 167 | current value +1. Used to ensure messages across devices are in sync 168 | upon receiving a message if device is restarted. 169 | """ 170 | if Header._shared_sequence_counter is not None: 171 | Header._shared_sequence_counter.update_from_msg(sequence_id) 172 | else: 173 | Header._shared_sequence_counter = SequenceCounter(start=sequence_id) 174 | 175 | def _update_response_id_from_message(self, response_id): 176 | """Update ResponseID Value if the message input is greater then 177 | current value +1. Used to ensure messages across devices are in sync 178 | upon receiving a message if device is restarted. 179 | """ 180 | if Header._shared_response_id_counter is not None: 181 | Header._shared_response_id_counter.update_from_msg(response_id) 182 | else: 183 | Header._shared_response_id_counter = ResponseID(start=response_id) 184 | 185 | def update_ids_from_message(self, sequence_id, response_id): 186 | """Update SequenceID and ResponseID values based on message inputs.""" 187 | self._update_sequence_counter_from_message(sequence_id) 188 | self._update_response_id_from_message(response_id) 189 | 190 | 191 | class GetTrajectoryRequest(Message): 192 | """ 193 | The GetTrajectoryRequest class represents a request message from a user 194 | to the CAD for retrieving a trajectory. 195 | 196 | Parameters 197 | ---------- 198 | element_id : str 199 | The ID of the element associated with the trajectory. 200 | robot_name : str 201 | The name of the robot associated with the trajectory. 202 | header : Header, optional 203 | The header object containing additional message information. 204 | 205 | Attributes 206 | ---------- 207 | header : Header 208 | The header object containing additional message information. 209 | element_id : str 210 | The ID of the step in the BuildingPlan associated with the trajectory. 211 | robot_name : str 212 | The name of the robot associated with the trajectory. 213 | trajectory_id : str 214 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 215 | """ 216 | 217 | def __init__(self, element_id, robot_name, header=None, *args, **kwargs): 218 | super(GetTrajectoryRequest, self).__init__(*args, **kwargs) 219 | self["header"] = header or Header(increment_response_ID=True) 220 | self["element_id"] = element_id 221 | self["robot_name"] = robot_name 222 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 223 | 224 | @classmethod 225 | def parse(cls, data): 226 | """Parse the GetTrajectoryRequest message from the input data. 227 | Starts by parsing the header information and then the Message. 228 | """ 229 | return cls(data["element_id"], data["robot_name"], Header.parse(data["header"])) 230 | 231 | 232 | class GetTrajectoryResult(Message): 233 | """ 234 | The GetTrajectoryResult class represents a response message from the CAD 235 | to all active devices containing a retrieved trajectory. 236 | 237 | Parameters 238 | ---------- 239 | element_id : str 240 | The ID of the element associated with the trajectory. 241 | robot_name : str 242 | The name of the robot associated with the trajectory. 243 | robot_base_frame : compas.geometry.Frame 244 | The base frame of the robot. 245 | trajectory : dict of joint names and joint values 246 | The retrieved trajectory. 247 | header : Header, optional 248 | The header object containing additional message information. 249 | 250 | Attributes 251 | ---------- 252 | header : Header 253 | The header object containing additional message information. 254 | element_id : str 255 | The ID of the step in the BuildingPlan associated with the trajectory. 256 | robot_name : str 257 | The name of the robot associated with the trajectory. 258 | robot_base_frame : compas.geometry.Frame 259 | The base frame of the robot. 260 | trajectory_id : str 261 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 262 | trajectory : dict of joint names and joint values 263 | The trajectory information computed for the request. 264 | """ 265 | 266 | def __init__(self, element_id, robot_name, robot_base_frame, trajectory, pick_and_place=False, pick_index=None, end_effector_link_name=None, header=None): 267 | super(GetTrajectoryResult, self).__init__() 268 | self["header"] = header or Header() 269 | self["element_id"] = element_id 270 | self["robot_name"] = robot_name 271 | self["robot_base_frame"] = robot_base_frame 272 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 273 | self["pick_and_place"] = pick_and_place 274 | self["pick_index"] = pick_index 275 | self["end_effector_link_name"] = end_effector_link_name 276 | self["trajectory"] = trajectory 277 | 278 | @classmethod 279 | def parse(cls, data): 280 | """Parse the GetTrajectoryResult message from the input data. 281 | Starts by parsing the header information and then the Message. 282 | """ 283 | return cls( 284 | data["element_id"], 285 | data["robot_name"], 286 | Frame(**data["robot_base_frame"]), 287 | data["trajectory"], 288 | data["pick_and_place"], 289 | data["pick_index"], 290 | data["end_effector_link_name"], 291 | Header.parse(data["header"]), 292 | ) 293 | 294 | 295 | class ApproveTrajectory(Message): 296 | """ 297 | The ApproveTrajectory class represents a response message between 298 | all active devices containing an approval decision for each user. 299 | 300 | Parameters 301 | ---------- 302 | element_id : str 303 | The ID of the element associated with the trajectory. 304 | robot_name : str 305 | The name of the robot associated with the trajectory. 306 | trajectory : dict of joint names and joint values 307 | The approved trajectory. 308 | approval_status : int 309 | The approval status of the trajectory. 310 | header : Header, optional 311 | The header object containing additional message information. 312 | 313 | Attributes 314 | ---------- 315 | header : Header 316 | The header object containing additional message information. 317 | element_id : str 318 | The ID of the step in the BuildingPlan associated with the trajectory. 319 | robot_name : str 320 | The name of the robot associated with the trajectory. 321 | trajectory_id : str 322 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 323 | trajectory : dict of joint names and joint values 324 | The approved trajectory. 325 | approval_status : int 326 | The approval status of the trajectory. 327 | 0: Not Approved, 1: Approved, 2: Consensus Approval, 3: Cancelation. 328 | """ 329 | 330 | def __init__(self, element_id, robot_name, trajectory, approval_status, header=None): 331 | super(ApproveTrajectory, self).__init__() 332 | self["header"] = header or Header() 333 | self["element_id"] = element_id 334 | self["robot_name"] = robot_name 335 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 336 | self["trajectory"] = trajectory 337 | self["approval_status"] = approval_status 338 | 339 | @classmethod 340 | def parse(cls, data): 341 | """Parse the ApproveTrajectory message from the input data. 342 | Starts by parsing the header information and then the Message. 343 | """ 344 | return cls( 345 | data["element_id"], 346 | data["robot_name"], 347 | data["trajectory"], 348 | data["approval_status"], 349 | Header.parse(data["header"]), 350 | ) 351 | 352 | 353 | class ApprovalCounterRequest(Message): 354 | """ 355 | The ApprovalCounterRequest class represents a request message from a single user 356 | to all active users to retrieve a count of all activie devices. 357 | 358 | Parameters 359 | ---------- 360 | element_id : str 361 | The ID of the element associated with the approval counter. 362 | header : Header, optional 363 | The header object containing additional message information. 364 | 365 | Attributes 366 | ---------- 367 | header : Header 368 | The header object containing additional message information. 369 | element_id : str 370 | The ID of the element associated with the approval counter. 371 | trajectory_id : str 372 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 373 | """ 374 | 375 | def __init__(self, element_id, header=None): 376 | super(ApprovalCounterRequest, self).__init__() 377 | self["header"] = header or Header() 378 | self["element_id"] = element_id 379 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 380 | 381 | @classmethod 382 | def parse(cls, data): 383 | """Construct an object of this type from the provided data to support COMPAS JSON serialization. 384 | 385 | Parameters 386 | ---------- 387 | data : dict 388 | The raw Python data representing the object. 389 | 390 | Returns 391 | ------- 392 | object 393 | """ 394 | return cls(data["element_id"], Header.parse(data["header"])) 395 | 396 | 397 | class ApprovalCounterResult(Message): 398 | """ 399 | The ApprovalCounterResult class represents a response message from all active devices 400 | containing to notify the primary device of the users listening. 401 | 402 | Parameters 403 | ---------- 404 | element_id : str 405 | The ID of the element associated with the approval counter. 406 | header : Header, optional 407 | The header object containing additional message information. 408 | 409 | Attributes 410 | ---------- 411 | header : Header 412 | The header object containing additional message information. 413 | element_id : str 414 | The ID of the element associated with the approval counter. 415 | trajectory_id : str 416 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 417 | """ 418 | 419 | def __init__(self, element_id, header=None): 420 | super(ApprovalCounterResult, self).__init__() 421 | self["header"] = header or Header() 422 | self["element_id"] = element_id 423 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 424 | 425 | @classmethod 426 | def parse(cls, data): 427 | """Parse the ApprovalCounterResult message from the input data. 428 | Starts by parsing the header information and then the Message. 429 | """ 430 | return cls(data["element_id"], Header.parse(data["header"])) 431 | 432 | 433 | class SendTrajectory(Message): 434 | """ 435 | The SendTrajectory class represents a message from a user to the CAD 436 | to give the Approval for Robotic Exacution. 437 | 438 | Parameters 439 | ---------- 440 | element_id : str 441 | The ID of the element associated with the trajectory. 442 | robot_name : str 443 | The name of the robot associated with the trajectory. 444 | trajectory : dict of joint names and joint values 445 | The trajectory to be sent. 446 | header : Header, optional 447 | The header object containing additional message information. 448 | 449 | Attributes 450 | ---------- 451 | header : Header 452 | The header object containing additional message information. 453 | element_id : str 454 | The ID of the element associated with the trajectory. 455 | robot_name : str 456 | The name of the robot associated with the trajectory. 457 | trajectory_id : str 458 | The ID of the trajectory. Default is ``"trajectory_id_" + str(element_id)``. 459 | trajectory : dict of joint names and joint values 460 | The trajectory to be sent. 461 | """ 462 | 463 | def __init__(self, element_id, robot_name, trajectory, header=None): 464 | super(SendTrajectory, self).__init__() 465 | self["header"] = header or Header() 466 | self["element_id"] = element_id 467 | self["robot_name"] = robot_name 468 | self["trajectory_id"] = "trajectory_id_" + str(element_id) 469 | self["trajectory"] = trajectory 470 | 471 | @classmethod 472 | def parse(cls, data): 473 | """Parse the SendTrajectory message from the input data. 474 | Starts by parsing the header information and then the Message. 475 | """ 476 | return cls(data["element_id"], data["robot_name"], data["trajectory"], Header.parse(data["header"])) 477 | -------------------------------------------------------------------------------- /src/compas_xr/project/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr.project 4 | ******************************************************************************** 5 | 6 | This package contains classes to manage the XR project including the assembly 7 | and building plan. 8 | 9 | .. currentmodule:: compas_xr.project 10 | 11 | Classes 12 | ------- 13 | 14 | .. autosummary:: 15 | :toctree: generated/ 16 | :nosignatures: 17 | 18 | ProjectManager 19 | AssemblyExtensions 20 | BuildingPlanExtensions 21 | 22 | """ 23 | 24 | from compas_xr.project.assembly_extensions import AssemblyExtensions 25 | from compas_xr.project.buildingplan_extensions import BuildingPlanExtensions 26 | from compas_xr.project.project_manager import ProjectManager 27 | 28 | __all__ = ["ProjectManager", "AssemblyExtensions", "BuildingPlanExtensions"] 29 | -------------------------------------------------------------------------------- /src/compas_xr/project/assembly_extensions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from compas.datastructures import Assembly 4 | from compas.datastructures import Mesh 5 | from compas.datastructures import Part 6 | from compas.geometry import Frame 7 | from compas.geometry import Point 8 | from compas.geometry import Transformation 9 | from compas.geometry import Vector 10 | from compas_timber.consumers import BrepGeometryConsumer 11 | 12 | 13 | class AssemblyExtensions(object): 14 | """ 15 | AssemblyExtensions is a class for extending the functionality of the :class:`~compas.datastructures.Assembly` class. 16 | 17 | The AssemblyExtensions class provides additional functionalities such as exporting parts as .obj files 18 | and creating a frame assembly from a list of :class:`~compas.geometry.Frame` with a specific data structure 19 | for localization information. 20 | 21 | """ 22 | 23 | def export_timberassembly_objs(self, assembly, folder_path, new_folder_name, z_to_y_remap=False): 24 | """ 25 | Export timber assembly beams as .obj files to a folder path. 26 | 27 | Parameters 28 | ---------- 29 | assembly : :class:`~compas_timber.assembly.TimberAssembly` 30 | The assembly that you want to export beams from. 31 | folder_path : str 32 | The path in which you would like to create a storage folder. 33 | new_folder_name : str 34 | The name of the folder you would like to create. 35 | z_to_y_remap : bool, optional 36 | A boolean that determines if the z-axis should be remapped to the y-axis for .obj export. Default is False. 37 | 38 | Returns 39 | ------- 40 | None 41 | 42 | """ 43 | target_folder_path = os.path.join(folder_path, new_folder_name) 44 | if not os.path.exists(target_folder_path): 45 | os.makedirs(target_folder_path) 46 | 47 | if z_to_y_remap: 48 | frame = Frame(Point(0, 0, 0), Vector.Xaxis(), Vector.Zaxis()) 49 | else: 50 | frame = Frame.worldXY() 51 | 52 | for result in BrepGeometryConsumer(assembly).result: 53 | brep = result.geometry 54 | beam_frame = result.beam.frame 55 | key = result.beam.key 56 | 57 | brep_meshes = brep.to_meshes() 58 | compas_mesh = Mesh() 59 | for mesh in brep_meshes: 60 | compas_mesh.join(mesh) 61 | mesh_transformed = compas_mesh.transformed(Transformation.from_frame_to_frame(beam_frame, frame)) 62 | 63 | filename = "{}.obj".format(str(key)) 64 | mesh_transformed.to_obj(os.path.join(target_folder_path, filename)) 65 | 66 | def export_mesh_assembly_objs(self, assembly, folder_path, new_folder_name, z_to_y_remap=False): 67 | """ 68 | Export Mesh assembly parts as .obj files to a folder path. 69 | 70 | Parameters 71 | ---------- 72 | assembly : :class:`~compas.datastructures.Assembly` 73 | The Mesh assembly that you want to export parts from. 74 | folder_path : str 75 | The path in which you would like to create a storage folder. 76 | new_folder_name : str 77 | The name of the folder you would like to create. 78 | z_to_y_remap : bool, optional 79 | A boolean that determines if the z-axis should be remapped to the y-axis for .obj export. Default is False. 80 | 81 | Returns 82 | ------- 83 | None 84 | 85 | """ 86 | target_folder_path = os.path.join(folder_path, new_folder_name) 87 | if not os.path.exists(target_folder_path): 88 | os.makedirs(target_folder_path) 89 | 90 | if z_to_y_remap: 91 | frame = Frame(Point(0, 0, 0), Vector.Xaxis(), Vector.Zaxis()) 92 | else: 93 | frame = Frame.worldXY() 94 | 95 | for part in assembly.parts(): 96 | # Mesh assembly can be made with or without a frame (ex. assembly.add_part(Mesh)) try & default to worldXY 97 | if hasattr(part, "frame"): 98 | part_frame = part.frame 99 | else: 100 | part_frame = Frame.worldXY() 101 | 102 | # TODO: This is weird, but I can't transform a Part object, so I need to check if it's a Part or a Mesh 103 | transformation = Transformation.from_frame_to_frame(part_frame, frame) 104 | if isinstance(part, Part): 105 | part_transformed = part.attributes["shape"].transformed(transformation) 106 | else: 107 | part_transformed = part.transformed(transformation) 108 | 109 | filename = "{}.obj".format(str(part.key)) 110 | part_transformed.to_obj(os.path.join(target_folder_path, filename)) 111 | 112 | def create_qr_assembly(self, qr_frames): 113 | """ 114 | Create a frame assembly from a list of :class:`~compas.geometry.Frame` with a specific data structure for localization. 115 | 116 | Parameters 117 | ---------- 118 | qr_frames : list of :class:`~compas.geometry.Frame` 119 | A list of frames at specific locations for localization data. 120 | 121 | Returns 122 | ------- 123 | :class:`~compas.datastructures.Assembly` 124 | The constructed database reference. 125 | 126 | """ 127 | assembly = Assembly() 128 | for i, frame in enumerate(qr_frames): 129 | name = "QR_{}".format(i) 130 | part = Part(name=name, frame=frame, shape=frame) 131 | assembly.add_part(part) 132 | return assembly 133 | -------------------------------------------------------------------------------- /src/compas_xr/project/buildingplan_extensions.py: -------------------------------------------------------------------------------- 1 | from compas_timber.planning import BuildingPlan 2 | from compas_timber.planning import SimpleSequenceGenerator 3 | from compas_timber.planning import Step 4 | 5 | 6 | class BuildingPlanExtensions(object): 7 | """ 8 | BuildingPlanExtensions is a class for extending the functionality of the :class:`~compas_timber.planning.BuildingPlan` class. 9 | 10 | The BuildingPlanExtensions class provides additional functionalities to the :class:`~compas_timber.planning.BuildingPlan` 11 | by providing a way to create a buildling plan from an established assembly sequence. 12 | """ 13 | 14 | # TODO: This makes the building plan in a very manual way 15 | # TODO: but this needs to be resolved in tandem with building plan revisions. 16 | def create_buildingplan_from_assembly_sequence(self, assembly, data_type, robot_keys, priority_keys_lists): 17 | """ 18 | Create a compas_timber.planning.BuildingPlan based on the sequence of the assembly parts. 19 | 20 | Parameters 21 | ---------- 22 | assembly : :class:`~compas_timber.assembly.TimberAssembly` or :class:`~compas.datastructures.Assembly` 23 | The assembly that you want to generate the buiding plan for. 24 | data_type : int 25 | List index of which data type will be loaded on the application side [0: 'Cylinder', 1: 'Box', 2: 'ObjFile'] 26 | robot_keys : list of str 27 | List of keys that are intended to be built by the robot. 28 | priority_keys_lists : list of list of str 29 | List in assembly order of lists of assembly keys that can be built in parallel. 30 | 31 | Returns 32 | ------- 33 | building_plan : :class:`~compas_timber.planning.BuildingPlan` 34 | The building plan generated from the assembly sequence. 35 | 36 | """ 37 | data_type_list = ["0.Cylinder", "1.Box", "2.ObjFile"] 38 | building_plan = SimpleSequenceGenerator(assembly=assembly).result 39 | 40 | for step in building_plan.steps: 41 | step.geometry = data_type_list[data_type] 42 | # TODO: These are unused for now, but are expeted on the application side 43 | step.instructions = ["none"] 44 | step.elements_held = [0] 45 | 46 | element_key = str(step.element_ids[0]) 47 | if robot_keys: 48 | if element_key in robot_keys: 49 | step.actor = "ROBOT" 50 | 51 | if not priority_keys_lists: 52 | step.priority = 0 53 | else: 54 | for i, keys_list in enumerate(priority_keys_lists): 55 | if element_key in keys_list: 56 | step.priority = i 57 | break 58 | 59 | return building_plan 60 | 61 | def create_buildingplan_from_with_custom_sequence(self, assembly, sequenced_keys, data_type, robot_keys, priority_keys_lists): 62 | """ 63 | Create a compas_timber.planning.BuildingPlan based on the sequence of the assembly parts. 64 | 65 | Parameters 66 | ---------- 67 | assembly : compas_timber.assembly.TimberAssembly or compas.datastructures.Assembly 68 | The assembly that you want to generate the buiding plan for. 69 | sequenced_keys : list of str 70 | List of keys that are intended to be built in the order provided. 71 | data_type : int 72 | List index of which data type will be loaded on the application side [0: 'Cylinder', 1: 'Box', 2: 'ObjFile'] 73 | robot_keys : list of str 74 | List of keys that are intended to be built by the robot. 75 | priority_keys_lists : list of list of str 76 | List in assembly order of lists of assembly keys that can be built in parallel. 77 | 78 | Returns 79 | ------- 80 | building_plan : compas_timber.planning.BuildingPlan 81 | The building plan generated from the assembly sequence. 82 | 83 | """ 84 | data_type_list = ["0.Cylinder", "1.Box", "2.ObjFile"] 85 | graph_data = assembly.graph.__data__ 86 | node_data = graph_data["node"] 87 | building_plan = BuildingPlan() 88 | 89 | for key in sequenced_keys: 90 | step = Step(key) 91 | # TODO: This is dumb, but the element_ids are generated incorrectly so they are overwritten here 92 | step.element_ids = [key] 93 | step.geometry = data_type_list[data_type] 94 | # TODO: These are unused for now, but are expeted on the application side 95 | step.instructions = ["none"] 96 | step.elements_held = [0] 97 | step.location = node_data[str(key)]["part"].frame 98 | 99 | if robot_keys: 100 | if key in robot_keys: 101 | step.actor = "ROBOT" 102 | else: 103 | step.actor = "HUMAN" 104 | else: 105 | step.actor = "HUMAN" 106 | 107 | if not priority_keys_lists: 108 | step.priority = 0 109 | else: 110 | for i, keys_list in enumerate(priority_keys_lists): 111 | if key in keys_list: 112 | step.priority = i 113 | break 114 | building_plan.add_step(step) 115 | 116 | return building_plan 117 | -------------------------------------------------------------------------------- /src/compas_xr/project/project_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from compas.geometry import Frame 4 | from compas_timber.assembly import TimberAssembly 5 | from compas_timber.planning import BuildingPlan 6 | from compas_timber.planning import Step 7 | 8 | from compas_xr.project.assembly_extensions import AssemblyExtensions 9 | from compas_xr.realtime_database import RealtimeDatabase 10 | from compas_xr.storage import Storage 11 | 12 | 13 | class ProjectManager(object): 14 | """ 15 | The ProjectManager class is responsible for managing project specific data and operations that involve 16 | Firebase Storage and Realtime Database configuration. 17 | 18 | Parameters 19 | ---------- 20 | config_path : str 21 | The path to the configuration file for the project. 22 | 23 | Attributes 24 | ---------- 25 | storage : Storage 26 | The storage instance for the project. 27 | database : RealtimeDatabase 28 | The realtime database instance for the project. 29 | """ 30 | 31 | def __init__(self, config_path): 32 | if not os.path.exists(config_path): 33 | raise Exception("Could not create Storage or Database with path {}!".format(config_path)) 34 | self.storage = Storage(config_path) 35 | self.database = RealtimeDatabase(config_path) 36 | 37 | def application_settings_writer(self, project_name, storage_folder="None", z_to_y_remap=False): 38 | """ 39 | Uploads required application settings to the Firebase RealtimeDatabase. 40 | 41 | Parameters 42 | ---------- 43 | project_name : str 44 | The name of the project where the app will look for information. 45 | storage_folder : str, optional 46 | The name of the storage folder, by default "None" 47 | z_to_y_remap : bool, optional 48 | The orientation of the object, if the obj was exported with z to y remap, by default False 49 | 50 | Returns 51 | ------- 52 | None 53 | 54 | """ 55 | data = {"project_name": project_name, "storage_folder": storage_folder, "z_to_y_remap": z_to_y_remap} 56 | self.database.upload_data(data, "ApplicationSettings") 57 | 58 | def create_project_data_from_compas(self, assembly, building_plan, qr_frames_list): 59 | """ 60 | Formats data structure from COMPAS Class Objects. 61 | 62 | Parameters 63 | ---------- 64 | assembly : :class:`compas.datastructures.Assembly` or :class:`compas_timber.assembly.TimberAssembly` 65 | The assembly in which data will be extracted from. 66 | building_plan : :class:`compas_timber.planning.BuildingPlan` 67 | The BuildingPlan in which data will be extracted from. 68 | qr_frames_list : list of :class:`compas.geometry.Frame` 69 | List of frames at specific locations for application localization data. 70 | 71 | Returns 72 | ------- 73 | None 74 | 75 | """ 76 | qr_assembly = AssemblyExtensions().create_qr_assembly(qr_frames_list) 77 | if isinstance(assembly, TimberAssembly): 78 | data = { 79 | "QRFrames": qr_assembly.__data__, 80 | "assembly": assembly.__data__, 81 | "beams": {beam.key: beam for beam in assembly.beams}, 82 | "joints": {joint.key: joint for joint in assembly.joints}, 83 | "building_plan": building_plan, 84 | } 85 | else: 86 | data = { 87 | "QRFrames": qr_assembly.__data__, 88 | "assembly": assembly.__data__, 89 | "parts": {part.key: part for part in assembly.parts()}, 90 | "building_plan": building_plan, 91 | } 92 | return data 93 | 94 | def upload_data_to_project(self, data, project_name, data_name): 95 | """ 96 | Uploads data to the Firebase RealtimeDatabase under the specified project name. 97 | 98 | Parameters 99 | ---------- 100 | data : Any should be json serializable 101 | The data to be uploaded. 102 | project_name : str 103 | The name of the project under which the data will be stored. 104 | data_name : str 105 | The name of the child in which data will be stored. 106 | 107 | Returns 108 | ------- 109 | None 110 | 111 | """ 112 | self.database.upload_data_to_reference_as_child(data, project_name, data_name) 113 | 114 | def upload_project_data_from_compas(self, project_name, assembly, building_plan, qr_frames_list): 115 | """ 116 | Formats data structure from COMPAS Class Objects and uploads them to the RealtimeDatabase under project name. 117 | 118 | Parameters 119 | ---------- 120 | assembly : :class:`compas.datastructures.Assembly` or :class:`compas_timber.assembly.TimberAssembly` 121 | The assembly in which data will be extracted from. 122 | building_plan : :class:`compas_timber.planning.BuildingPlan` 123 | The BuildingPlan in which data will be extracted from. 124 | qr_frames_list : list of :class:`compas.geometry.Frame` 125 | List of frames at specific locations for application localization data. 126 | project_name : str 127 | The name of the project under which the data will be stored. 128 | 129 | Returns 130 | ------- 131 | None 132 | 133 | """ 134 | data = self.create_project_data_from_compas(assembly, building_plan, qr_frames_list) 135 | self.database.upload_data(data, project_name) 136 | 137 | def upload_qr_frames_to_project(self, project_name, qr_frames_list): 138 | """ 139 | Uploads QR Frames to the Firebase RealtimeDatabase under the specified project name. 140 | 141 | Parameters 142 | ---------- 143 | qr_frames_list : list of :class:`compas.geometry.Frame` 144 | List of frames at specific locations for application localization data. 145 | project_name : str 146 | The name of the project under which the data will be stored. 147 | 148 | Returns 149 | ------- 150 | None 151 | 152 | """ 153 | qr_assembly = AssemblyExtensions().create_qr_assembly(qr_frames_list) 154 | data = qr_assembly.__data__ 155 | self.database.upload_data_to_reference_as_child(data, project_name, "QRFrames") 156 | 157 | def upload_obj_to_storage(self, path_local, storage_folder_name): 158 | """ 159 | Upload an .obj file to the Firebase Storage under the specified storage folder name. 160 | 161 | Parameters 162 | ---------- 163 | file_path : str 164 | The path at which the obj file is stored. 165 | storage_folder_name : str 166 | The name of the storage folder where the .obj file will be uploaded. 167 | 168 | Returns 169 | ------- 170 | None 171 | 172 | """ 173 | storage_folder_list = ["obj_storage", storage_folder_name] 174 | self.storage.upload_file_as_bytes_to_deep_reference(path_local, storage_folder_list) 175 | 176 | def upload_objs_from_directory_to_storage(self, local_directory, storage_folder_name): 177 | """ 178 | Uploads all .obj files from a directory to the Firebase Storage under the specified storage folder name. 179 | 180 | Parameters 181 | ---------- 182 | directory_path : str 183 | The path to the directory where the projects .obj files are stored. 184 | storage_folder_name : str 185 | The name of the storage folder where the .obj files will be uploaded. 186 | 187 | Returns 188 | ------- 189 | None 190 | 191 | """ 192 | storage_folder_list = ["obj_storage", storage_folder_name] 193 | self.storage.upload_files_as_bytes_from_directory_to_deep_reference(local_directory, storage_folder_list) 194 | 195 | def get_project_data(self, project_name): 196 | """ 197 | Retrieves data from the Firebase RealtimeDatabase under the specified project name. 198 | 199 | Parameters 200 | ---------- 201 | project_name : str 202 | The name of the project under which the data will be stored. 203 | 204 | Returns 205 | ------- 206 | data : dict 207 | The data retrieved from the database at the point of fetching. 208 | 209 | """ 210 | return self.database.get_data(project_name) 211 | 212 | def upload_compas_object_to_storage(self, compas_object, cloud_file_name, pretty=True): 213 | """ 214 | Uploads an assembly to the Firebase Storage. 215 | 216 | Parameters 217 | ---------- 218 | compas_object : Any 219 | Any compas class instance that is serializable. 220 | cloud_file_name : str 221 | The name of the cloud file. Saved in JSON format, and needs to have a .json extension. 222 | 223 | Returns 224 | ------- 225 | None 226 | 227 | """ 228 | self.storage.upload_data(compas_object, cloud_file_name, pretty=pretty) 229 | 230 | def get_assembly_from_storage(self, cloud_file_name): 231 | """ 232 | Retrieves an assembly from the Firebase Storage. 233 | 234 | Parameters 235 | ---------- 236 | cloud_file_name : str 237 | The name of the cloud file. 238 | 239 | Returns 240 | ------- 241 | assembly : :class:`compas.datastructures.Assembly` or :class:`compas_timber.assembly.TimberAssembly` 242 | The assembly retrieved from the storage. 243 | 244 | """ 245 | return self.storage.get_data(cloud_file_name) 246 | 247 | def edit_step_on_database(self, project_name, key, actor, is_built, is_planned, priority): 248 | """ 249 | Edits a building plan step in the Firebase RealtimeDatabase under the specified project name. 250 | 251 | Parameters 252 | ---------- 253 | project_name : str 254 | The name of the project under which the data will be stored. 255 | key : str 256 | The key of the building plan step to be edited. 257 | actor : str 258 | The actor who will be performing the step. 259 | is_built : bool 260 | A boolean that determines if the step is built. 261 | is_planned : bool 262 | A boolean that determines if the step is planned. 263 | priority : int 264 | The priority of the step. 265 | 266 | Returns 267 | ------- 268 | None 269 | 270 | """ 271 | database_reference_list = [project_name, "building_plan", "data", "steps", key, "data"] 272 | current_data = self.database.get_data_from_deep_reference(database_reference_list) 273 | current_data["actor"] = actor 274 | current_data["is_built"] = is_built 275 | current_data["is_planned"] = is_planned 276 | current_data["priority"] = priority 277 | self.database.upload_data_to_deep_reference(current_data, database_reference_list) 278 | 279 | def visualize_project_state_timbers(self, timber_assembly, project_name): 280 | """ 281 | Retrieves and visualizes data from the Firebase RealtimeDatabase under the specified project name. 282 | 283 | Parameters 284 | ---------- 285 | timber_assembly : :class:`compas_timbers.assembly.TimberAssembly` 286 | The assembly in which the project is based off of: Used for part visulization. 287 | project_name : str 288 | The name of the project under which the data will be stored. 289 | 290 | Returns 291 | ------- 292 | last_built_index : int 293 | The index of the last built part in the project. 294 | step_locations : list of :class:`compas.geometry.Frame` 295 | The locations of the building plan steps. 296 | built_human : list of :class:`compas_timber.beam.Blank` 297 | The parts that have been built by a human. 298 | unbuilt_human : list of :class:`compas_timber.beam.Blank` 299 | The parts that have not been built by a human. 300 | built_robot : list of :class:`compas_timber.beam.Blank` 301 | The parts that have been built by a robot. 302 | unbuilt_robot : list of :class:`compas_timber.beam.Blank` 303 | The parts that have not been built by a robot. 304 | 305 | """ 306 | nodes = timber_assembly.graph.__data__["node"] 307 | buiding_plan_data_reference_list = [project_name, "building_plan", "data"] 308 | current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list) 309 | 310 | built_human = [] 311 | unbuilt_human = [] 312 | built_robot = [] 313 | unbuilt_robot = [] 314 | step_locations = [] 315 | 316 | # Try to get the value for the last built index, if it doesn't exist make it null 317 | # TODO: This is a bit weird, but it will throw an error if I pass the last 318 | # TODO: built index to the BuildingPlan constructor 319 | if "LastBuiltIndex" in current_state_data: 320 | last_built_index = current_state_data["LastBuiltIndex"] 321 | current_state_data.pop("LastBuiltIndex") 322 | else: 323 | last_built_index = None 324 | if "PriorityTreeDictionary" in current_state_data: 325 | current_state_data.pop("PriorityTreeDictionary") 326 | 327 | if "PriorityTreeDictionary" in current_state_data: 328 | current_state_data.pop("PriorityTreeDictionary") 329 | 330 | building_plan = BuildingPlan.__from_data__(current_state_data) 331 | for step in building_plan.steps: 332 | step_data = step["data"] 333 | # Try to get the value for device_id, and if it exists remove it. 334 | if "device_id" in step_data: 335 | step_data.pop("device_id") 336 | step = Step.__from_data__(step["data"]) 337 | step_locations.append(Frame.__from_data__(step.location)) 338 | assembly_element_id = step.element_ids[0] 339 | # TODO: Tried to write like this, but find_by_key returns a NoneType object 340 | """ 341 | part = timber_assembly.find_by_key(assembly_element_id) 342 | """ 343 | part = nodes[assembly_element_id]["part"] 344 | if step.actor == "HUMAN": 345 | if step.is_built: 346 | built_human.append(part.blank) 347 | else: 348 | unbuilt_human.append(part.blank) 349 | else: 350 | if step.is_built: 351 | built_robot.append(part.blank) 352 | else: 353 | unbuilt_robot.append(part.blank) 354 | return last_built_index, step_locations, built_human, unbuilt_human, built_robot, unbuilt_robot 355 | 356 | def visualize_project_state(self, assembly, project_name): 357 | """ 358 | Retrieves and visualizes data from the Firebase RealtimeDatabase under the specified project name. 359 | 360 | Parameters 361 | ---------- 362 | assembly : :class:`compas.datastructure.Assembly` 363 | The assembly in which the project is based off of: Used for part visulization. 364 | project_name : str 365 | The name of the project under which the data is stored. 366 | 367 | Returns 368 | ------- 369 | last_built_index : int 370 | The index of the last built part in the project. 371 | step_locations : list of :class:`compas.geometry.Frame` 372 | The locations of the building plan steps. 373 | built_human : list of :class:`compas.datastructures.Part` 374 | The parts that have been built by a human. 375 | unbuilt_human : list of :class:`compas.datastructures.Part` 376 | The parts that have not been built by a human. 377 | built_robot : list of :class:`compas.datastructures.Part` 378 | The parts that have been built by a robot. 379 | unbuilt_robot : list of :class:`compas.datastructures.Part` 380 | The parts that have not been built by a robot. 381 | 382 | """ 383 | buiding_plan_data_reference_list = [project_name, "building_plan", "data"] 384 | current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list) 385 | nodes = assembly.graph.__data__["node"] 386 | 387 | built_human = [] 388 | unbuilt_human = [] 389 | built_robot = [] 390 | unbuilt_robot = [] 391 | step_locations = [] 392 | 393 | # Try to get the value for the last built index, if it doesn't exist make it null 394 | # TODO: This is a bit weird, but it will throw an error if I pass the last built index to the BuildingPlan 395 | if "LastBuiltIndex" in current_state_data: 396 | last_built_index = current_state_data["LastBuiltIndex"] 397 | current_state_data.pop("LastBuiltIndex") 398 | else: 399 | last_built_index = None 400 | if "PriorityTreeDictionary" in current_state_data: 401 | current_state_data.pop("PriorityTreeDictionary") 402 | 403 | if "PriorityTreeDictionary" in current_state_data: 404 | current_state_data.pop("PriorityTreeDictionary") 405 | 406 | building_plan = BuildingPlan.__from_data__(current_state_data) 407 | for step in building_plan.steps: 408 | step_data = step["data"] 409 | # Try to get the value for device_id, and if it exists remove it. 410 | if "device_id" in step_data: 411 | step_data.pop("device_id") 412 | step = Step.__from_data__(step["data"]) 413 | step_locations.append(Frame.__from_data__(step.location)) 414 | assembly_element_id = step.element_ids[0] 415 | part = nodes[str(assembly_element_id)]["part"] 416 | 417 | if step.actor == "HUMAN": 418 | # TODO: I am not sure if this works in all scenarios of Part 419 | if step.is_built: 420 | built_human.append(part) 421 | else: 422 | unbuilt_human.append(part) 423 | elif step.actor == "ROBOT": 424 | if step.is_built: 425 | built_robot.append(part) 426 | else: 427 | unbuilt_robot.append(part) 428 | else: 429 | raise Exception("Part actor is Unknown!") 430 | return last_built_index, step_locations, built_human, unbuilt_human, built_robot, unbuilt_robot 431 | -------------------------------------------------------------------------------- /src/compas_xr/realtime_database/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr.realtime_database 4 | ******************************************************************************** 5 | 6 | This package contains classes for using Firebase realtime database. 7 | 8 | .. currentmodule:: compas_xr.realtime_database 9 | 10 | Classes 11 | ------- 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | :nosignatures: 16 | 17 | RealtimeDatabase 18 | 19 | """ 20 | 21 | import sys 22 | 23 | if sys.platform == "cli": 24 | from compas_xr.realtime_database.realtime_database_cli import RealtimeDatabase 25 | else: 26 | from compas_xr.realtime_database.realtime_database_pyrebase import RealtimeDatabase 27 | 28 | __all__ = ["RealtimeDatabase"] 29 | -------------------------------------------------------------------------------- /src/compas_xr/realtime_database/realtime_database_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import threading 5 | 6 | import clr 7 | from compas.data import json_dumps 8 | 9 | from compas_xr.realtime_database.realtime_database_interface import RealtimeDatabaseInterface 10 | 11 | try: 12 | from urllib.request import urlopen 13 | except ImportError: 14 | from urllib import urlopen 15 | 16 | 17 | lib_dir = os.path.join(os.path.dirname(__file__), "..", "dependencies") 18 | if lib_dir not in sys.path: 19 | sys.path.append(lib_dir) 20 | 21 | clr.AddReference("Newtonsoft.Json.dll") 22 | clr.AddReference("Firebase.Auth.dll") 23 | clr.AddReference("Firebase.dll") 24 | clr.AddReference("LiteDB.dll") 25 | clr.AddReference("System.Reactive.dll") 26 | 27 | from Firebase.Database import FirebaseClient # noqa: E402 28 | from Firebase.Database.Query import QueryExtensions # noqa: E402 29 | 30 | 31 | class RealtimeDatabase(RealtimeDatabaseInterface): 32 | """ 33 | A RealtimeDatabase is defined by a Firebase configuration path and database reference. 34 | 35 | The RealtimeDatabase class is responsible for initializing and managing the connection to a Firebase Realtime Database. 36 | It ensures that the database connection is established only once and shared across all instances of the class. 37 | 38 | Parameters 39 | ---------- 40 | config_path : str 41 | The path to the Firebase configuration JSON file. 42 | 43 | Attributes 44 | ---------- 45 | config_path : str 46 | The path to the Firebase configuration JSON file. 47 | database : FirebaseClient 48 | The FirebaseClient instance representing the connection to the Firebase Realtime Database. 49 | _shared_database : FirebaseClient, class attribute 50 | The shared FirebaseClient instance representing the connection to the Firebase Realtime Database. 51 | """ 52 | 53 | _shared_database = None 54 | 55 | def __init__(self, config_path): 56 | self.config_path = config_path 57 | self.database = self._ensure_database() 58 | 59 | def _ensure_database(self): 60 | """ 61 | Ensures that the database connection is established. 62 | If the connection is not yet established, it initializes it. 63 | If the connection is already established, it returns the existing connection. 64 | """ 65 | if not RealtimeDatabase._shared_database: 66 | path = self.config_path 67 | if not os.path.exists(path): 68 | raise Exception("Could not find config file at path {}!".format(path)) 69 | with open(path) as config_file: 70 | config = json.load(config_file) 71 | # TODO: Database Authorization (Works only with public databases) 72 | database_url = config["databaseURL"] 73 | database_client = FirebaseClient(database_url) 74 | RealtimeDatabase._shared_database = database_client 75 | 76 | if not RealtimeDatabase._shared_database: 77 | raise Exception("Could not initialize Database!") 78 | 79 | return RealtimeDatabase._shared_database 80 | 81 | def _start_async_call(self, fn, timeout=10): 82 | """ 83 | Manages asynchronous calls to the RealtimeDatabase. 84 | """ 85 | result = {} 86 | result["event"] = threading.Event() 87 | async_thread = threading.Thread(target=fn, args=(result,)) 88 | async_thread.start() 89 | async_thread.join(timeout=timeout) 90 | return result["data"] 91 | 92 | def _get_file_from_remote(self, url): 93 | """ 94 | This function is used to get the information form the source url and returns a string 95 | It also checks if the data is None or == null (firebase return if no data) 96 | """ 97 | try: 98 | file_content = urlopen(url).read() 99 | except Exception as e: 100 | raise Exception("Unable to get file from url {}. Error={}".format(url, str(e))) 101 | 102 | if file_content is not None and file_content != "null": 103 | return file_content 104 | else: 105 | raise Exception("unable to get file from url {}".format(url)) 106 | 107 | # TODO: Can this be configured to be a global callback for all methods where I pass the task and the result? This would simplify the code a lot. 108 | def _task_callback(self, task, result): 109 | task_awaiter = task.GetAwaiter() 110 | task_awaiter.OnCompleted(lambda: result["event"].set()) 111 | result["event"].wait() 112 | result["data"] = True 113 | 114 | def construct_reference(self, parentname): 115 | """ 116 | Constructs a database reference under the specified parent name. 117 | 118 | Parameters 119 | ---------- 120 | parentname : str 121 | The name of the parent under which the reference will be constructed. 122 | 123 | Returns 124 | ------- 125 | :class: 'Firebase.Database.Query.ChildQuery' 126 | The constructed database reference. 127 | 128 | """ 129 | database_reference = RealtimeDatabase._shared_database 130 | reference = database_reference.Child(parentname) 131 | return reference 132 | 133 | def construct_child_refrence(self, parentname, childname): 134 | """ 135 | Constructs a database reference under the specified parent name & child name. 136 | 137 | Parameters 138 | ---------- 139 | parentname : str 140 | The name of the parent under which the reference will be constructed. 141 | childname : str 142 | The name of the child under which the reference will be constructed. 143 | 144 | Returns 145 | ------- 146 | :class: 'Firebase.Database.Query.ChildQuery' 147 | The constructed database reference. 148 | 149 | """ 150 | database_reference = RealtimeDatabase._shared_database 151 | childquery = database_reference.Child(parentname) 152 | child_reference = QueryExtensions.Child(childquery, childname) 153 | return child_reference 154 | 155 | def construct_grandchild_refrence(self, parentname, childname, grandchildname): 156 | """ 157 | Constructs a database reference under the specified parent name, child name, & grandchild name. 158 | 159 | Parameters 160 | ---------- 161 | parentname : str 162 | The name of the parent under which the reference will be constructed. 163 | childname : str 164 | The name of the child under which the reference will be constructed. 165 | grandchildname : str 166 | The name of the grandchild under which the reference will be constructed. 167 | 168 | Returns 169 | ------- 170 | :class: 'Firebase.Database.Query.ChildQuery' 171 | The constructed database reference. 172 | 173 | """ 174 | database_reference = RealtimeDatabase._shared_database 175 | childquery = database_reference.Child(parentname) 176 | child_reference = QueryExtensions.Child(childquery, childname) 177 | grand_child_reference = QueryExtensions.Child(child_reference, grandchildname) 178 | return grand_child_reference 179 | 180 | def construct_reference_from_list(self, reference_list): 181 | """ 182 | Constructs a database reference under the specified refrences in list order. 183 | 184 | Parameters 185 | ---------- 186 | reference_list : list of str 187 | The name of the parent under which the reference will be constructed. 188 | 189 | Returns 190 | ------- 191 | :class: 'Firebase.Database.Query.ChildQuery' 192 | The constructed database reference. 193 | 194 | """ 195 | reference = RealtimeDatabase._shared_database 196 | for ref in reference_list: 197 | if ref == reference_list[0]: 198 | reference = reference.Child(ref) 199 | else: 200 | reference = QueryExtensions.Child(reference, ref) 201 | return reference 202 | 203 | def delete_data_from_reference(self, database_reference): 204 | """ 205 | Method for deleting data from a constructed database reference. 206 | 207 | Parameters 208 | ---------- 209 | database_reference: 'Firebase.Database.Query.ChildQuery' 210 | Reference to the database location where the data will be deleted from. 211 | 212 | Returns 213 | ------- 214 | None 215 | """ 216 | self._ensure_database() 217 | 218 | def _begin_delete(result): 219 | deletetask = database_reference.DeleteAsync() 220 | delete_data = deletetask.GetAwaiter() 221 | delete_data.OnCompleted(lambda: result["event"].set()) 222 | result["event"].wait() 223 | result["data"] = True 224 | 225 | self._start_async_call(_begin_delete) 226 | 227 | def get_data_from_reference(self, database_reference): 228 | """ 229 | Method for retrieving data from a constructed database reference. 230 | 231 | Parameters 232 | ---------- 233 | database_reference: 'Firebase.Database.Query.ChildQuery' 234 | Reference to the database location where the data will be retreived from. 235 | 236 | Returns 237 | ------- 238 | dict 239 | The retrieved data as a dictionary. 240 | 241 | """ 242 | self._ensure_database() 243 | 244 | def _begin_build_url(result): 245 | urlbuldtask = database_reference.BuildUrlAsync() 246 | task_url = urlbuldtask.GetAwaiter() 247 | task_url.OnCompleted(lambda: result["event"].set()) 248 | result["event"].wait() 249 | result["data"] = urlbuldtask.Result 250 | 251 | url = self._start_async_call(_begin_build_url) 252 | json_data = self._get_file_from_remote(url) 253 | 254 | # TODO: json.load(data) vs. json_loads(data) 255 | """ 256 | This is because error will be thrown with json_loads(data)... 257 | Because I cannot gaurentee that all data will be filled (FIREBASE DOES NOT UPLOAD NULL VALUES) 258 | Therefore, unless the data is completely filled json_loads(data) will throw an error. 259 | 260 | """ 261 | data = json.loads(json_data) 262 | return data 263 | 264 | def stream_data_from_reference(self, callback, database_reference): 265 | raise NotImplementedError("Function Under Developement") 266 | 267 | def upload_data_to_reference(self, data, database_reference): 268 | """ 269 | Method for uploading data to a constructed database reference. 270 | 271 | Parameters 272 | ---------- 273 | data : Any 274 | The data to be uploaded. Data should be JSON serializable. 275 | database_reference: 'Firebase.Database.Query.ChildQuery' 276 | Reference to the database location where the data will be uploaded. 277 | 278 | Returns 279 | ------- 280 | None 281 | """ 282 | self._ensure_database() 283 | serialized_data = json_dumps(data) 284 | 285 | def _begin_upload(result): 286 | uploadtask = database_reference.PutAsync(serialized_data) 287 | task_upload = uploadtask.GetAwaiter() 288 | task_upload.OnCompleted(lambda: result["event"].set()) 289 | result["event"].wait() 290 | result["data"] = True 291 | 292 | self._start_async_call(_begin_upload) 293 | -------------------------------------------------------------------------------- /src/compas_xr/realtime_database/realtime_database_interface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | class RealtimeDatabaseInterface(object): 6 | """ 7 | The RealtimeDatabaseInterface class serves as the shared interface for RealtimeDatabase classes that 8 | operate in IronPython and Python 3.0. 9 | 10 | Methods within this class are designed to rely on shared interfaces that are implemented in child classes. 11 | """ 12 | 13 | def construct_reference(self, parentname): 14 | raise NotImplementedError("Implemented on child classes") 15 | 16 | def construct_child_refrence(self, parentname, childname): 17 | raise NotImplementedError("Implemented on child classes") 18 | 19 | def construct_grandchild_refrence(self, parentname, childname, grandchildname): 20 | raise NotImplementedError("Implemented on child classes") 21 | 22 | def construct_reference_from_list(self, reference_list): 23 | raise NotImplementedError("Implemented on child classes") 24 | 25 | def upload_data_to_reference(self, data, database_reference): 26 | raise NotImplementedError("Implemented on child classes") 27 | 28 | def get_data_from_reference(self, database_reference): 29 | raise NotImplementedError("Implemented on child classes") 30 | 31 | def delete_data_from_reference(self, database_reference): 32 | raise NotImplementedError("Implemented on child classes") 33 | 34 | def stream_data_from_reference(self, callback, database_reference): 35 | raise NotImplementedError("Implemented on child classes") 36 | 37 | def upload_data(self, data, reference_name): 38 | """ 39 | Uploads data to the Firebase Realtime Database under specified reference name. 40 | 41 | Parameters 42 | ---------- 43 | data : Any 44 | The data to be uploaded, needs to be JSON serializable. 45 | reference_name : str 46 | The name of the reference under which the data will be stored. 47 | 48 | Returns 49 | ------- 50 | None 51 | 52 | """ 53 | database_reference = self.construct_reference(reference_name) 54 | self.upload_data_to_reference(data, database_reference) 55 | 56 | def upload_data_to_reference_as_child(self, data, reference_name, child_name): 57 | """ 58 | Uploads data to the Firebase Realtime Database under specified reference name & child name. 59 | 60 | Parameters 61 | ---------- 62 | data : Any 63 | The data to be uploaded, needs to be JSON serializable. 64 | reference_name : str 65 | The name of the reference under which the child should exist. 66 | child_name : str 67 | The name of the reference under which the data will be stored. 68 | 69 | Returns 70 | ------- 71 | None 72 | 73 | """ 74 | database_reference = self.construct_child_refrence(reference_name, child_name) 75 | self.upload_data_to_reference(data, database_reference) 76 | 77 | def upload_data_to_deep_reference(self, data, reference_list): 78 | """ 79 | Uploads data to the Firebase Realtime Database under specified reference names in list order. 80 | 81 | Parameters 82 | ---------- 83 | data : Any 84 | The data to be uploaded, needs to be JSON serializable. 85 | reference_list : list of str 86 | The names in sequence order in which the data should be nested for upload. 87 | 88 | Returns 89 | ------- 90 | None 91 | 92 | """ 93 | database_reference = self.construct_reference_from_list(reference_list) 94 | self.upload_data_to_reference(data, database_reference) 95 | 96 | def upload_data_from_file(self, path_local, refernce_name): 97 | """ 98 | Uploads data to the Firebase Realtime Database under specified reference name from a file. 99 | 100 | Parameters 101 | ---------- 102 | path_local : str 103 | The local path in which the data is stored as a json file. 104 | reference_name : str 105 | The name of the reference under which the data will be stored. 106 | 107 | Returns 108 | ------- 109 | None 110 | 111 | """ 112 | if not os.path.exists(path_local): 113 | raise Exception("path does not exist {}".format(path_local)) 114 | with open(path_local) as config_file: 115 | data = json.load(config_file) 116 | database_reference = self.construct_reference(refernce_name) 117 | self.upload_data_to_reference(data, database_reference) 118 | 119 | def get_data(self, reference_name): 120 | """ 121 | Retrieves data from the Firebase Realtime Database under the specified reference name. 122 | 123 | Parameters 124 | ---------- 125 | reference_name : str 126 | The name of the reference under which the data is stored. 127 | 128 | Returns 129 | ------- 130 | data : dict 131 | The retrieved data in dictionary format. 132 | 133 | """ 134 | database_reference = self.construct_reference(reference_name) 135 | return self.get_data_from_reference(database_reference) 136 | 137 | def get_data_from_child_reference(self, reference_name, child_name): 138 | """ 139 | Retreives data from the Firebase Realtime Database under specified reference name & child name. 140 | 141 | Parameters 142 | ---------- 143 | reference_name : str 144 | The name of the reference under which the child exists. 145 | child_name : str 146 | The name of the reference under which the data is stored. 147 | 148 | Returns 149 | ------- 150 | data : dict 151 | The retrieved data in dictionary format. 152 | 153 | """ 154 | database_reference = self.construct_child_refrence(reference_name, child_name) 155 | return self.get_data_from_reference(database_reference) 156 | 157 | def get_data_from_deep_reference(self, reference_list): 158 | """ 159 | Retreives data from the Firebase Realtime Database under specified reference names in list order. 160 | 161 | Parameters 162 | ---------- 163 | data : Any 164 | The data to be uploaded, needs to be JSON serializable. 165 | reference_list : list of str 166 | The names in sequence order in which the is nested. 167 | 168 | Returns 169 | ------- 170 | data : dict 171 | The retrieved data in dictionary format. 172 | """ 173 | database_reference = self.construct_reference_from_list(reference_list) 174 | return self.get_data_from_reference(database_reference) 175 | 176 | def delete_data(self, reference_name): 177 | """ 178 | Deletes data from the Firebase Realtime Database under specified reference name. 179 | 180 | Parameters 181 | ---------- 182 | reference_name : str 183 | The name of the reference under which the child should exist. 184 | 185 | Returns 186 | ------- 187 | None 188 | 189 | """ 190 | database_reference = self.construct_reference(reference_name) 191 | self.delete_data_from_reference(database_reference) 192 | 193 | def delete_data_from_child_reference(self, reference_name, child_name): 194 | """ 195 | Deletes data from the Firebase Realtime Database under specified reference name & child name. 196 | 197 | Parameters 198 | ---------- 199 | reference_name : str 200 | The name of the reference under which the child should exist. 201 | child_name : str 202 | The name of the reference under which the data will be stored. 203 | 204 | Returns 205 | ------- 206 | None 207 | 208 | """ 209 | database_reference = self.construct_child_refrence(reference_name, child_name) 210 | self.delete_data_from_reference(database_reference) 211 | 212 | def delete_data_from_deep_reference(self, reference_list): 213 | """ 214 | Deletes data from the Firebase Realtime Database under specified reference names in list order. 215 | 216 | Parameters 217 | ---------- 218 | reference_list : list of str 219 | The names in sequence order in which the data should be nested for upload. 220 | 221 | Returns 222 | ------- 223 | None 224 | 225 | """ 226 | database_reference = self.construct_reference_from_list(reference_list) 227 | self.delete_data_from_reference(database_reference) 228 | -------------------------------------------------------------------------------- /src/compas_xr/realtime_database/realtime_database_pyrebase.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import json 6 | import os 7 | 8 | import pyrebase 9 | from compas.data import json_dumps 10 | 11 | from compas_xr.realtime_database.realtime_database_interface import RealtimeDatabaseInterface 12 | 13 | 14 | class RealtimeDatabase(RealtimeDatabaseInterface): 15 | """ 16 | A RealtimeDatabase is defined by a Firebase configuration path and a shared database reference. 17 | 18 | The RealtimeDatabase class is responsible for initializing and managing the connection to a Firebase Realtime Database. 19 | It ensures that the database connection is established only once and shared across all instances of the class. 20 | 21 | Parameters 22 | ---------- 23 | config_path : str 24 | The path to the Firebase configuration JSON file. 25 | 26 | Attributes 27 | ---------- 28 | config_path : str 29 | The path to the Firebase configuration JSON file. 30 | database : Database 31 | The Database instance representing the connection to the Firebase Realtime Database. 32 | _shared_database : Database, class attribute 33 | The shared Database instance representing the connection to the Firebase Realtime Database. 34 | """ 35 | 36 | _shared_database = None 37 | 38 | def __init__(self, config_path): 39 | self.config_path = config_path 40 | self._ensure_database() 41 | 42 | def _ensure_database(self): 43 | """ 44 | Ensures that the database connection is established. 45 | If the connection is not yet established, it initializes it. 46 | If the connection is already established, it returns the existing connection. 47 | """ 48 | if not RealtimeDatabase._shared_database: 49 | path = self.config_path 50 | 51 | if not os.path.exists(path): 52 | raise Exception("Could not find config file at path {}!".format(path)) 53 | with open(path) as config_file: 54 | config = json.load(config_file) 55 | # TODO: Database Authorization (Works only with public databases) 56 | firebase = pyrebase.initialize_app(config) 57 | RealtimeDatabase._shared_database = firebase.database() 58 | 59 | if not RealtimeDatabase._shared_database: 60 | raise Exception("Could not initialize database!") 61 | 62 | def construct_reference(self, parentname): 63 | """ 64 | Constructs a database reference under the specified parent name. 65 | 66 | Parameters 67 | ---------- 68 | parentname : str 69 | The name of the parent under which the reference will be constructed. 70 | 71 | Returns 72 | ------- 73 | :class: 'pyrebase.pyrebase.Database' 74 | The constructed database reference. 75 | 76 | """ 77 | return RealtimeDatabase._shared_database.child(parentname) 78 | 79 | def construct_child_refrence(self, parentname, childname): 80 | """ 81 | Constructs a database reference under the specified parent name & child name. 82 | 83 | Parameters 84 | ---------- 85 | parentname : str 86 | The name of the parent under which the reference will be constructed. 87 | childname : str 88 | The name of the child under which the reference will be constructed. 89 | 90 | Returns 91 | ------- 92 | :class: 'pyrebase.pyrebase.Database' 93 | The constructed database reference. 94 | 95 | """ 96 | return RealtimeDatabase._shared_database.child(parentname).child(childname) 97 | 98 | def construct_grandchild_refrence(self, parentname, childname, grandchildname): 99 | """ 100 | Constructs a database reference under the specified parent name, child name, & grandchild name. 101 | 102 | Parameters 103 | ---------- 104 | parentname : str 105 | The name of the parent under which the reference will be constructed. 106 | childname : str 107 | The name of the child under which the reference will be constructed. 108 | grandchildname : str 109 | The name of the grandchild under which the reference will be constructed. 110 | 111 | Returns 112 | ------- 113 | :class: 'pyrebase.pyrebase.Database' 114 | The constructed database reference. 115 | 116 | """ 117 | return RealtimeDatabase._shared_database.child(parentname).child(childname).child(grandchildname) 118 | 119 | def construct_reference_from_list(self, reference_list): 120 | """ 121 | Constructs a database reference under the specified refrences in list order. 122 | 123 | Parameters 124 | ---------- 125 | reference_list : list of str 126 | The name of the parent under which the reference will be constructed. 127 | 128 | Returns 129 | ------- 130 | :class: 'pyrebase.pyrebase.Database' 131 | The constructed database reference. 132 | 133 | """ 134 | reference = RealtimeDatabase._shared_database 135 | for ref in reference_list: 136 | reference = reference.child(ref) 137 | return reference 138 | 139 | def delete_data_from_reference(self, database_reference): 140 | """ 141 | Method for deleting data from a constructed database reference. 142 | 143 | Parameters 144 | ---------- 145 | database_reference: 'pyrebase.pyrebase.Database' 146 | Reference to the database location where the data will be deleted from. 147 | 148 | Returns 149 | ------- 150 | None 151 | """ 152 | self._ensure_database() 153 | database_reference.remove() 154 | 155 | def get_data_from_reference(self, database_reference): 156 | """ 157 | Method for retrieving data from a constructed database reference. 158 | 159 | Parameters 160 | ---------- 161 | database_reference: 'pyrebase.pyrebase.Database' 162 | Reference to the database location where the data will be retreived from. 163 | 164 | Returns 165 | ------- 166 | dict 167 | The retrieved data as a dictionary. 168 | 169 | """ 170 | self._ensure_database() 171 | database_directory = database_reference.get() 172 | data = database_directory.val() 173 | data_dict = dict(data) 174 | return data_dict 175 | 176 | def stream_data_from_reference(self, callback, database_reference): 177 | raise NotImplementedError("Function Under Developement") 178 | 179 | def upload_data_to_reference(self, data, database_reference): 180 | """ 181 | Method for uploading data to a constructed database reference. 182 | 183 | Parameters 184 | ---------- 185 | data : Any 186 | The data to be uploaded. Data should be JSON serializable. 187 | database_reference: 'pyrebase.pyrebase.Database' 188 | Reference to the database location where the data will be uploaded. 189 | 190 | Returns 191 | ------- 192 | None 193 | """ 194 | self._ensure_database() 195 | # TODO: Check if this is stupid... it provides the functionality of making it work with compas objects and consistency across both child classes 196 | json_string = json_dumps(data) 197 | database_reference.set(json.loads(json_string)) 198 | -------------------------------------------------------------------------------- /src/compas_xr/rhino/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_xr/15614411d49009452d2baf3d45c176b2ea0dddf4/src/compas_xr/rhino/__init__.py -------------------------------------------------------------------------------- /src/compas_xr/rhino/install.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import glob 6 | import os 7 | 8 | import compas 9 | import compas.plugins 10 | from compas_ghpython.components import install_userobjects 11 | from compas_ghpython.components import uninstall_userobjects 12 | 13 | 14 | @compas.plugins.plugin(category="install") 15 | def installable_rhino_packages(): 16 | return ["compas_xr"] 17 | 18 | 19 | @compas.plugins.plugin(category="install") 20 | def after_rhino_install(installed_packages): 21 | project = "compas_xr" 22 | if project not in installed_packages: 23 | return [] 24 | 25 | srcdir = os.path.join(os.path.dirname(__file__), "..", "ghpython", "components", "ghuser") 26 | installed_objects = install_userobjects(srcdir) 27 | msg = "Installed {} GH User Objects".format(len(installed_objects)) 28 | 29 | return [(project, msg, True)] 30 | 31 | 32 | @compas.plugins.plugin(category="install") 33 | def after_rhino_uninstall(uninstalled_packages): 34 | project = "compas_xr" 35 | if project not in uninstalled_packages: 36 | return [] 37 | 38 | srcdir = os.path.join(os.path.dirname(__file__), "..", "ghpython", "components", "ghuser") 39 | userobjects = [os.path.basename(ghuser) for ghuser in glob.glob(os.path.join(srcdir, "*.ghuser"))] 40 | uninstalled_objects = uninstall_userobjects(userobjects) 41 | 42 | uninstall_errors = [uo[0] for uo in uninstalled_objects if not uo[1]] 43 | error_msg = "" if not uninstall_errors else "and {} failed to uninstall".format(len(uninstall_errors)) 44 | msg = "Uninstalled {} GH User Objects {}".format(len(uninstalled_objects), error_msg) 45 | 46 | return [(project, msg, True)] 47 | -------------------------------------------------------------------------------- /src/compas_xr/storage/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_xr.storage 4 | ******************************************************************************** 5 | 6 | This package contains classes for data storage using Firebase. 7 | 8 | .. currentmodule:: compas_xr.storage 9 | 10 | Classes 11 | ------- 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | :nosignatures: 16 | 17 | Storage 18 | 19 | """ 20 | 21 | import sys 22 | 23 | if sys.platform == "cli": 24 | from compas_xr.storage.storage_cli import Storage 25 | else: 26 | from compas_xr.storage.storage_pyrebase import Storage 27 | 28 | __all__ = ["Storage"] 29 | -------------------------------------------------------------------------------- /src/compas_xr/storage/storage_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import threading 5 | 6 | import clr 7 | from compas.data import json_dumps 8 | from compas.data import json_loads 9 | from System.IO import File 10 | from System.IO import MemoryStream 11 | from System.Text import Encoding 12 | 13 | from compas_xr.storage.storage_interface import StorageInterface 14 | 15 | try: 16 | from urllib.request import urlopen 17 | except ImportError: 18 | from urllib import urlopen 19 | 20 | lib_dir = os.path.join(os.path.dirname(__file__), "..", "dependencies") 21 | if lib_dir not in sys.path: 22 | sys.path.append(lib_dir) 23 | 24 | clr.AddReference("Firebase.Auth.dll") 25 | clr.AddReference("Firebase.dll") 26 | clr.AddReference("Firebase.Storage.dll") 27 | 28 | from Firebase.Storage import FirebaseStorage # noqa: E402 29 | 30 | 31 | class Storage(StorageInterface): 32 | """ 33 | A Storage Class is defined by a Firebase configuration path and a shared storage reference. 34 | 35 | The Storage class is responsible for initializing and managing the connection to a Firebase Storage. 36 | It ensures that the storage connection is established only once and shared across all instances of the class. 37 | 38 | Parameters 39 | ---------- 40 | config_path : str 41 | The path to the Firebase configuration JSON file. 42 | 43 | Attributes 44 | ---------- 45 | config_path : str 46 | The path to the Firebase configuration JSON file. 47 | _shared_storage : pyrebase.Storage, class attribute 48 | The shared pyrebase.Storage instance representing the connection to the Firebase Storage. 49 | """ 50 | 51 | _shared_storage = None 52 | 53 | def __init__(self, config_path): 54 | self.config_path = config_path 55 | self.storage = self._ensure_storage() 56 | 57 | def _ensure_storage(self): 58 | """ 59 | Ensures that the storage connection is established. 60 | If the connection is not yet established, it initializes it. 61 | If the connection is already established, it returns the existing connection. 62 | """ 63 | if not Storage._shared_storage: 64 | path = self.config_path 65 | if not os.path.exists(path): 66 | raise Exception("Path Does to config Not Exist: {}".format(path)) 67 | with open(path) as config_file: 68 | config = json.load(config_file) 69 | # TODO: Authorization for storage security (Works for now for us because our Storage is public) 70 | storage_client = FirebaseStorage(config["storageBucket"]) 71 | Storage._shared_storage = storage_client 72 | 73 | if not Storage._shared_storage: 74 | raise Exception("Could not initialize storage!") 75 | 76 | return Storage._shared_storage 77 | 78 | def _start_async_call(self, fn, timeout=10): 79 | """ 80 | Manages asynchronous calls to the Storage. 81 | """ 82 | result = {} 83 | result["event"] = threading.Event() 84 | async_thread = threading.Thread(target=fn, args=(result,)) 85 | async_thread.start() 86 | async_thread.join(timeout=timeout) 87 | return result["data"] 88 | 89 | def _get_file_from_remote(self, url): 90 | """ 91 | This function is used to get the information form the source url and returns a string 92 | It also checks if the data is None or == null (firebase return if no data) 93 | """ 94 | try: 95 | file_content = urlopen(url).read() 96 | except Exception as e: 97 | raise Exception("Unable to get file from url {}. Error={}".format(url, str(e))) 98 | 99 | if file_content is not None and file_content != "null": 100 | return file_content 101 | 102 | else: 103 | raise Exception("unable to get file from url {}".format(url)) 104 | 105 | # TODO: Same as RTDB: Can I turn this into a gloabal call back that can be used inside of every method? 106 | def _task_callback(task, result): 107 | task_awaiter = task.GetAwaiter() 108 | task_awaiter.OnCompleted(lambda: result["event"].set()) 109 | result["event"].wait() 110 | result["data"] = True 111 | 112 | def construct_reference(self, cloud_file_name): 113 | """ 114 | Constructs a storage reference for the specified cloud file name. 115 | 116 | Parameters 117 | ---------- 118 | cloud_file_name : str 119 | The name of the cloud file. 120 | 121 | Returns 122 | ------- 123 | :class: 'Firebase.Storage.FirebaseStorageReference' 124 | The constructed storage reference. 125 | 126 | """ 127 | return Storage._shared_storage.Child(cloud_file_name) 128 | 129 | def construct_reference_with_folder(self, cloud_folder_name, cloud_file_name): 130 | """ 131 | Constructs a storage reference for the specified cloud folder name and file name. 132 | 133 | Parameters 134 | ---------- 135 | cloud_folder_name : str 136 | The name of the cloud folder. 137 | cloud_file_name : str 138 | The name of the cloud file. 139 | 140 | Returns 141 | ------- 142 | :class: 'Firebase.Storage.FirebaseStorageReference' 143 | The constructed storage reference. 144 | 145 | """ 146 | return Storage._shared_storage.Child(cloud_folder_name).Child(cloud_file_name) 147 | 148 | def construct_reference_from_list(self, cloud_path_list): 149 | """ 150 | Constructs a storage reference for consecutive cloud folders in list order. 151 | 152 | Parameters 153 | ---------- 154 | cloud_path_list : list of str 155 | The list of cloud path names. 156 | 157 | Returns 158 | ------- 159 | :class: 'Firebase.Storage.FirebaseStorageReference' 160 | The constructed storage reference. 161 | 162 | """ 163 | storage_ref = Storage._shared_storage 164 | for path in cloud_path_list: 165 | storage_ref = storage_ref.Child(path) 166 | return storage_ref 167 | 168 | def get_data_from_reference(self, storage_refrence): 169 | """ 170 | Retrieves data from the specified storage reference. 171 | 172 | Parameters 173 | ---------- 174 | storage_reference : Firebase.Storage.FirebaseStorageReference 175 | The storage reference pointing to the desired data. 176 | 177 | Returns 178 | ------- 179 | data : dict or Compas Class Object 180 | The deserialized data retrieved from the storage reference. 181 | 182 | """ 183 | self._ensure_storage() 184 | 185 | def _begin_download(result): 186 | downloadurl_task = storage_refrence.GetDownloadUrlAsync() 187 | task_download = downloadurl_task.GetAwaiter() 188 | task_download.OnCompleted(lambda: result["event"].set()) 189 | result["event"].wait() 190 | result["data"] = downloadurl_task.Result 191 | 192 | url = self._start_async_call(_begin_download) 193 | data = self._get_file_from_remote(url) 194 | desearialized_data = json_loads(data) 195 | return desearialized_data 196 | 197 | def upload_bytes_to_reference_from_local_file(self, file_path, storage_reference): 198 | """ 199 | Uploads data from bytes to the specified storage reference from a local file. 200 | 201 | Parameters 202 | ---------- 203 | file_path : str 204 | The path to the local file. 205 | storage_reference : Firebase.Storage.FirebaseStorageReference 206 | The storage reference to upload the byte data to. 207 | 208 | Returns 209 | ------ 210 | None 211 | 212 | """ 213 | if not os.path.exists(file_path): 214 | raise FileNotFoundError("File not found: {}".format(file_path)) 215 | self._ensure_storage() 216 | byte_data = File.ReadAllBytes(file_path) 217 | stream = MemoryStream(byte_data) 218 | 219 | def _begin_upload(result): 220 | uploadtask = storage_reference.PutAsync(stream) 221 | task_upload = uploadtask.GetAwaiter() 222 | task_upload.OnCompleted(lambda: result["event"].set()) 223 | result["event"].wait() 224 | result["data"] = True 225 | 226 | self._start_async_call(_begin_upload) 227 | 228 | def upload_data_to_reference(self, data, storage_reference, pretty=True): 229 | """ 230 | Uploads data to the specified storage reference. 231 | 232 | Parameters 233 | ---------- 234 | data : Any should be json serializable 235 | The data to be uploaded. 236 | storage_reference : Firebase.Storage.FirebaseStorageReference 237 | The storage reference to upload the data to. 238 | pretty : bool, optional 239 | Whether to format the JSON data with indentation and line breaks (default is True). 240 | 241 | Returns 242 | ------ 243 | None 244 | 245 | """ 246 | self._ensure_storage() 247 | serialized_data = json_dumps(data, pretty=pretty) 248 | byte_data = Encoding.UTF8.GetBytes(serialized_data) 249 | stream = MemoryStream(byte_data) 250 | 251 | def _begin_upload(result): 252 | uploadtask = storage_reference.PutAsync(stream) 253 | task_upload = uploadtask.GetAwaiter() 254 | task_upload.OnCompleted(lambda: result["event"].set()) 255 | result["event"].wait() 256 | result["data"] = True 257 | 258 | self._start_async_call(_begin_upload) 259 | -------------------------------------------------------------------------------- /src/compas_xr/storage/storage_interface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from copy import deepcopy 4 | 5 | from compas.data import json_dump 6 | 7 | 8 | class StorageInterface(object): 9 | """ 10 | The StorageInterface class serves as the shared interface for Storage classes that 11 | operate in IronPython and Python 3.0. 12 | 13 | Methods within this class are designed to rely on shared interfaces that are implemented in child classes. 14 | """ 15 | 16 | def construct_reference(self, cloud_file_name): 17 | raise NotImplementedError("Implemented on child classes") 18 | 19 | def construct_reference_with_folder(self, cloud_folder_name, cloud_file_name): 20 | raise NotImplementedError("Implemented on child classes") 21 | 22 | def construct_reference_from_list(self, cloud_path_list): 23 | raise NotImplementedError("Implemented on child classes") 24 | 25 | def upload_data_to_reference(self, data, storage_reference, pretty=True): 26 | raise NotImplementedError("Implemented on child classes") 27 | 28 | def get_data_from_reference(self, storage_refrence): 29 | raise NotImplementedError("Implemented on child classes") 30 | 31 | def upload_bytes_to_reference_from_local_file(self, file_path, storage_reference): 32 | raise NotImplementedError("Implemented on child classes") 33 | 34 | def upload_data(self, data, cloud_file_name, pretty=True): 35 | """ 36 | Uploads data to the Firebase Storage under specified cloud file name. 37 | 38 | Parameters 39 | ---------- 40 | data : Any 41 | The data to be uploaded, needs to be JSON serializable. 42 | cloud_file_name : str 43 | The name of the reference under which the data will be stored file type should be specified.(ex: .json) 44 | pretty : bool, optional 45 | A boolean that determines if the data should be formatted for readability. Default is True. 46 | 47 | Returns 48 | ------- 49 | None 50 | 51 | """ 52 | storage_reference = self.construct_reference(cloud_file_name) 53 | self.upload_data_to_reference(data, storage_reference, pretty) 54 | 55 | def upload_data_from_json(self, path_local, pretty=True): 56 | """ 57 | Uploads data to the Firebase Storage from JSON file. 58 | 59 | Parameters 60 | ---------- 61 | path_local : str (path) 62 | The local path at which the JSON file is stored. 63 | pretty : bool, optional 64 | A boolean that determines if the data should be formatted for readability. Default is True. 65 | 66 | Returns 67 | ------- 68 | None 69 | 70 | """ 71 | if not os.path.exists(path_local): 72 | raise Exception("path does not exist {}".format(path_local)) 73 | with open(path_local) as file: 74 | data = json.load(file) 75 | cloud_file_name = os.path.basename(path_local) 76 | storage_reference = self.construct_reference(cloud_file_name) 77 | self.upload_data_to_reference(data, storage_reference, pretty) 78 | 79 | def upload_data_to_folder(self, data, cloud_folder_name, cloud_file_name, pretty=True): 80 | """ 81 | Uploads data to the Firebase Storage under specified cloud folder name in cloud file name. 82 | 83 | Parameters 84 | ---------- 85 | data : Any 86 | The data to be uploaded, needs to be JSON serializable. 87 | cloud_folder_name : str 88 | The name of the folder under which the data will be stored. 89 | cloud_file_name : str 90 | The name of the reference under which the data will be stored file type should be specified.(ex: .json) 91 | pretty : bool, optional 92 | A boolean that determines if the data should be formatted for readability. Default is True. 93 | 94 | Returns 95 | ------- 96 | None 97 | 98 | """ 99 | storage_reference = self.construct_reference_with_folder(cloud_folder_name, cloud_file_name) 100 | self.upload_data_to_reference(data, storage_reference, pretty) 101 | 102 | def upload_data_to_deep_reference(self, data, cloud_path_list, pretty=True): 103 | """ 104 | Uploads data to the Firebase Storage under specified reference names in list order. 105 | 106 | Parameters 107 | ---------- 108 | data : Any 109 | The data to be uploaded, needs to be JSON serializable. 110 | cloud_path_list : list of str 111 | The list of reference names under which the data will be stored file type should be specified.(ex: .json) 112 | pretty : bool, optional 113 | A boolean that determines if the data should be formatted for readability. Default is True. 114 | 115 | Returns 116 | ------- 117 | None 118 | 119 | """ 120 | storage_reference = self.construct_reference_from_list(cloud_path_list) 121 | self.upload_data_to_reference(data, storage_reference, pretty) 122 | 123 | def upload_file_as_bytes(self, file_path): 124 | """ 125 | Uploads a file as bytes to the Firebase Storage. 126 | 127 | Parameters 128 | ---------- 129 | file_path : str 130 | The local path of the file to be uploaded. 131 | 132 | Returns 133 | ------- 134 | None 135 | 136 | """ 137 | if not os.path.exists(file_path): 138 | raise FileNotFoundError("File not found: {}".format(file_path)) 139 | file_name = os.path.basename(file_path) 140 | storage_reference = self.construct_reference(file_name) 141 | self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) 142 | 143 | def upload_file_as_bytes_to_deep_reference(self, file_path, cloud_path_list): 144 | """ 145 | Uploads a file as bytes to the Firebase Storage to specified cloud path. 146 | 147 | Parameters 148 | ---------- 149 | file_path : str 150 | The local path of the file to be uploaded. 151 | cloud_path_list : list of str 152 | The list of reference names under which the file will be stored. 153 | 154 | Returns 155 | ------- 156 | None 157 | 158 | """ 159 | if not os.path.exists(file_path): 160 | raise FileNotFoundError("File not found: {}".format(file_path)) 161 | file_name = os.path.basename(file_path) 162 | new_path_list = deepcopy(cloud_path_list) 163 | new_path_list.append(file_name) 164 | 165 | storage_reference = self.construct_reference_from_list(new_path_list) 166 | self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) 167 | 168 | def upload_files_as_bytes_from_directory_to_deep_reference(self, directory_path, cloud_path_list): 169 | """ 170 | Uploads all files in specified directory as bytes to the Firebase Storage at specified cloud path in list order. 171 | 172 | Parameters 173 | ---------- 174 | directory_path : str 175 | The local path of the directory in which files are stored. 176 | cloud_path_list : list of str 177 | The list of reference names under which the file will be stored. 178 | 179 | Returns 180 | ------- 181 | None 182 | 183 | """ 184 | if not os.path.exists(directory_path) or not os.path.isdir(directory_path): 185 | raise FileNotFoundError("Directory not found: {}".format(directory_path)) 186 | for file_name in os.listdir(directory_path): 187 | file_path = os.path.join(directory_path, file_name) 188 | self.upload_file_as_bytes_to_deep_reference(file_path, cloud_path_list) 189 | 190 | # TODO: This works as it should, but I have a lot of problems with json_loads 191 | def get_data(self, cloud_file_name): 192 | """ 193 | Retrieves data from the Firebase Storage for specified cloud file name. 194 | 195 | Parameters 196 | ---------- 197 | cloud_file_name : str 198 | The name of the cloud file. 199 | 200 | Returns 201 | ------- 202 | data : dict or Compas Class Object 203 | The retrieved data in dictionary format or as Compas Class Object. 204 | 205 | """ 206 | storage_reference = self.construct_reference(cloud_file_name) 207 | return self.get_data_from_reference(storage_reference) 208 | 209 | # TODO: This is not working... for some reason the GetDownloadUrlAsync always results Faulted 210 | def get_data_from_folder(self, cloud_folder_name, cloud_file_name): 211 | """ 212 | Retrieves data from the Firebase Storage for specified cloud folder name and cloud file name. 213 | 214 | Parameters 215 | ---------- 216 | cloud_folder_name : str 217 | The name of the cloud folder. 218 | cloud_file_name : str 219 | The name of the cloud file. 220 | 221 | Returns 222 | ------- 223 | data : dict or Compas Class Object 224 | The retrieved data in dictionary format or as Compas Class Object. 225 | 226 | """ 227 | storage_reference = self.construct_reference_with_folder(cloud_folder_name, cloud_file_name) 228 | return self.get_data_from_reference(storage_reference) 229 | 230 | # TODO: This is not working... for some reason the GetDownloadUrlAsync always results Faulted 231 | def get_data_from_deep_reference(self, cloud_path_list): 232 | """ 233 | Retrieves data from the Firebase Storage for specified cloud folder name and cloud file name. 234 | 235 | Parameters 236 | ---------- 237 | cloud_folder_name : str 238 | The name of the cloud folder. 239 | cloud_file_name : str 240 | The name of the cloud file. 241 | 242 | Returns 243 | ------- 244 | data : dict or Compas Class Object 245 | The retrieved data in dictionary format or as Compas Class Object. 246 | 247 | """ 248 | storage_reference = self.construct_reference_from_list(cloud_path_list) 249 | return self.get_data_from_reference(storage_reference) 250 | 251 | # TODO: This worked with Frame.__data__ but not with TimberAssembly.__data__ 252 | def download_data_to_json(self, cloud_file_name, path_local, pretty=True): 253 | """ 254 | Downloads data from the Firebase Storage for specified cloud file name. 255 | 256 | Parameters 257 | ---------- 258 | cloud_file_name : str 259 | The name of the cloud file. 260 | path_local : str (path) 261 | The local path at which the JSON file will be stored. 262 | pretty : bool, optional 263 | A boolean that determines if the data should be formatted for readability. Default is True. 264 | 265 | Returns 266 | ------- 267 | None 268 | 269 | """ 270 | data = self.get_data(cloud_file_name) 271 | directory_name = os.path.dirname(path_local) 272 | if not os.path.exists(directory_name): 273 | raise FileNotFoundError("Directory {} does not exist!".format(directory_name)) 274 | json_dump(data=data, fp=path_local, pretty=pretty) 275 | -------------------------------------------------------------------------------- /src/compas_xr/storage/storage_pyrebase.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import os 4 | 5 | import pyrebase 6 | from compas.data import json_dumps 7 | from compas.data import json_loads 8 | 9 | from compas_xr.storage.storage_interface import StorageInterface 10 | 11 | try: 12 | # from urllib.request import urlopen 13 | from urllib.request import urlopen 14 | except ImportError: 15 | from urllib import urlopen 16 | 17 | 18 | class Storage(StorageInterface): 19 | """ 20 | A Storage is defined by a Firebase configuration path and a shared storage reference. 21 | 22 | The Storage class is responsible for initializing and managing the connection to a Firebase Storage. 23 | It ensures that the storage connection is established only once and shared across all instances of the class. 24 | 25 | Parameters 26 | ---------- 27 | config_path : str 28 | The path to the Firebase configuration JSON file. 29 | 30 | Attributes 31 | ---------- 32 | config_path : str 33 | The path to the Firebase configuration JSON file. 34 | _shared_storage : pyrebase.Storage, class attribute 35 | The shared pyrebase.Storage instance representing the connection to the Firebase Storage. 36 | """ 37 | 38 | _shared_storage = None 39 | 40 | def __init__(self, config_path): 41 | self.config_path = config_path 42 | self._ensure_storage() 43 | 44 | def _ensure_storage(self): 45 | """ 46 | Ensures that the storage connection is established. 47 | If the connection is not yet established, it initializes it. 48 | If the connection is already established, it returns the existing connection. 49 | """ 50 | if not Storage._shared_storage: 51 | path = self.config_path 52 | if not os.path.exists(path): 53 | raise Exception("Path Does Not Exist: {}".format(path)) 54 | with open(path) as config_file: 55 | config = json.load(config_file) 56 | # TODO: Authorization for storage security (Works for now for us because our Storage is public) 57 | firebase = pyrebase.initialize_app(config) 58 | Storage._shared_storage = firebase.storage() 59 | 60 | if not Storage._shared_storage: 61 | raise Exception("Could not initialize storage!") 62 | 63 | def _get_file_from_remote(self, url): 64 | """ 65 | This function is used to get the information form the source url and returns a string 66 | It also checks if the data is None or == null (firebase return if no data) 67 | """ 68 | try: 69 | file_content = urlopen(url).read().decode() 70 | except Exception as e: 71 | raise Exception("Unable to get file from url {}. Error={}".format(url, str(e))) 72 | 73 | if file_content is not None and file_content != "null": 74 | return file_content 75 | 76 | else: 77 | raise Exception("unable to get file from url {}".format(url)) 78 | 79 | def construct_reference(self, cloud_file_name): 80 | """ 81 | Constructs a storage reference for the specified cloud file name. 82 | 83 | Parameters 84 | ---------- 85 | cloud_file_name : str 86 | The name of the cloud file. 87 | 88 | Returns 89 | ------- 90 | :class: 'pyrebase.pyrebase.Storage' 91 | The constructed storage reference. 92 | 93 | """ 94 | return Storage._shared_storage.child(cloud_file_name) 95 | 96 | def construct_reference_with_folder(self, cloud_folder_name, cloud_file_name): 97 | """ 98 | Constructs a storage reference for the specified cloud folder name and file name. 99 | 100 | Parameters 101 | ---------- 102 | cloud_folder_name : str 103 | The name of the cloud folder. 104 | cloud_file_name : str 105 | The name of the cloud file. 106 | 107 | Returns 108 | ------- 109 | :class: 'pyrebase.pyrebase.Storage' 110 | The constructed storage reference. 111 | 112 | """ 113 | return Storage._shared_storage.child(cloud_folder_name).child(cloud_file_name) 114 | 115 | def construct_reference_from_list(self, cloud_path_list): 116 | """ 117 | Constructs a storage reference for consecutive cloud folders in list order. 118 | 119 | Parameters 120 | ---------- 121 | cloud_path_list : list of str 122 | The list of cloud path names. 123 | 124 | Returns 125 | ------- 126 | :class: 'pyrebase.pyrebase.Storage' 127 | The constructed storage reference. 128 | 129 | """ 130 | storage_reference = Storage._shared_storage 131 | for path in cloud_path_list: 132 | storage_reference = storage_reference.child(path) 133 | return storage_reference 134 | 135 | def get_data_from_reference(self, storage_reference): 136 | """ 137 | Retrieves data from the specified storage reference. 138 | 139 | Parameters 140 | ---------- 141 | storage_reference : pyrebase.pyrebase.Storage 142 | The storage reference pointing to the desired data. 143 | 144 | Returns 145 | ------- 146 | data : dict or Compas Class Object 147 | The deserialized data retrieved from the storage reference. 148 | 149 | """ 150 | url = storage_reference.get_url(token=None) 151 | data = self._get_file_from_remote(url) 152 | deserialized_data = json_loads(data) 153 | return deserialized_data 154 | 155 | def upload_bytes_to_reference_from_local_file(self, file_path, storage_reference): 156 | """ 157 | Uploads data from bytes to the specified storage reference from a local file. 158 | 159 | Parameters 160 | ---------- 161 | file_path : str 162 | The path to the local file. 163 | storage_reference : pyrebase.pyrebase.Storage 164 | The storage reference to upload the byte data to. 165 | 166 | Returns 167 | ------ 168 | None 169 | 170 | """ 171 | if not os.path.exists(file_path): 172 | raise FileNotFoundError("File not found: {}".format(file_path)) 173 | with open(file_path, "rb") as file: 174 | byte_data = file.read() 175 | storage_reference.put(byte_data) 176 | 177 | def upload_data_to_reference(self, data, storage_reference, pretty=True): 178 | """ 179 | Uploads data to the specified storage reference. 180 | 181 | Parameters 182 | ---------- 183 | data : Any should be json serializable 184 | The data to be uploaded. 185 | storage_reference : pyrebase.pyrebase.Storage 186 | The storage reference to upload the data to. 187 | pretty : bool, optional 188 | Whether to format the JSON data with indentation and line breaks (default is True). 189 | 190 | Returns 191 | ------ 192 | None 193 | 194 | """ 195 | serialized_data = json_dumps(data, pretty=pretty) 196 | file_object = io.BytesIO(serialized_data.encode()) 197 | storage_reference.put(file_object) 198 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | from compas_invocations2 import build 6 | from compas_invocations2 import docs 7 | from compas_invocations2 import style 8 | from compas_invocations2 import tests 9 | from invoke import Collection 10 | 11 | ns = Collection( 12 | docs.help, 13 | style.check, 14 | style.lint, 15 | style.format, 16 | docs.docs, 17 | docs.linkcheck, 18 | tests.test, 19 | tests.testdocs, 20 | build.build_ghuser_components, 21 | build.prepare_changelog, 22 | build.clean, 23 | build.release, 24 | ) 25 | ns.configure( 26 | { 27 | "base_folder": os.path.dirname(__file__), 28 | "ghuser": { 29 | "source_dir": "src/compas_xr/ghpython/components", 30 | "target_dir": "src/compas_xr/ghpython/components/ghuser", 31 | }, 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /temp/PLACEHOLDER: -------------------------------------------------------------------------------- 1 | # container for temorary files 2 | # these will be ignored by the version control system 3 | -------------------------------------------------------------------------------- /tests/compas_xr/project/test_project_manager.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | import pytest 4 | import json 5 | from compas_xr.project import ProjectManager 6 | 7 | 8 | @pytest.fixture 9 | def config_path(): 10 | config_path = tempfile.mktemp(suffix=".json", prefix="config_compas_xr") 11 | with open(config_path, "w+") as config_file: 12 | data = { 13 | "apiKey": "x123x123", 14 | "authDomain": "x123.firebaseapp.com", 15 | "databaseURL": "https://x123-default-rtdb.europe-west1.firebasedatabase.app", 16 | "storageBucket": "x123.appspot.com", 17 | } 18 | json.dump(data, config_file) 19 | return config_path 20 | 21 | 22 | def test_project_manager(config_path): 23 | pm = ProjectManager(config_path) 24 | assert pm is not None 25 | -------------------------------------------------------------------------------- /tests/ipy_test_runner.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | HERE = os.path.dirname(__file__) 8 | 9 | if __name__ == "__main__": 10 | # Fake Rhino modules 11 | pytest.load_fake_module("Rhino") 12 | pytest.load_fake_module("Rhino.Geometry", fake_types=["RTree", "Sphere", "Point3d"]) 13 | 14 | pytest.run(HERE) 15 | --------------------------------------------------------------------------------