├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── docs.yml │ ├── pr-checks.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UI_Component_Refactoring_Summary.md ├── compas_viewer-small.png ├── compas_viewer.png ├── docs ├── _images │ ├── PLACEHOLDER.png │ ├── about.jpg │ ├── arrows.jpg │ ├── blending.jpg │ ├── brep.jpg │ ├── colors.jpg │ ├── compas_viewer.png │ ├── confirm.jpg │ ├── critical.jpg │ ├── dynamic.gif │ ├── dynamic_mesh.gif │ ├── face_vertext_color.jpg │ ├── fonts.jpg │ ├── frames.jpg │ ├── geometries.jpg │ ├── info.jpg │ ├── lines.jpg │ ├── mesh_display.jpg │ ├── nurbs.jpg │ ├── orbiting.gif │ ├── pointcloud.png │ ├── points.jpg │ ├── polyline.jpg │ ├── property_form.gif │ ├── question.jpg │ ├── robot.gif │ ├── scale.jpg │ ├── slider.gif │ ├── texts.jpg │ ├── transform.gif │ ├── tree.jpg │ ├── tree_view.jpg │ ├── vector.jpg │ └── warning.jpg ├── _static │ └── compas_icon_white.png ├── acknowledgements.rst ├── api.rst ├── api │ ├── compas_viewer.commands.rst │ ├── compas_viewer.components.rst │ ├── compas_viewer.events.rst │ ├── compas_viewer.renderer.rst │ ├── compas_viewer.scene.rst │ └── compas_viewer.ui.rst ├── conf.py ├── examples.rst ├── examples │ ├── __temp │ │ ├── basics.rst │ │ ├── basics │ │ │ └── messages.rst │ │ ├── layout.rst │ │ ├── layout │ │ │ ├── property_form.py │ │ │ ├── property_form.rst │ │ │ ├── slider.py │ │ │ ├── slider.rst │ │ │ ├── tree.py │ │ │ ├── tree.rst │ │ │ ├── tree_view.py │ │ │ └── tree_view.rst │ │ ├── object.rst │ │ └── object │ │ │ ├── fonts.py │ │ │ ├── fonts.rst │ │ │ ├── robot.py │ │ │ ├── robot.rst │ │ │ ├── texts.py │ │ │ └── texts.rst │ ├── color.rst │ ├── color │ │ ├── blending.py │ │ ├── blending.rst │ │ ├── colors.py │ │ ├── colors.rst │ │ ├── face_vertext_color.py │ │ └── face_vertext_color.rst │ ├── dynamic.rst │ ├── dynamic │ │ ├── dynamic.py │ │ ├── dynamic.rst │ │ ├── dynamic_mesh.py │ │ ├── dynamic_mesh.rst │ │ ├── orbiting.py │ │ ├── orbiting.rst │ │ ├── transform.py │ │ └── transform.rst │ ├── object.rst │ └── object │ │ ├── arrows.py │ │ ├── arrows.rst │ │ ├── brep.py │ │ ├── brep.rst │ │ ├── frames.py │ │ ├── frames.rst │ │ ├── geometries.py │ │ ├── geometries.rst │ │ ├── lines.py │ │ ├── lines.rst │ │ ├── mesh_display.py │ │ ├── mesh_display.rst │ │ ├── nurbs.py │ │ ├── nurbs.rst │ │ ├── pointcloud.py │ │ ├── pointcloud.rst │ │ ├── points.py │ │ ├── points.rst │ │ ├── polyline.py │ │ ├── polyline.rst │ │ ├── scale.py │ │ ├── scale.rst │ │ ├── vector.py │ │ └── vector.rst ├── index.rst ├── installation.rst ├── license.rst ├── tutorial.rst └── tutorial │ ├── 000_basics.rst │ ├── 010_scenes.rst │ ├── 020_configurations.rst │ ├── 030_controls.rst │ └── 040_commands.rst ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── arrows.py ├── boxes.py ├── buffer.py ├── camera.py ├── collection.py ├── defaultcolor.py ├── dynamic_box.py ├── dynamic_scene.py ├── example.py ├── group.py ├── model.py ├── nurbscurve.py ├── plane.py ├── polygon.py ├── polyhedron.py ├── robot.py ├── scene.py ├── sidedock.py ├── tag.py ├── test_ui.py ├── treeform.py └── unit.py ├── src └── compas_viewer │ ├── __init__.py │ ├── __main__.py │ ├── assets │ ├── fonts │ │ └── FreeSans.ttf │ └── icons │ │ ├── camera_info.svg │ │ ├── compas_icon_white.png │ │ ├── delete_selected.svg │ │ ├── export_file.svg │ │ ├── gl_info.svg │ │ ├── import_file.svg │ │ ├── selection_info.svg │ │ ├── view_front.svg │ │ ├── view_perspective.svg │ │ ├── view_right.svg │ │ ├── view_top.svg │ │ └── zoom_selected.svg │ ├── base.py │ ├── commands.py │ ├── components │ ├── __init__.py │ ├── booleantoggle.py │ ├── boundcomponent.py │ ├── button.py │ ├── camerasetting.py │ ├── colorpicker.py │ ├── component.py │ ├── container.py │ ├── mainwindow.py │ ├── menubar.py │ ├── numberedit.py │ ├── objectsetting.py │ ├── sceneform.py │ ├── sidebar.py │ ├── sidedock.py │ ├── slider.py │ ├── statusbar.py │ ├── tabform.py │ ├── textedit.py │ ├── toolbar.py │ ├── treeform.py │ └── viewport.py │ ├── config.py │ ├── events.py │ ├── gl.py │ ├── mouse.py │ ├── qt.py │ ├── renderer │ ├── __init__.py │ ├── camera.py │ ├── renderer.py │ └── shaders │ │ ├── __init__.py │ │ ├── model.frag │ │ ├── model.vert │ │ ├── modellines.frag │ │ ├── modellines.geom │ │ ├── modellines.vert │ │ ├── shader.py │ │ ├── tag.frag │ │ └── tag.vert │ ├── scene │ ├── __init__.py │ ├── brepobject.py │ ├── buffermanager.py │ ├── bufferobject.py │ ├── circleobject.py │ ├── collectionobject.py │ ├── ellipseobject.py │ ├── frameobject.py │ ├── geometryobject.py │ ├── graphobject.py │ ├── gridobject.py │ ├── group.py │ ├── lineobject.py │ ├── meshobject.py │ ├── nurbscurveobject.py │ ├── nurbssurfaceobject.py │ ├── planeobject.py │ ├── pointcloudobject.py │ ├── pointobject.py │ ├── polygonobject.py │ ├── polyhedronobject.py │ ├── polylineobject.py │ ├── scene.py │ ├── sceneobject.py │ ├── shapeobject.py │ ├── tagobject.py │ └── vectorobject.py │ ├── singleton.py │ ├── timer.py │ ├── ui.py │ └── viewer.py ├── tasks.py └── tests ├── test_placeholder.py └── test_viewer.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 = 180 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_ubuntu: 13 | if: "!contains(github.event.pull_request.labels.*.name, 'docs-only')" 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python: ["3.9", "3.10", "3.11"] 18 | steps: 19 | - name: update 20 | run: sudo apt-get update 21 | - name: install libegl1 22 | run: sudo apt-get install libegl1 23 | - name: install python3-opengl 24 | run: sudo apt-get install python3-opengl 25 | - uses: compas-dev/compas-actions.build@v4 26 | with: 27 | invoke_lint: true 28 | invoke_test: true 29 | python: ${{ matrix.python }} 30 | 31 | build: 32 | if: "!contains(github.event.pull_request.labels.*.name, 'docs-only')" 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | os: [macos-latest, windows-latest] 37 | python: ["3.9", "3.10", "3.11"] 38 | 39 | steps: 40 | - uses: compas-dev/compas-actions.build@v4 41 | with: 42 | invoke_lint: true 43 | invoke_test: true 44 | python: ${{ matrix.python }} 45 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | docs: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: update 18 | run: sudo apt-get update 19 | - name: install libegl1 20 | run: sudo apt-get install libegl1 21 | - name: install python3-opengl 22 | run: sudo apt-get install python3-opengl 23 | - uses: compas-dev/compas-actions.docs@v4 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | doc_url: https://compas.dev/compas_viewer/ 27 | -------------------------------------------------------------------------------- /.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 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build_ubuntu: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python: ["3.9", "3.10", "3.11"] 14 | steps: 15 | - name: update 16 | run: sudo apt-get update 17 | - name: install libegl1 18 | run: sudo apt-get install libegl1 19 | - name: install python3-opengl 20 | run: sudo apt-get install python3-opengl 21 | - uses: compas-dev/compas-actions.build@v4 22 | with: 23 | invoke_lint: true 24 | invoke_test: true 25 | python: ${{ matrix.python }} 26 | 27 | build: 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | matrix: 31 | os: [macos-latest, windows-latest] 32 | python: ["3.9", "3.10", "3.11"] 33 | 34 | steps: 35 | - uses: compas-dev/compas-actions.build@v4 36 | with: 37 | invoke_lint: true 38 | invoke_test: true 39 | python: ${{ matrix.python }} 40 | 41 | Publish: 42 | needs: [build, build_ubuntu] 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: compas-dev/compas-actions.publish@v3 46 | with: 47 | pypi_token: ${{ secrets.PYPI }} 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.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_viewer 105 | # ============================================================================== 106 | 107 | *.3dmbak 108 | *.rhl 109 | *.rui_bak 110 | 111 | temp/** 112 | !temp/PLACEHOLDER 113 | 114 | .DS_Store 115 | 116 | .vscode 117 | 118 | **/generated/ 119 | 120 | conda.recipe/ 121 | 122 | .ruff_cache 123 | .pytest_cache 124 | -------------------------------------------------------------------------------- /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_viewer) 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 -r requirements-dev.txt 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_viewer/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_viewer/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 | COMPAS Association 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 | -------------------------------------------------------------------------------- /UI_Component_Refactoring_Summary.md: -------------------------------------------------------------------------------- 1 | # UI Component System Refactoring 2 | 3 | ## Overview 4 | Complete restructuring of the compas_viewer UI architecture to implement a modern component-based system with improved modularity and maintainability. 5 | 6 | ## Key Changes 7 | 8 | ### New Component Architecture 9 | - Added `Component` base class with standardized `widget` attribute and `update()` method 10 | - Added `BoundComponent` class for components bound to object attributes with automatic value synchronization 11 | - All UI components now inherit from `Base` class for consistent structure 12 | 13 | ### Component Refactoring 14 | - **Replaced dialogs with integrated components**: `CameraSettingsDialog` → `CameraSetting`, `ObjectSettingDialog` → `ObjectSetting` 15 | - **Enhanced existing components**: Updated `Slider`, `TextEdit`, `Button` to use new inheritance model 16 | - **Added new components**: `BooleanToggle`, `ColorPicker`, `NumberEdit`, `Container`, `Tabform` 17 | - **Removed deprecated components**: `ColorComboBox`, `ComboBox`, `DoubleEdit`, `LineEdit`, `LabelWidget` 18 | 19 | ### UI Structure Improvements 20 | - Moved components to dedicated `components/` folder 21 | - Added `MainWindow`, `StatusBar`, `ViewPort` components 22 | - Refactored `MenuBar`, `ToolBar`, `SideDock` to use new component system 23 | - Updated `UI` class to use new component architecture 24 | 25 | ### Technical Improvements 26 | - Standardized component initialization with `obj`, `attr`, `action` parameters 27 | - Improved data binding with automatic attribute synchronization 28 | - Enhanced container system with scrollable and splitter options 29 | - Updated event handling and signal connections 30 | 31 | ## Impact 32 | This refactoring provides a cleaner, more maintainable codebase with better separation of concerns and improved extensibility for future UI development. -------------------------------------------------------------------------------- /compas_viewer-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/compas_viewer-small.png -------------------------------------------------------------------------------- /compas_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/compas_viewer.png -------------------------------------------------------------------------------- /docs/_images/PLACEHOLDER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/PLACEHOLDER.png -------------------------------------------------------------------------------- /docs/_images/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/about.jpg -------------------------------------------------------------------------------- /docs/_images/arrows.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/arrows.jpg -------------------------------------------------------------------------------- /docs/_images/blending.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/blending.jpg -------------------------------------------------------------------------------- /docs/_images/brep.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/brep.jpg -------------------------------------------------------------------------------- /docs/_images/colors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/colors.jpg -------------------------------------------------------------------------------- /docs/_images/compas_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/compas_viewer.png -------------------------------------------------------------------------------- /docs/_images/confirm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/confirm.jpg -------------------------------------------------------------------------------- /docs/_images/critical.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/critical.jpg -------------------------------------------------------------------------------- /docs/_images/dynamic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/dynamic.gif -------------------------------------------------------------------------------- /docs/_images/dynamic_mesh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/dynamic_mesh.gif -------------------------------------------------------------------------------- /docs/_images/face_vertext_color.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/face_vertext_color.jpg -------------------------------------------------------------------------------- /docs/_images/fonts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/fonts.jpg -------------------------------------------------------------------------------- /docs/_images/frames.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/frames.jpg -------------------------------------------------------------------------------- /docs/_images/geometries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/geometries.jpg -------------------------------------------------------------------------------- /docs/_images/info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/info.jpg -------------------------------------------------------------------------------- /docs/_images/lines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/lines.jpg -------------------------------------------------------------------------------- /docs/_images/mesh_display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/mesh_display.jpg -------------------------------------------------------------------------------- /docs/_images/nurbs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/nurbs.jpg -------------------------------------------------------------------------------- /docs/_images/orbiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/orbiting.gif -------------------------------------------------------------------------------- /docs/_images/pointcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/pointcloud.png -------------------------------------------------------------------------------- /docs/_images/points.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/points.jpg -------------------------------------------------------------------------------- /docs/_images/polyline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/polyline.jpg -------------------------------------------------------------------------------- /docs/_images/property_form.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/property_form.gif -------------------------------------------------------------------------------- /docs/_images/question.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/question.jpg -------------------------------------------------------------------------------- /docs/_images/robot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/robot.gif -------------------------------------------------------------------------------- /docs/_images/scale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/scale.jpg -------------------------------------------------------------------------------- /docs/_images/slider.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/slider.gif -------------------------------------------------------------------------------- /docs/_images/texts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/texts.jpg -------------------------------------------------------------------------------- /docs/_images/transform.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/transform.gif -------------------------------------------------------------------------------- /docs/_images/tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/tree.jpg -------------------------------------------------------------------------------- /docs/_images/tree_view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/tree_view.jpg -------------------------------------------------------------------------------- /docs/_images/vector.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/vector.jpg -------------------------------------------------------------------------------- /docs/_images/warning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_images/warning.jpg -------------------------------------------------------------------------------- /docs/_static/compas_icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/docs/_static/compas_icon_white.png -------------------------------------------------------------------------------- /docs/acknowledgements.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Acknowledgements 3 | ******************************************************************************** 4 | 5 | Coming soon! 6 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | API Reference 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :titlesonly: 8 | 9 | api/* 10 | 11 | 12 | Classes 13 | ======= 14 | 15 | .. currentmodule:: compas_viewer 16 | 17 | .. autosummary:: 18 | :toctree: generated/ 19 | :nosignatures: 20 | 21 | Viewer 22 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.commands.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.commands 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.commands 6 | 7 | Functions 8 | ========= 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | :nosignatures: 13 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.components.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.components 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.components 6 | 7 | Classes 8 | ======= 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | :nosignatures: 13 | 14 | Button 15 | ComboBox 16 | CameraSettingsDialog 17 | Renderer 18 | Slider 19 | Treeform 20 | ViewModeAction 21 | mainwindow.MainWindow 22 | menubar.MenuBar 23 | sidebar.SideBarRight 24 | sidedock.SideDock 25 | statusbar.StatusBar 26 | toolbar.ToolBar 27 | viewport.ViewPort 28 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.events.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.events 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.events 6 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.renderer.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.renderer 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.renderer 6 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.scene.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.scene 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.scene 6 | 7 | Classes 8 | ======= 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | :nosignatures: 13 | 14 | ViewerSceneObject 15 | MeshObject 16 | PointObject 17 | LineObject 18 | VectorObject 19 | Tag 20 | TagObject 21 | FrameObject 22 | CircleObject 23 | BoxObject 24 | TorusObject 25 | PolylineObject 26 | PolygonObject 27 | SphereObject 28 | PlaneObject 29 | CylinderObject 30 | EllipseObject 31 | ConeObject 32 | CapsuleObject 33 | NurbsSurfaceObject 34 | -------------------------------------------------------------------------------- /docs/api/compas_viewer.ui.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | compas_viewer.ui 3 | ******************************************************************************* 4 | 5 | .. currentmodule:: compas_viewer.ui 6 | 7 | Classes 8 | ======= 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | :nosignatures: 13 | 14 | UI 15 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | 4 | import sphinx_compas2_theme 5 | from sphinx.writers import html 6 | from sphinx.writers import html5 7 | 8 | # -- General configuration ------------------------------------------------ 9 | 10 | project = "COMPAS Viewer" 11 | copyright = "COMPAS Association" 12 | author = "Li Chen" 13 | organization = "compas-dev" 14 | package = "compas_viewer" 15 | 16 | master_doc = "index" 17 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"} 18 | templates_path = sphinx_compas2_theme.get_autosummary_templates_path() 19 | exclude_patterns = sphinx_compas2_theme.default_exclude_patterns 20 | add_module_names = False 21 | language = "en" 22 | 23 | latest_version = sphinx_compas2_theme.get_latest_version() 24 | 25 | if latest_version == "Unreleased": 26 | release = "Unreleased" 27 | version = "latest" 28 | else: 29 | release = latest_version 30 | version = ".".join(release.split(".")[0:2]) # type: ignore 31 | 32 | # -- Extension configuration ------------------------------------------------ 33 | 34 | extensions = sphinx_compas2_theme.default_extensions 35 | 36 | # numpydoc options 37 | 38 | numpydoc_show_class_members = False 39 | numpydoc_class_members_toctree = False 40 | numpydoc_attributes_as_param_list = True 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 | intersphinx_mapping = { 80 | "python": ("https://docs.python.org/", None), 81 | "compas": ("https://compas.dev/compas/latest/", None), 82 | } 83 | 84 | # linkcode 85 | 86 | linkcode_resolve = sphinx_compas2_theme.get_linkcode_resolve(organization, package) 87 | 88 | # extlinks 89 | 90 | extlinks = { 91 | "GL": ("https://pyopengl.sourceforge.net/documentation/manual-3.0/%s.html", "%s"), 92 | "QtCore": ("https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.%s", "%s"), 93 | "PySide6": ("https://doc.qt.io/qtforpython-6/%s.html", "%s"), 94 | } 95 | 96 | # from pytorch 97 | 98 | sphinx_compas2_theme.replace(html.HTMLTranslator) 99 | sphinx_compas2_theme.replace(html5.HTML5Translator) 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | html_theme = "sidebaronly" 104 | html_title = project 105 | 106 | favicons = [ 107 | { 108 | "rel": "icon", 109 | "href": "compas.ico", # relative to the static path 110 | } 111 | ] 112 | 113 | html_theme_options = { 114 | "external_links": [ 115 | {"name": "COMPAS Framework", "url": "https://compas.dev"}, 116 | ], 117 | "icon_links": [ 118 | { 119 | "name": "GitHub", 120 | "url": f"https://github.com/{organization}/{package}", 121 | "icon": "fa-brands fa-github", 122 | "type": "fontawesome", 123 | }, 124 | { 125 | "name": "Discourse", 126 | "url": "http://forum.compas-framework.org/", 127 | "icon": "fa-brands fa-discourse", 128 | "type": "fontawesome", 129 | }, 130 | { 131 | "name": "PyPI", 132 | "url": f"https://pypi.org/project/{package}/", 133 | "icon": "fa-brands fa-python", 134 | "type": "fontawesome", 135 | }, 136 | ], 137 | "switcher": { 138 | "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", 139 | "version_match": version, 140 | }, 141 | "logo": { 142 | "image_light": "_static/compas_icon_white.png", 143 | "image_dark": "_static/compas_icon_white.png", 144 | "text": project, 145 | }, 146 | "navigation_depth": 2, 147 | } 148 | 149 | html_context = { 150 | "github_url": "https://github.com", 151 | "github_user": organization, 152 | "github_repo": package, 153 | "github_version": "main", 154 | "doc_path": "docs", 155 | } 156 | 157 | html_static_path = sphinx_compas2_theme.get_html_static_path() + ["_static"] 158 | html_css_files = [] 159 | html_extra_path = [] 160 | html_last_updated_fmt = "" 161 | html_copy_source = False 162 | html_show_sourcelink = True 163 | html_permalinks = False 164 | html_permalinks_icon = "" 165 | html_compact_lists = True 166 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :titlesonly: 8 | :glob: 9 | 10 | examples/* 11 | -------------------------------------------------------------------------------- /docs/examples/__temp/basics.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Basic Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | basics/** 10 | -------------------------------------------------------------------------------- /docs/examples/__temp/basics/messages.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Windows Messages 3 | ******************************************************************************* 4 | 5 | 6 | .. autosummary:: 7 | :toctree: 8 | :nosignatures: 9 | 10 | 11 | You can add window pop-up messages to your viewer using the following methods. 12 | 13 | 14 | 15 | "About" Message 16 | ===================== 17 | To display "about" message from the configuration file: 18 | 19 | .. code-block:: python 20 | 21 | viewer.layout.window.about() 22 | 23 | .. image:: ../../_images/about.jpg 24 | 25 | 26 | "Info" Message 27 | ==================== 28 | To display "info" message: 29 | 30 | .. code-block:: python 31 | 32 | viewer.layout.window.info("This is an info message.") 33 | 34 | .. image:: ../../_images/info.jpg 35 | 36 | "Warning" Message 37 | ======================= 38 | To display "warning" message: 39 | 40 | .. code-block:: python 41 | 42 | viewer.layout.window.warning("This is a warning message.") 43 | 44 | .. image:: ../../_images/warning.jpg 45 | 46 | "Critical" Message 47 | ======================== 48 | To display "critical" message: 49 | 50 | .. code-block:: python 51 | 52 | viewer.layout.window.critical("This is an error message.") 53 | 54 | .. image:: ../../_images/critical.jpg 55 | 56 | "Question" Message 57 | ======================== 58 | To display "question" message: 59 | 60 | .. code-block:: python 61 | 62 | viewer.layout.window.question("This is a question message.") 63 | 64 | .. image:: ../../_images/question.jpg 65 | 66 | "Confirm" Message 67 | ======================= 68 | To display "confirm" message: 69 | 70 | .. code-block:: python 71 | 72 | viewer.layout.window.confirm("This is a confirmation message.") 73 | 74 | .. image:: ../../_images/confirm.jpg 75 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Layout Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | layout/** 10 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/property_form.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Box 2 | from compas.geometry import Frame 3 | from compas_viewer import Viewer 4 | from compas_viewer.layout import Propertyform 5 | 6 | viewer = Viewer(rendermode="shaded") 7 | 8 | box_obj = viewer.scene.add(Box(5, 5, 5), name="Box_1") 9 | box_obj = viewer.add(Box(5, 5, 5, frame=Frame([-10, -10, 0], [1, 0, 0], [0, 1, 0])), name="Box_2") 10 | 11 | viewer.layout.sidedock.add_element(Propertyform()) 12 | 13 | viewer.show() 14 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/property_form.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Property Form 3 | ******************************************************************************* 4 | 5 | .. figure:: /_images/property_form.gif 6 | :figclass: figure 7 | :class: figure-img img-fluid 8 | 9 | .. literalinclude:: property_form.py 10 | :language: python 11 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/slider.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Bezier 3 | from compas.geometry import Point 4 | from compas.geometry import Polyline 5 | from compas_viewer import Viewer 6 | from compas_viewer.layout import Slider 7 | from compas_viewer.scene import PointObject 8 | 9 | curve = Bezier([[0, 0, 0], [3, 6, 0], [5, -3, 0], [10, 0, 0]]) 10 | 11 | viewer = Viewer(rendermode="shaded", width=1600, height=900) 12 | 13 | 14 | pointobj: PointObject = viewer.scene.add(Point(*curve.point_at(0)), pointsize=20, pointcolor=Color.red(), show_points=True) # type: ignore 15 | curveobj = viewer.scene.add(Polyline(curve.to_polyline()), linewidth=2, linecolor=Color.blue(), show_points=False) 16 | 17 | 18 | def slide(value, additional_var): 19 | value = value / 100 20 | print(additional_var + str(value)) 21 | pointobj.geometry = curve.point_at(value) 22 | pointobj.init() 23 | pointobj.update() 24 | viewer.renderer.update() 25 | 26 | 27 | viewer.layout.sidedock.add_element(Slider(slide, 0, 0, 100, 1, "Slide Point", additional_var="Slide the point along the curve at ")) 28 | 29 | 30 | viewer.show() 31 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/slider.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Slider 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/slider.gif 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: slider.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/tree.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Frame 2 | from compas.geometry import Sphere 3 | from compas_viewer import Viewer 4 | from compas_viewer.layout import Treeform 5 | 6 | viewer = Viewer(rendermode="shaded") 7 | 8 | viewer = Viewer() 9 | for i in range(10): 10 | for j in range(10): 11 | sp = viewer.scene.add(Sphere(1, Frame([10 * i, 10 * j, 0], [1, 0, 0], [0, 1, 0])), name=f"Sphere_{i}_{j}") 12 | 13 | 14 | viewer.layout.sidedock.add_element(Treeform(viewer.scene, {"Name": (lambda o: o.name), "Object": (lambda o: o)})) 15 | 16 | 17 | viewer.show() 18 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/tree.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Tree 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/tree.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: tree.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/tree_view.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas.geometry import Frame 4 | from compas_viewer import Viewer 5 | from compas_viewer.layout import Treeform 6 | 7 | viewer = Viewer(rendermode="shaded") 8 | 9 | for i in range(10): 10 | for j in range(10): 11 | viewer.scene.add( 12 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 13 | show_points=False, 14 | show_lines=True, 15 | surfacecolor=Color(i / 10, j / 10, 0.0), 16 | name=f"Box_{i}_{j}", 17 | ) 18 | 19 | form_ids = Treeform(viewer.scene, {"Name": (lambda o: o.name), "Object": (lambda o: o)}) 20 | viewer.layout.viewport.add_element(form_ids) 21 | # form_colors = Treeform( 22 | # viewer.scene, 23 | # {"Name": (lambda o: o.name), "Object-Color": (lambda o: o.surfacecolor)}, 24 | # backgrounds={"Object-Color": (lambda o: o.surfacecolor)}, 25 | # ) 26 | # viewer.layout.viewport.add_element(form_colors, False) 27 | 28 | viewer.show() 29 | -------------------------------------------------------------------------------- /docs/examples/__temp/layout/tree_view.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Tree View 3 | ******************************************************************************* 4 | 5 | .. figure:: /_images/tree_view.jpg 6 | :figclass: figure 7 | :class: figure-img img-fluid 8 | 9 | .. literalinclude:: tree_view.py 10 | :language: python 11 | -------------------------------------------------------------------------------- /docs/examples/__temp/object.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Object Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | object/** 10 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/fonts.py: -------------------------------------------------------------------------------- 1 | from os import PathLike 2 | 3 | from compas_viewer import Viewer 4 | from compas_viewer.scene import Tag 5 | 6 | viewer = Viewer() 7 | 8 | 9 | def find_sys_font(font_name: str) -> PathLike: # type: ignore 10 | from matplotlib import font_manager 11 | 12 | for font in font_manager.fontManager.ttflist: 13 | if font.name == font_name: 14 | font_dir = font.fname 15 | return font_dir # type: ignore 16 | 17 | 18 | # By default, the text is rendered using the FreeSans font from the library. 19 | t = Tag("EN", (0, 0, 0), height=50) 20 | viewer.scene.add(t) 21 | 22 | # Font specified is possible. 23 | t = Tag("EN", (3, 0, 0), height=50, font=find_sys_font("Times New Roman")) 24 | viewer.scene.add(t) 25 | 26 | # Multi-language text is possible if the machine has the font installed. 27 | t = Tag("中文 CN", (3, 3, 0), height=50, font=find_sys_font("DengXian")) 28 | viewer.scene.add(t) 29 | 30 | viewer.show() 31 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/fonts.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Fonts 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/fonts.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: fonts.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/robot.py: -------------------------------------------------------------------------------- 1 | from compas_robots import RobotModel 2 | from compas_robots.resources import GithubPackageMeshLoader 3 | from compas_robots.viewer.scene.robotmodelobject import RobotModelObject 4 | 5 | from compas_viewer import Viewer 6 | from compas_viewer.layout import Slider 7 | from compas_viewer.layout import Treeform 8 | 9 | viewer = Viewer(rendermode="lighted") 10 | 11 | 12 | github = GithubPackageMeshLoader("ros-industrial/abb", "abb_irb6600_support", "kinetic-devel") 13 | model = RobotModel.from_urdf_file(github.load_urdf("irb6640.urdf")) 14 | model.load_geometry(github) 15 | 16 | configuration = model.random_configuration() 17 | robot_object: RobotModelObject = viewer.scene.add(model, show_lines=False, show_points=False, configuration=configuration) # type: ignore 18 | 19 | 20 | def rotate(value, robot_object: RobotModelObject, index: int): 21 | config = robot_object.configuration 22 | config.joint_values[index] = value / 360 * 2 * 3.14159 23 | robot_object.update_joints(config) 24 | 25 | 26 | for i, joint in enumerate(robot_object.configuration.joint_names): 27 | slider = Slider( 28 | rotate, 29 | configuration.joint_values[i] / 360 * 2 * 3.14159, 30 | -180, 31 | 180, 32 | 1, 33 | joint, 34 | robot_object=robot_object, 35 | index=i, 36 | ) 37 | slider = viewer.layout.sidedock.add_element(slider) 38 | 39 | 40 | treeform = Treeform(viewer.scene, {"Name": (lambda o: o.name), "Object": (lambda o: o)}) 41 | 42 | viewer.layout.sidedock.add_element(treeform) 43 | 44 | robot_object.update_joints(robot_object.configuration) 45 | 46 | viewer.show() 47 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/robot.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Robot 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | 8 | .. figure:: /_images/robot.gif 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: robot.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/texts.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Translation 3 | from compas_viewer import Viewer 4 | from compas_viewer.scene import Tag 5 | 6 | viewer = Viewer() 7 | 8 | t = Tag("a", (0, 0, 0), height=50) 9 | viewer.scene.add(t) 10 | 11 | t = Tag("123", (0, 0, 0), height=50) 12 | t.transform(Translation.from_vector([3, 0, 0])) 13 | viewer.scene.add(t) 14 | 15 | t = Tag("ABC", (0, 0, 0), height=50, absolute_height=True, color=Color.red()) 16 | t_obj = viewer.scene.add(t) 17 | t_obj.transformation = Translation.from_vector([3, 3, 0]) 18 | 19 | 20 | viewer.show() 21 | -------------------------------------------------------------------------------- /docs/examples/__temp/object/texts.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Texts 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/texts.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: texts.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/color.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Color Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | color/** 10 | -------------------------------------------------------------------------------- /docs/examples/color/blending.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | import compas 4 | from compas.colors import Color 5 | from compas.datastructures import Mesh 6 | from compas.geometry import Scale 7 | from compas.geometry import Translation 8 | from compas_viewer import Viewer 9 | 10 | viewer = Viewer() 11 | 12 | mesh = Mesh.from_obj(compas.get("faces.obj")) 13 | T = Translation.from_vector([0, 0, 1]) 14 | S = Scale.from_factors([0.5, 0.5, 0.5]) 15 | mesh.transform(T * S) 16 | 17 | mesh2 = mesh.transformed(Translation.from_vector([-6, 0, 0])) 18 | 19 | facecolor = {k: Color(random(), random(), random()) for k in mesh.faces()} 20 | edgecolor = {k: Color(random(), random(), random()) for k in mesh.edges()} 21 | vertexcolor = {k: Color(random(), random(), random()) for k in mesh.vertices()} 22 | 23 | viewer.scene.add(mesh, name="mesh1", show_points=True, facecolor=facecolor, edgecolor=edgecolor, vertexcolor=vertexcolor, use_vertexcolors=True) # type: ignore 24 | viewer.scene.add( 25 | mesh2, 26 | name="mesh2", 27 | show_points=True, 28 | facecolor=Color.red(), 29 | edgecolor=Color.green(), 30 | vertexcolor=Color.blue(), 31 | ) 32 | 33 | viewer.show() 34 | -------------------------------------------------------------------------------- /docs/examples/color/blending.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Blending 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/blending.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: blending.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/color/colors.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas.geometry import Frame 4 | from compas.geometry import Translation 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | 9 | box = Box(1, 1, 1, Frame((0, 0, 0), [1, 0, 0], [0, 1, 0])) 10 | obj1 = viewer.scene.add(box, surfacecolor=Color(1.0, 0.0, 0.0), opacity=0.7) 11 | 12 | box = Box(1, 1, 1, Frame((0, 0, 0), [1, 0, 0], [0, 1, 0])) 13 | obj2 = viewer.scene.add(box, surfacecolor=Color(0.0, 1.0, 0.0), opacity=0.7) 14 | 15 | box = Box(1, 1, 1, Frame((0, 0, 0), [1, 0, 0], [0, 1, 0])) 16 | obj3 = viewer.scene.add(box, surfacecolor=Color(0.0, 0.0, 1.0), opacity=0.7) 17 | 18 | 19 | obj2.transformation = Translation.from_vector([2, 0, 0]) 20 | obj3.transformation = Translation.from_vector([4, 0, 0]) 21 | 22 | viewer.show() 23 | -------------------------------------------------------------------------------- /docs/examples/color/colors.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Colors 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/colors.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: colors.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/color/face_vertext_color.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | import compas 4 | from compas.colors import Color 5 | from compas.datastructures import Mesh 6 | from compas_viewer import Viewer 7 | 8 | viewer = Viewer() 9 | 10 | mesh = Mesh.from_obj(compas.get("faces.obj")) 11 | 12 | vertexcolor = {vertex: Color.from_i(random()) for vertex in mesh.vertices()} 13 | 14 | viewer.scene.add(mesh, use_vertexcolors=True, vertexcolor=vertexcolor) 15 | 16 | viewer.show() 17 | -------------------------------------------------------------------------------- /docs/examples/color/face_vertext_color.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Face Vertex Color 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/face_vertext_color.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: face_vertext_color.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/dynamic.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Dynamic Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | dynamic/** 10 | -------------------------------------------------------------------------------- /docs/examples/dynamic/dynamic.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Point 3 | from compas_viewer import Viewer 4 | from compas_viewer.scene import PointObject 5 | 6 | viewer = Viewer() 7 | obj: PointObject = viewer.scene.add(Point(0, 0, 0), show_points=True, pointcolor=Color.red(), pointsize=10) # type: ignore 8 | 9 | 10 | @viewer.on(interval=1000) 11 | def movepoint(frame): 12 | print("frame", frame) 13 | obj.geometry.x += 0.1 14 | obj.update(update_data=True) 15 | 16 | 17 | viewer.show() 18 | -------------------------------------------------------------------------------- /docs/examples/dynamic/dynamic.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Dynamic 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/dynamic.gif 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: dynamic.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/dynamic/dynamic_mesh.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | import compas 4 | from compas.colors import Color 5 | from compas.datastructures import Mesh 6 | from compas_viewer import Viewer 7 | 8 | viewer = Viewer() 9 | 10 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 11 | obj = viewer.scene.add(mesh, surfacecolor=Color.cyan(), use_vertexcolors=False) 12 | 13 | 14 | @viewer.on(interval=200) 15 | def deform_mesh(frame): 16 | for v in mesh.vertices(): 17 | vertex: list = mesh.vertex_attributes(v, "xyz") # type: ignore 18 | vertex[0] += random() - 0.5 19 | vertex[1] += random() - 0.5 20 | vertex[2] += random() - 0.5 21 | mesh.vertex_attributes(v, "xyz", vertex) 22 | obj.update(update_data=True) 23 | print(frame) 24 | 25 | 26 | viewer.show() 27 | -------------------------------------------------------------------------------- /docs/examples/dynamic/dynamic_mesh.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Dynamic Mesh 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/dynamic_mesh.gif 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: dynamic_mesh.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/dynamic/orbiting.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.datastructures import Mesh 3 | from compas.geometry import Frame 4 | from compas.geometry import Sphere 5 | from compas_viewer import Viewer 6 | 7 | sphere = Sphere(1.0, Frame.worldXY()) 8 | 9 | # ============================================================================= 10 | # Visualization 11 | # ============================================================================= 12 | 13 | viewer = Viewer(show_grid=False, rendermode="lighted") 14 | viewer.renderer.camera.rotation.x = 10 15 | viewer.renderer.camera.rotation.y = 10 16 | viewer.renderer.camera.distance = 5 17 | 18 | viewer.scene.add( 19 | Mesh.from_shape(sphere, u=32, v=32), 20 | facecolor=Color.cyan(), 21 | linecolor=Color.blue(), 22 | use_vertexcolors=False, 23 | ) 24 | 25 | 26 | @viewer.on(interval=100) 27 | def orbit(f): 28 | viewer.renderer.camera.rotation.z += 0.02 29 | 30 | 31 | viewer.show() 32 | -------------------------------------------------------------------------------- /docs/examples/dynamic/orbiting.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Orbit Around an Object 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/orbiting.gif 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: orbiting.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/dynamic/transform.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas.geometry import Frame 4 | from compas.geometry import Scale 5 | from compas.geometry import Translation 6 | from compas_viewer import Viewer 7 | 8 | viewer = Viewer() 9 | 10 | box1 = Box(1, 1, 1, Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])) 11 | box2 = Box(1, 1, 1, Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])) 12 | box3 = Box(1, 1, 1, Frame([0, 0, 0], [1, 0, 0], [0, 1, 0])) 13 | obj1 = viewer.scene.add(box1, surfacecolor=Color.red()) 14 | obj2 = viewer.scene.add(box2, surfacecolor=Color.blue()) 15 | obj3 = viewer.scene.add(box3, surfacecolor=Color.green()) 16 | 17 | s = 1 18 | 19 | 20 | @viewer.on(interval=100) 21 | def transform(frame): 22 | obj1.transformation = Translation.from_vector([0, 0, 0.01 * frame]) 23 | obj1.update() 24 | 25 | obj2.transformation = Translation.from_vector([0, 0.01 * frame, 0]) 26 | obj2.update() 27 | 28 | S = Scale.from_factors([0.01 * frame, 0.01 * frame, 0.01 * frame]) 29 | obj3.transformation = S 30 | obj3.update() 31 | 32 | 33 | viewer.show() 34 | -------------------------------------------------------------------------------- /docs/examples/dynamic/transform.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Transform 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/transform.gif 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: transform.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/object.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Object Examples 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | object/** 10 | -------------------------------------------------------------------------------- /docs/examples/object/arrows.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Point 5 | from compas.geometry import Vector 6 | from compas_viewer import Viewer 7 | 8 | viewer = Viewer() 9 | 10 | for x in range(5): 11 | for y in range(5): 12 | viewer.scene.add(Vector(0, 0, 1), anchor=Point(x, y, 0), linecolor=Color.from_i(random())) 13 | 14 | viewer.show() 15 | -------------------------------------------------------------------------------- /docs/examples/object/arrows.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Arrows 3 | ******************************************************************************* 4 | 5 | .. autosummary:: 6 | :toctree: 7 | :nosignatures: 8 | .. figure:: /_images/arrows.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: arrows.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/object/brep.py: -------------------------------------------------------------------------------- 1 | from compas_occ.brep import OCCBrep 2 | 3 | from compas.geometry import Box 4 | from compas.geometry import Frame 5 | from compas_viewer import Viewer 6 | 7 | box = Box(1, 1, 1, Frame.worldXY()) 8 | brep = OCCBrep.from_box(box) 9 | 10 | viewer = Viewer() 11 | viewer.scene.add(brep) 12 | viewer.show() 13 | -------------------------------------------------------------------------------- /docs/examples/object/brep.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Brep 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/brep.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: brep.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/frames.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Frame 2 | from compas_viewer import Viewer 3 | 4 | viewer = Viewer() 5 | 6 | frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) 7 | 8 | viewer.scene.add(frame) 9 | 10 | viewer.show() 11 | -------------------------------------------------------------------------------- /docs/examples/object/frames.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Frames 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/frames.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: frames.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/geometries.py: -------------------------------------------------------------------------------- 1 | from compas_occ.brep import OCCBrep 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Box 5 | from compas.geometry import Capsule 6 | from compas.geometry import Circle 7 | from compas.geometry import Cone 8 | from compas.geometry import Cylinder 9 | from compas.geometry import Ellipse 10 | from compas.geometry import Frame 11 | from compas.geometry import NurbsSurface 12 | from compas.geometry import Plane 13 | from compas.geometry import Point 14 | from compas.geometry import Polyline 15 | from compas.geometry import Sphere 16 | from compas.geometry import Torus 17 | from compas.geometry import Translation 18 | from compas.geometry import Vector 19 | from compas_viewer import Viewer 20 | 21 | viewer = Viewer(rendermode="lighted", fullscreen=True) 22 | 23 | points = [ 24 | [Point(0, 0, 0), Point(1, 0, 0), Point(2, 0, 0), Point(3, 0, 0)], 25 | [Point(0, 1, 0), Point(1, 1, 2), Point(2, 1, 2), Point(3, 1, 0)], 26 | [Point(0, 2, 0), Point(1, 2, 2), Point(2, 2, 2), Point(3, 2, 0)], 27 | [Point(0, 3, 0), Point(1, 3, 0), Point(2, 3, 0), Point(3, 3, 0)], 28 | ] 29 | surface = NurbsSurface.from_points(points=points) 30 | obj = viewer.scene.add( 31 | surface, 32 | show_points=True, 33 | show_lines=True, 34 | pointcolor=Color(0.5, 0.0, 0.0), 35 | linecolor=Color(0.0, 0.0, 0.5), 36 | ) 37 | obj.transformation = Translation.from_vector(Vector(-1.5, -1.5, 0.0)) 38 | 39 | plane = Plane([0, 0, 0], [0, 0, 1]) 40 | obj = viewer.scene.add(plane, size=0.5, linecolor=Color(0.5, 0.0, 0.0), surfacecolor=Color(0.0, 0.0, 0.5)) 41 | obj.transformation = Translation.from_vector(Vector(5, 0.0, 0.0)) 42 | 43 | circle = Circle(0.8, Frame.worldXY()) 44 | obj = viewer.scene.add(circle) 45 | obj.transformation = Translation.from_vector(Vector(10, 0.0, 0.0)) 46 | 47 | ellipse = Ellipse(1.5, 0.5, Frame.worldXY()) 48 | obj = viewer.scene.add( 49 | item=ellipse, 50 | show_points=True, 51 | linecolor=Color(1.0, 0.0, 0.0), 52 | pointcolor=Color(0.0, 0.0, 1.0), 53 | ) 54 | obj.transformation = Translation.from_vector(Vector(0, 5, 0)) 55 | 56 | cone = Cone.from_circle_and_height(circle, 1.5) 57 | obj = viewer.scene.add(cone, surfacecolor=Color(0.5, 0.5, 0.0)) 58 | obj.transformation = Translation.from_vector(Vector(5, 5, 0)) 59 | 60 | box = OCCBrep.from_box(Box(1.5)) 61 | cx = OCCBrep.from_cylinder(Cylinder(0.5, 8, frame=Frame.worldYZ())) 62 | cy = OCCBrep.from_cylinder(Cylinder(0.5, 8, frame=Frame.worldZX())) 63 | cz = OCCBrep.from_cylinder(Cylinder(0.5, 8, frame=Frame.worldXY())) 64 | result = box - (cx + cy + cz) 65 | obj = viewer.scene.add(item=result, surfacecolor=Color(0.0, 0.5, 0.5), use_vertexcolors=False) 66 | obj.transformation = Translation.from_vector(Vector(10, 5, 0)) 67 | 68 | capsule = Capsule(0.8, 1) 69 | obj = viewer.scene.add(item=capsule, surfacecolor=Color(0.0, 0.0, 0.5), show_lines=False) 70 | obj.transformation = Translation.from_vector(Vector(0, 10, 0)) 71 | 72 | box = Box(1, 1, 1, Frame.worldXY()) 73 | obj = viewer.scene.add( 74 | item=box, 75 | surfacecolor=Color(0.0, 0.0, 0.5), 76 | linecolor=Color(0.5, 0.0, 0.0), 77 | pointcolor=Color(0.0, 0.5, 0.0), 78 | show_points=True, 79 | ) 80 | obj.transformation = Translation.from_vector(Vector(5, 10, 0)) 81 | 82 | points = [ 83 | [-0.5, -0.5, 0], 84 | [0.5, -0.5, 0], 85 | [0.5, 0.5, 0], 86 | [-0.5, 0.5, 0], 87 | [-0.5, -0.5, 0], 88 | ] 89 | obj = viewer.scene.add(Polyline(points), surfacecolor=Color(0.0, 0.0, 0.5), show_points=True) 90 | obj.transformation = Translation.from_vector(Vector(10, 10, 0)) 91 | 92 | torus = Torus(radius_axis=1, radius_pipe=0.5) 93 | obj = viewer.scene.add(torus, surfacecolor=Color(0.0, 0.5, 0.0), show_lines=False) 94 | obj.transformation = Translation.from_vector(Vector(0, 15, 0)) 95 | 96 | sphere = Sphere(frame=Frame.worldXY(), radius=1) 97 | obj = viewer.scene.add(sphere, surfacecolor=Color(0.0, 0.0, 0.5)) 98 | obj.transformation = Translation.from_vector(Vector(5, 15, 0)) 99 | 100 | cylinder = Cylinder(frame=Frame.worldXY(), radius=0.5, height=1) 101 | obj = viewer.scene.add(cylinder, surfacecolor=Color(0.0, 0.5, 0.5)) 102 | obj.transformation = Translation.from_vector(Vector(10, 15, 0)) 103 | 104 | viewer.show() 105 | -------------------------------------------------------------------------------- /docs/examples/object/geometries.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Geometries 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/geometries.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: geometries.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/lines.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Line 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | 9 | for i in range(10): 10 | line = Line([random() * 20, random() * 20, random() * 20], [random() * 20, random() * 20, random() * 20]) 11 | viewer.scene.add(line, linecolor=Color(random(), random(), random())) 12 | 13 | viewer.show() 14 | -------------------------------------------------------------------------------- /docs/examples/object/lines.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Lines 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/lines.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: lines.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/mesh_display.py: -------------------------------------------------------------------------------- 1 | import compas 2 | from compas.datastructures import Mesh 3 | from compas.geometry import Scale 4 | from compas.geometry import Translation 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | 9 | mesh = Mesh.from_obj(compas.get("faces.obj")) 10 | T = Translation.from_vector([0, 0, 1]) 11 | S = Scale.from_factors([0.5, 0.5, 0.5]) 12 | mesh.transform(T * S) 13 | 14 | mesh2 = mesh.transformed(Translation.from_vector([-6, 0, 0])) 15 | 16 | viewer.scene.add(mesh, hide_coplanaredges=False, use_vertexcolors=False) 17 | viewer.scene.add(mesh2, hide_coplanaredges=True, use_vertexcolors=False) 18 | 19 | viewer.show() 20 | -------------------------------------------------------------------------------- /docs/examples/object/mesh_display.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Mesh Display 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/mesh_display.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: mesh_display.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/nurbs.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import NurbsSurface 3 | from compas.geometry import Point 4 | from compas_viewer import Viewer 5 | 6 | points = [ 7 | [Point(0, 0, 0), Point(1, 0, 0), Point(2, 0, 0), Point(3, 0, 0)], 8 | [Point(0, 1, 0), Point(1, 1, 2), Point(2, 1, 2), Point(3, 1, 0)], 9 | [Point(0, 2, 0), Point(1, 2, 2), Point(2, 2, 2), Point(3, 2, 0)], 10 | [Point(0, 3, 0), Point(1, 3, 0), Point(2, 3, 0), Point(3, 3, 0)], 11 | ] 12 | 13 | surface = NurbsSurface.from_points(points=points) 14 | 15 | viewer = Viewer(rendermode="lighted") 16 | viewer.scene.add(surface, show_points=True, show_lines=True, pointcolor=Color(1.0, 0.0, 0.0), linecolor=Color(0.0, 0.0, 1.0)) 17 | viewer.show() 18 | -------------------------------------------------------------------------------- /docs/examples/object/nurbs.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Nurbs 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/nurbs.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: nurbs.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/pointcloud.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Pointcloud 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | pointcloud = Pointcloud.from_bounds(10, 10, 10, 1000) 9 | viewer.scene.add(pointcloud, pointcolor=Color(1.0,0.0,0.0), pointsize=10) 10 | 11 | viewer.show() 12 | -------------------------------------------------------------------------------- /docs/examples/object/pointcloud.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Point Cloud 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/pointcloud.png 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: pointcloud.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/points.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Point 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | for i in range(10): 9 | point = Point(random() * 10, random() * 10, random() * 10) 10 | viewer.scene.add(point, pointcolor=Color(random(), random(), random()), pointsize=random() * 50) 11 | 12 | 13 | viewer.show() 14 | -------------------------------------------------------------------------------- /docs/examples/object/points.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Points 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/points.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: points.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/polyline.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Polyline 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | 9 | pts = [] 10 | for i in range(10): 11 | pts.append([random.uniform(0, 10), random.uniform(0, 10), random.uniform(0, 10)]) 12 | 13 | polyline = Polyline(pts) 14 | viewer.scene.add(polyline, show_points=True, pointcolor=Color(1.0, 0.0, 1.0), linecolor=Color(1.0, 0.5, 1.0), linewidth=1) 15 | 16 | pts = [] 17 | for i in range(5): 18 | pts.append([random.uniform(0, 10), random.uniform(0, 10), random.uniform(0, 10)]) 19 | 20 | polyline = Polyline(pts) 21 | viewer.scene.add(polyline, show_points=True, pointcolor=Color(0.0, 0.0, 1.0), linecolor=Color(1.0, 0.0, 0.0), linewidth=5) 22 | 23 | viewer.show() 24 | -------------------------------------------------------------------------------- /docs/examples/object/polyline.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Polyline 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | .. figure:: /_images/polyline.jpg 8 | :figclass: figure 9 | :class: figure-img img-fluid 10 | 11 | .. literalinclude:: polyline.py 12 | :language: python 13 | -------------------------------------------------------------------------------- /docs/examples/object/scale.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Torus 2 | from compas_viewer import Viewer 3 | 4 | viewer = Viewer(rendermode="lighted") 5 | 6 | for i in range(1, 1000, 100): 7 | viewer.scene.add(Torus(i, i / 10), u=int(i / 5) + 10) 8 | 9 | viewer.show() 10 | -------------------------------------------------------------------------------- /docs/examples/object/scale.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Scale 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | 8 | .. figure:: /_images/scale.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: scale.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/examples/object/vector.py: -------------------------------------------------------------------------------- 1 | from math import cos 2 | from math import radians 3 | from math import sin 4 | from random import random 5 | 6 | from compas.colors import Color 7 | from compas.geometry import Vector 8 | from compas_viewer import Viewer 9 | 10 | viewer = Viewer() 11 | 12 | for i in range(0, 360, 20): 13 | for j in range(0, 180, 10): 14 | position = Vector( 15 | sin(radians(i)) * sin(radians(j)), 16 | cos(radians(i)) * sin(radians(j)), 17 | cos(radians(j)), 18 | ) 19 | vector = Vector(sin(radians(i)), cos(radians(i)), cos(radians(j))) 20 | viewer.scene.add(vector, anchor=position, linecolor=Color(random(), random(), random())) 21 | 22 | viewer.show() 23 | -------------------------------------------------------------------------------- /docs/examples/object/vector.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************* 2 | Vector 3 | ******************************************************************************* 4 | .. autosummary:: 5 | :toctree: 6 | :nosignatures: 7 | 8 | .. figure:: /_images/vector.jpg 9 | :figclass: figure 10 | :class: figure-img img-fluid 11 | 12 | .. literalinclude:: vector.py 13 | :language: python 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | COMPAS Viewer 3 | ******************************************************************************** 4 | 5 | .. figure:: /_images/compas_viewer.png 6 | :figclass: figure 7 | :class: figure-img img-fluid 8 | 9 | .. rst-class:: lead 10 | 11 | :mod:`compas_viewer` is a standalone CAD viewer and data management interface for COMPAS. 12 | It is based on Qt (through PySide6) and OpenGL (through PyOpenGL) and can be installed on Windows, OSX, and Linux to work with COMPAS geometry and data objects without further requirements or additional software. 13 | In addition, :mod:`compas_viewer` provides an easy-to-use API to make custom interfaces with specialised controls and widgets. 14 | 15 | 16 | Table of Contents 17 | ================= 18 | 19 | .. toctree:: 20 | :maxdepth: 3 21 | :titlesonly: 22 | 23 | Introduction 24 | installation 25 | tutorial 26 | examples 27 | api 28 | license 29 | acknowledgements 30 | 31 | 32 | Indices and tables 33 | ================== 34 | 35 | * :ref:`genindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Installation 3 | ******************************************************************************** 4 | 5 | Stable 6 | ====== 7 | 8 | Stable releases are available on PyPI and can be installed with pip. 9 | 10 | .. code-block:: bash 11 | 12 | pip install compas_viewer 13 | 14 | 15 | Latest 16 | ====== 17 | 18 | The latest version can be installed from local source. 19 | 20 | .. code-block:: bash 21 | 22 | git clone https://github.com/compas-dev/compas_viewer.git 23 | cd compas_viewer 24 | pip install -e . 25 | 26 | 27 | Development 28 | =========== 29 | 30 | To install `compas_viewer` for development, install from local source with the "dev" requirements. 31 | 32 | .. code-block:: bash 33 | 34 | git clone https://github.com/compas-dev/compas_viewer.git 35 | cd compas_viewer 36 | pip install -e ".[dev]" 37 | 38 | 39 | Known issues 40 | ============ 41 | 42 | For ubuntu users, following dependencies might be required depending on the system version: 43 | 44 | .. code-block:: bash 45 | 46 | sudo apt-get install libegl1 47 | sudo apt-get install python3-opengl 48 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | License 3 | ******************************************************************************** 4 | 5 | .. literalinclude:: ../LICENSE 6 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Tutorial 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :titlesonly: 8 | :glob: 9 | 10 | tutorial/* 11 | -------------------------------------------------------------------------------- /docs/tutorial/000_basics.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Viewer Basics 3 | ******************************************************************************** 4 | 5 | * Basic usage of the viewer is through a script. 6 | * Simply create a viewer instance and add objects to its scene. 7 | * The scene is an instance of the default COMPAS Scene and provides all standard visualisation options for all COMPAS objects. 8 | * Groups 9 | * Collections 10 | * Buffer Geometry 11 | * Default Configuration (See Configurations) 12 | * Custom User Interfaces (See Controls) 13 | -------------------------------------------------------------------------------- /docs/tutorial/010_scenes.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Viewer Scenes 3 | ******************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/tutorial/020_configurations.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Viewer Configurations 3 | ******************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/tutorial/030_controls.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Viewer Controls 3 | ******************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/tutorial/040_commands.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Viewer Commands 3 | ******************************************************************************** 4 | -------------------------------------------------------------------------------- /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_viewer" 11 | description = "Standalone viewer for COMPAS 2 based on PyOpenGL and PyQt6." 12 | keywords = ['visualisation', 'compas', 'pyopengl', 'pyqt6'] 13 | authors = [ 14 | { name = "tom van mele", email = "tom.v.mele@gmail.com" }, 15 | { name = "Li Chen", email = "li.chen@arch.ethz.ch" }, 16 | { name = "Zac Zhuo Zhang", email = "zhuo.zhang@arch.ethz.ch" }, 17 | ] 18 | license = { file = "LICENSE" } 19 | readme = "README.md" 20 | requires-python = ">=3.9" 21 | dynamic = ['dependencies', 'optional-dependencies', 'version'] 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Topic :: Scientific/Engineering", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | ] 31 | 32 | [project.urls] 33 | Homepage = "https://compas-dev.github.io/compas_viewer" 34 | Documentation = "https://compas-dev.github.io/compas_viewer" 35 | Repository = "https://github.com/compas-dev/compas_viewer.git" 36 | Changelog = "https://github.com/compas-dev/compas_viewer/blob/main/CHANGELOG.md" 37 | 38 | # ============================================================================ 39 | # setuptools config 40 | # ============================================================================ 41 | 42 | [tool.setuptools] 43 | package-dir = { "" = "src" } 44 | include-package-data = true 45 | zip-safe = false 46 | 47 | [tool.setuptools.dynamic] 48 | version = { attr = "compas_viewer.__version__" } 49 | dependencies = { file = "requirements.txt" } 50 | optional-dependencies = { dev = { file = "requirements-dev.txt" } } 51 | 52 | [tool.setuptools.packages.find] 53 | where = ["src"] 54 | 55 | [tool.setuptools.package-data] 56 | compas_viewer = ['config.json'] 57 | "compas_viewer.assets" = ["*.svg", "*.png"] 58 | "compas_viewer.assets.fonts" = ["*.ttf"] 59 | "compas_viewer.renderer.shaders" = ["*.vert", "*.frag", "*.geom"] 60 | 61 | # ============================================================================ 62 | # replace pytest.ini 63 | # ============================================================================ 64 | 65 | [tool.pytest.ini_options] 66 | minversion = "6.0" 67 | testpaths = ["tests", "src/compas_viewer"] 68 | python_files = ["test_*.py", "*_test.py", "test.py"] 69 | addopts = ["-ra", "--strict-markers", "--doctest-glob=*.rst", "--tb=short"] 70 | doctest_optionflags = [ 71 | "NORMALIZE_WHITESPACE", 72 | "IGNORE_EXCEPTION_DETAIL", 73 | "ALLOW_UNICODE", 74 | "ALLOW_BYTES", 75 | "NUMBER", 76 | ] 77 | 78 | # ============================================================================ 79 | # replace bumpversion.cfg 80 | # ============================================================================ 81 | 82 | [tool.bumpversion] 83 | current_version = "2.0.1" 84 | message = "Bump version to {new_version}" 85 | commit = true 86 | tag = true 87 | 88 | [[tool.bumpversion.files]] 89 | filename = "src/compas_viewer/__init__.py" 90 | search = "{current_version}" 91 | replace = "{new_version}" 92 | 93 | [[tool.bumpversion.files]] 94 | filename = "CHANGELOG.md" 95 | search = "Unreleased" 96 | replace = "[{new_version}] {now:%Y-%m-%d}" 97 | 98 | # ============================================================================ 99 | # replace setup.cfg 100 | # ============================================================================ 101 | 102 | [tool.black] 103 | line-length = 179 104 | 105 | [tool.ruff] 106 | line-length = 179 107 | indent-width = 4 108 | target-version = "py39" 109 | 110 | [tool.ruff.lint] 111 | select = ["E", "F", "I"] 112 | 113 | [tool.ruff.lint.per-file-ignores] 114 | "__init__.py" = ["I001"] 115 | "tests/*" = ["I001"] 116 | "tasks.py" = ["I001"] 117 | 118 | [tool.ruff.lint.isort] 119 | force-single-line = true 120 | known-first-party = ["compas", "compas_viewer"] 121 | 122 | [tool.ruff.lint.pydocstyle] 123 | convention = "numpy" 124 | 125 | [tool.ruff.lint.pycodestyle] 126 | max-doc-length = 179 127 | 128 | [tool.ruff.format] 129 | docstring-code-format = true 130 | docstring-code-line-length = "dynamic" 131 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | attrs >=17.4 2 | black >=22.12.0 3 | bump-my-version 4 | compas_invocations2 5 | compas_notebook 6 | invoke >=0.14 7 | ruff 8 | sphinx_compas2_theme 9 | twine 10 | wheel 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | compas >= 2.2.0 2 | freetype-py 3 | PyOpenGL 4 | PySide6 5 | -------------------------------------------------------------------------------- /scripts/arrows.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Vector 3 | from compas_viewer.viewer import Viewer 4 | 5 | viewer = Viewer() 6 | 7 | N = 10 8 | M = 10 9 | 10 | for i in range(N): 11 | for j in range(M): 12 | viewer.scene.add( 13 | Vector(0, 0, (i + j + 1) / 5), 14 | anchor = [i, j, 0], 15 | linecolor=Color(i / N, j / M, 0.0), 16 | name=f"Arrow_{i}_{j}", 17 | ) 18 | 19 | viewer.show() 20 | -------------------------------------------------------------------------------- /scripts/boxes.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas_viewer.viewer import Viewer 4 | from compas.geometry import Translation 5 | 6 | viewer = Viewer() 7 | N = 10 8 | for i in range(N): 9 | for j in range(N): 10 | for k in range(N): 11 | obj = viewer.scene.add( 12 | Box(0.5, 0.5, 0.5), 13 | show_points=True, 14 | linecolor=Color.white(), 15 | pointcolor=Color(1 - i / N, 1 - j / N, 1 - k / N), 16 | pointsize=10, 17 | facecolor=Color(i / N, j / N, k / N), 18 | name=f"Box_{i}_{j}_{k}", 19 | transformation = Translation.from_vector([i, j, k]) 20 | ) 21 | 22 | viewer.show() 23 | -------------------------------------------------------------------------------- /scripts/buffer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from compas_viewer import Viewer 4 | from compas_viewer.scene import BufferGeometry 5 | 6 | # One vertex per point 7 | points = np.random.rand(1000, 3) * 10 8 | pointcolor = np.random.rand(1000, 4) 9 | 10 | # Two vertices per line 11 | lines = np.random.rand(1000 * 2, 3) * 10 12 | linecolor = np.random.rand(1000 * 2, 4) 13 | 14 | # Three vertices per face 15 | faces = np.random.rand(1000 * 3, 3) * 10 16 | facecolor = np.random.rand(1000 * 3, 4) 17 | 18 | geometry = BufferGeometry(points=points, pointcolor=pointcolor, lines=lines, linecolor=linecolor, faces=faces, facecolor=facecolor) 19 | 20 | viewer = Viewer() 21 | obj = viewer.scene.add(geometry) 22 | 23 | 24 | @viewer.on(interval=200) 25 | def update(frame): 26 | 27 | geometry.points = np.random.rand(1000, 3) * 10 28 | geometry.pointcolor = np.random.rand(1000, 4) 29 | geometry.lines = np.random.rand(1000 * 2, 3) * 10 30 | geometry.linecolor = np.random.rand(1000 * 2, 4) 31 | geometry.faces = np.random.rand(1000 * 3, 3) * 10 32 | geometry.facecolor = np.random.rand(1000 * 3, 4) 33 | 34 | obj.update(update_data=True) 35 | 36 | 37 | viewer.show() 38 | -------------------------------------------------------------------------------- /scripts/camera.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas.geometry import Frame 4 | from compas_viewer.viewer import Viewer 5 | from compas_viewer.components import Button 6 | from compas_viewer.components import Slider 7 | 8 | viewer = Viewer() 9 | 10 | viewer.renderer.camera.target = [5, 0, 0] 11 | viewer.renderer.camera.position = [5, 10, 5] 12 | 13 | print("target", viewer.renderer.camera.target) 14 | 15 | for i in range(5): 16 | for j in range(5): 17 | viewer.scene.add( 18 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 19 | show_points=True, 20 | show_lines=True, 21 | surfacecolor=Color(i / 10, j / 10, 0.0), 22 | name=f"Box_{i}_{j}", 23 | ) 24 | 25 | def print_camera(): 26 | print("target", viewer.renderer.camera.target) 27 | print("position", viewer.renderer.camera.position) 28 | 29 | 30 | def update_camera_position_x(slider: Slider, value: int): 31 | viewer.renderer.camera.position.x = value 32 | viewer.renderer.update() 33 | print(viewer.renderer.camera.position) 34 | 35 | def update_camera_position_y(slider: Slider, value: int): 36 | viewer.renderer.camera.position.y = value 37 | viewer.renderer.update() 38 | print(viewer.renderer.camera.position) 39 | 40 | 41 | def update_camera_position_z(slider: Slider, value: int): 42 | viewer.renderer.camera.position.z = value 43 | viewer.renderer.update() 44 | print(viewer.renderer.camera.position) 45 | 46 | 47 | def update_camera_target_x(slider: Slider, value: int): 48 | viewer.renderer.camera.target.x = value 49 | viewer.renderer.update() 50 | print(viewer.renderer.camera.target) 51 | 52 | 53 | def update_camera_target_y(slider: Slider, value: int): 54 | viewer.renderer.camera.target.y = value 55 | viewer.renderer.update() 56 | print(viewer.renderer.camera.target) 57 | 58 | def update_camera_target_z(slider: Slider, value: int): 59 | viewer.renderer.camera.target.z = value 60 | viewer.renderer.update() 61 | print(viewer.renderer.camera.target) 62 | 63 | 64 | viewer.ui.sidedock.show = True 65 | viewer.ui.sidedock.add(Button(text="Print Camera", action=print_camera)) 66 | viewer.ui.sidedock.add(Slider(title="Position X", min_val=-10, max_val=10, step=1, action=update_camera_position_x)) 67 | viewer.ui.sidedock.add(Slider(title="Position Y", min_val=-10, max_val=10, step=1, action=update_camera_position_y)) 68 | viewer.ui.sidedock.add(Slider(title="Position Z", min_val=-10, max_val=10, step=1, action=update_camera_position_z)) 69 | viewer.ui.sidedock.add(Slider(title="Target X", min_val=-10, max_val=10, step=1, action=update_camera_target_x)) 70 | viewer.ui.sidedock.add(Slider(title="Target Y", min_val=-10, max_val=10, step=1, action=update_camera_target_y)) 71 | viewer.ui.sidedock.add(Slider(title="Target Z", min_val=-10, max_val=10, step=1, action=update_camera_target_z)) 72 | 73 | viewer.show() 74 | -------------------------------------------------------------------------------- /scripts/collection.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.geometry import Box 4 | from compas.geometry import Frame 5 | from compas.geometry import Line 6 | from compas_viewer.scene import Collection 7 | from compas_viewer.viewer import Viewer 8 | 9 | viewer = Viewer() 10 | 11 | boxes = [] 12 | for i in range(20): 13 | for j in range(20): 14 | box = Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])) 15 | boxes.append(box) 16 | 17 | viewer.scene.add(Collection(boxes), name="Boxes") 18 | 19 | lines = [] 20 | for i in range(1000): 21 | lines.append(Line([random() * 20, random() * 20, random() * 20], [random() * 20, random() * 20, random() * 20])) 22 | 23 | viewer.scene.add(Collection(lines), name="Lines") 24 | 25 | viewer.show() 26 | -------------------------------------------------------------------------------- /scripts/defaultcolor.py: -------------------------------------------------------------------------------- 1 | import compas 2 | from compas.datastructures import Mesh 3 | from compas.geometry import Box 4 | from compas.geometry import Frame 5 | from compas_viewer.viewer import Viewer 6 | 7 | viewer = Viewer() 8 | 9 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 10 | obj = viewer.scene.add(mesh) 11 | 12 | for i in range(5): 13 | for j in range(5): 14 | viewer.scene.add( 15 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 16 | name=f"Box_{i}_{j}", 17 | ) 18 | 19 | viewer.show() 20 | -------------------------------------------------------------------------------- /scripts/dynamic_box.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | 3 | from compas.datastructures import Mesh 4 | from compas.geometry import Box 5 | from compas_viewer import Viewer 6 | from compas.geometry import Translation 7 | 8 | viewer = Viewer() 9 | 10 | mesh = Mesh.from_shape(Box.from_width_height_depth(2, 2, 2)) 11 | obj1 = viewer.scene.add(mesh) 12 | obj2 = viewer.scene.add(mesh, transformation=Translation.from_vector([5, 0, 0])) 13 | obj3 = viewer.scene.add(mesh, transformation=Translation.from_vector([-5, 0, 0])) 14 | 15 | obj1.opacity = 0.7 16 | 17 | @viewer.on(interval=100) 18 | def deform_mesh(frame): 19 | obj1.opacity = (frame / 20) % 1 20 | for v in mesh.vertices(): 21 | vertex: list = mesh.vertex_attributes(v, "xyz") 22 | vertex[0] += (random() - 0.5) * 0.1 23 | vertex[1] += (random() - 0.5) * 0.1 24 | vertex[2] += (random() - 0.5) * 0.1 25 | mesh.vertex_attributes(v, "xyz", vertex) 26 | 27 | obj1.transformation = Translation.from_vector([random() - 0.5, random() - 0.5, random() - 0.5]) 28 | obj3.transformation *= Translation.from_euler_angles([0.05, 0, 0]) 29 | 30 | # Three objects share the same geometry data, but updated differently 31 | # obj1 should move around and deform 32 | obj1.update(update_transform=True, update_data=True) 33 | 34 | # obj2 will not move but will deform 35 | obj2.update(update_transform=False, update_data=True) 36 | 37 | # obj3 will rotate but not deform 38 | obj3.update(update_transform=True, update_data=False) 39 | 40 | 41 | viewer.show() 42 | -------------------------------------------------------------------------------- /scripts/dynamic_scene.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Box 2 | from compas_viewer import Viewer 3 | from compas.colors import Color 4 | 5 | viewer = Viewer() 6 | 7 | boxes = [ 8 | Box.from_width_height_depth(1, 1, 1), 9 | Box.from_width_height_depth(2, 2, 0.5), 10 | Box.from_width_height_depth(3, 3, 0.2), 11 | ] 12 | 13 | obj1 = viewer.scene.add(boxes[0], facecolor=Color.blue()) 14 | 15 | objects_to_remove = [obj1] 16 | 17 | 18 | @viewer.on(interval=1000) 19 | def dynamic_update(frame): 20 | """ 21 | This function is called every 1000ms for 6 frames. 22 | """ 23 | print(f"Frame {frame}") 24 | 25 | if frame == 1: 26 | print("Adding second box...") 27 | added_object = viewer.scene.add(boxes[1], facecolor=Color.red()) 28 | objects_to_remove.append(added_object) 29 | 30 | if frame == 2: 31 | print("Adding third box...") 32 | added_object = viewer.scene.add(boxes[2], facecolor=Color.green()) 33 | objects_to_remove.append(added_object) 34 | 35 | if frame == 4: 36 | print("Removing first box...") 37 | viewer.scene.remove(objects_to_remove.pop(-1)) 38 | 39 | if frame == 5: 40 | print("Removing second box...") 41 | viewer.scene.remove(objects_to_remove.pop(-1)) 42 | 43 | if frame == 6: 44 | print("Removing third box...") 45 | viewer.scene.remove(objects_to_remove.pop(-1)) 46 | 47 | viewer.show() 48 | -------------------------------------------------------------------------------- /scripts/example.py: -------------------------------------------------------------------------------- 1 | import compas 2 | from compas.colors import Color 3 | from compas.datastructures import Mesh 4 | from compas.geometry import Box 5 | from compas.geometry import Frame 6 | from random import random 7 | from compas_viewer.viewer import Viewer 8 | 9 | viewer = Viewer(show_grid=True) 10 | 11 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 12 | obj = viewer.scene.add(mesh, show_points=True) 13 | 14 | N = 10 15 | M = 10 16 | 17 | for i in range(N): 18 | for j in range(M): 19 | viewer.scene.add( 20 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 21 | linecolor=Color.from_i(random()), 22 | facecolor=Color(i / N, j / M, 0.0), 23 | name=f"Box_{i}_{j}", 24 | linewidth=3 * random(), 25 | show_points=True, 26 | pointcolor=Color.from_i(random()), 27 | pointsize=10 28 | ) 29 | 30 | viewer.show() 31 | -------------------------------------------------------------------------------- /scripts/group.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Box 2 | from compas_viewer import Viewer 3 | from compas_viewer.scene import Tag 4 | 5 | box1 = Box.from_width_height_depth(5, 1, 1) 6 | box2 = Box.from_width_height_depth(1, 5, 1) 7 | t = Tag("EN", (5, 0, 0), height=50) 8 | 9 | 10 | viewer = Viewer() 11 | 12 | # Just fix API for groups 13 | group1 = viewer.scene.add_group(name="group1") 14 | group1.add(box1, name="box1") 15 | group1.add(box2, name="box2") 16 | group1.add(t, name="tag") 17 | viewer.show() 18 | -------------------------------------------------------------------------------- /scripts/model.py: -------------------------------------------------------------------------------- 1 | from compas_assembly.geometry import Arch 2 | from compas_model.elements import BlockElement 3 | from compas_model.models import Model 4 | 5 | from compas.colors import Color 6 | from compas.scene import register 7 | from compas.scene import register_scene_objects 8 | from compas_viewer import Viewer 9 | from compas_viewer.scene import GroupObject 10 | 11 | 12 | class ModelObject(GroupObject): 13 | def __init__(self, **kwargs): 14 | 15 | model = kwargs.pop("item") 16 | 17 | elements = [] 18 | 19 | for element in model.elements(): 20 | element: BlockElement 21 | 22 | if element.is_support: 23 | color: Color = Color.red().lightened(50) 24 | show_faces = True 25 | else: 26 | color = Color(0.8, 0.8, 0.8) 27 | show_faces = False 28 | 29 | elements.append( 30 | ( 31 | element.geometry, 32 | { 33 | "show_faces": show_faces, 34 | "surfacecolor": color, 35 | "linecolor": color.contrast, 36 | "show_points": False, 37 | }, 38 | ) 39 | ) 40 | 41 | blocks = (elements, {"name": "blocks"}) 42 | interfaces = ([], {"name": "interfaces"}) 43 | forces = ([], {"name": "forces"}) 44 | super().__init__(item=[blocks, interfaces, forces], name=model.name, **kwargs) 45 | 46 | 47 | register_scene_objects() # This has to be called before registering the model object 48 | register(Model, ModelObject, context="Viewer") 49 | 50 | 51 | template = Arch(rise=3, span=10, thickness=0.3, depth=0.5, n=30) 52 | 53 | model = Model() 54 | 55 | for block in template.blocks(): 56 | model.add_element(BlockElement(shape=block)) 57 | 58 | viewer = Viewer() 59 | 60 | 61 | viewer.scene.add(model) 62 | 63 | viewer.show() 64 | -------------------------------------------------------------------------------- /scripts/nurbscurve.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import NurbsCurve 2 | from compas_viewer.viewer import Viewer 3 | 4 | curve = NurbsCurve.from_points([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0], [4, 0, 0]], degree=3) 5 | 6 | viewer = Viewer() 7 | viewer.scene.add(curve) 8 | viewer.show() -------------------------------------------------------------------------------- /scripts/plane.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Plane 2 | from compas_viewer import Viewer 3 | 4 | viewer = Viewer() 5 | 6 | plane = Plane([0, 0, 1], [0, 1, 1]) 7 | 8 | viewer.scene.add(plane, show_points=True) 9 | viewer.show() -------------------------------------------------------------------------------- /scripts/polygon.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Polygon 2 | from compas_viewer.viewer import Viewer 3 | 4 | viewer = Viewer() 5 | polygon = Polygon([[0, 0, 0], [0.5, 0.5, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]) 6 | viewer.scene.add(polygon) 7 | viewer.show() 8 | -------------------------------------------------------------------------------- /scripts/polyhedron.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Polyhedron 2 | from compas_viewer.viewer import Viewer 3 | from compas.geometry import Translation 4 | 5 | viewer = Viewer() 6 | 7 | p1 = Polyhedron.from_platonicsolid(4) 8 | viewer.scene.add(p1) 9 | 10 | p2 = Polyhedron.from_platonicsolid(6) 11 | p2.transform(Translation.from_vector([5, 0, 0])) 12 | viewer.scene.add(p2) 13 | 14 | p3 = Polyhedron.from_platonicsolid(8) 15 | p3.transform(Translation.from_vector([10, 0, 0])) 16 | viewer.scene.add(p3) 17 | 18 | p4 = Polyhedron.from_platonicsolid(12) 19 | p4.transform(Translation.from_vector([15, 0, 0])) 20 | viewer.scene.add(p4) 21 | 22 | p5 = Polyhedron.from_platonicsolid(20) 23 | p5.transform(Translation.from_vector([20, 0, 0])) 24 | viewer.scene.add(p5) 25 | 26 | viewer.show() 27 | -------------------------------------------------------------------------------- /scripts/robot.py: -------------------------------------------------------------------------------- 1 | from compas_robots import RobotModel 2 | from compas_robots.resources import GithubPackageMeshLoader 3 | from compas_robots.viewer.scene.robotmodelobject import RobotModelObject 4 | from compas_viewer.components import Slider 5 | from compas_viewer import Viewer 6 | 7 | viewer = Viewer() 8 | viewer.renderer.rendermode="lighted" 9 | 10 | github = GithubPackageMeshLoader("ros-industrial/abb", "abb_irb6600_support", "kinetic-devel") 11 | model = RobotModel.from_urdf_file(github.load_urdf("irb6640.urdf")) 12 | model.load_geometry(github) 13 | 14 | configuration = model.zero_configuration() 15 | robot_object: RobotModelObject = viewer.scene.add(model, show_lines=False, configuration=configuration) # type: ignore 16 | 17 | viewer.ui.sidedock.show = True 18 | 19 | def make_rotate_function(index): 20 | def rotate(slider: Slider, value: int): 21 | config = robot_object.configuration 22 | config.joint_values[index] = value / 360 * 2 * 3.14159 23 | robot_object.update_joints(config) 24 | return rotate 25 | 26 | for i, joint in enumerate(robot_object.configuration.joint_names): 27 | rotate_function = make_rotate_function(i) 28 | viewer.ui.sidedock.add(Slider(title=joint, starting_val=0, min_val=-180, max_val=180, step=1, action=rotate_function)) 29 | 30 | 31 | robot_object.update_joints(robot_object.configuration) 32 | 33 | viewer.show() 34 | -------------------------------------------------------------------------------- /scripts/scene.py: -------------------------------------------------------------------------------- 1 | import compas 2 | from compas.colors import Color 3 | from compas.datastructures import Mesh 4 | from compas.geometry import Box 5 | from compas.geometry import Frame 6 | from compas_viewer.viewer import Viewer 7 | from compas.data import json_dump 8 | from compas.data import json_load 9 | from compas.scene import Scene 10 | 11 | 12 | viewer = Viewer() 13 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 14 | obj = viewer.scene.add(mesh, show_points=True, facecolor=Color.blue(), linecolor=Color.red(), pointcolor=Color.green()) 15 | for i in range(5): 16 | for j in range(5): 17 | viewer.scene.add( 18 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 19 | show_points=True, 20 | linecolor=Color.white(), 21 | pointcolor=Color.black(), 22 | pointsize=10, 23 | facecolor=Color(i / 10, j / 10, 0.0), 24 | name=f"Box_{i}_{j}", 25 | ) 26 | 27 | # Save a scene to a file 28 | json_dump(viewer.scene, "temp/scene.json") 29 | viewer.show() 30 | 31 | 32 | # Load a scene from a file 33 | viewer.scene = json_load("temp/scene.json") 34 | viewer.show() 35 | 36 | 37 | 38 | # Using a generic scene 39 | # Note the generic scene can be more limited in visualization options 40 | # depending on what's available in the compas.scene.SceneObject classes 41 | 42 | scene = Scene() 43 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 44 | scene.add(mesh) 45 | for i in range(5): 46 | for j in range(5): 47 | scene.add( 48 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 49 | name=f"Box_{i}_{j}", 50 | ) 51 | 52 | viewer = Viewer() 53 | viewer.scene = scene 54 | viewer.show() 55 | -------------------------------------------------------------------------------- /scripts/sidedock.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Box 2 | from compas.geometry import Translation 3 | from compas_viewer import Viewer 4 | from compas_viewer.components import Button 5 | from compas_viewer.components import Slider 6 | 7 | box = Box(1) 8 | 9 | viewer = Viewer() 10 | 11 | boxobj = viewer.scene.add(box) 12 | 13 | import time 14 | 15 | 16 | def toggle_box(): 17 | boxobj.show = not boxobj.show 18 | viewer.renderer.update() 19 | 20 | 21 | def slider_changed1(slider: Slider, value: float): 22 | global viewer 23 | global boxobj 24 | 25 | boxobj.transformation = Translation.from_vector([5 * value, 0, 0]) 26 | boxobj.update() 27 | viewer.renderer.update() 28 | 29 | def slider_changed2(slider: Slider, value: float): 30 | global boxobj 31 | 32 | boxobj.update() 33 | viewer.renderer.update() 34 | 35 | viewer.ui.sidedock.show = True 36 | viewer.ui.sidedock.add(Button(text="Toggle Box", action=toggle_box)) 37 | viewer.ui.sidedock.add(Slider(title="Move Box", min_val=0, max_val=2, step=0.2, action=slider_changed1)) 38 | viewer.ui.sidedock.add(Slider(title="Box Opacity", obj=boxobj, attr="opacity", min_val=0, max_val=1, step=0.1, action=slider_changed2)) 39 | 40 | viewer.show() 41 | -------------------------------------------------------------------------------- /scripts/tag.py: -------------------------------------------------------------------------------- 1 | from compas_viewer import Viewer 2 | from compas_viewer.scene import Tag 3 | 4 | t1 = Tag("Align to left", (0, 0, 0), height=50) # default align is left and bottom 5 | t2 = Tag("Align to center", (0, 5, 0), height=50, horizontal_align="center", vertical_align="center") 6 | t3 = Tag("Align to right", (0, 10, 0), height=50, horizontal_align="right", vertical_align="top") 7 | t4 = Tag("Absolute height", (5, 0, 0), absolute_height=True, height=100) 8 | 9 | viewer = Viewer() 10 | viewer.scene.add(t1) 11 | viewer.scene.add(t2) 12 | viewer.scene.add(t3) 13 | viewer.scene.add(t4) 14 | viewer.show() 15 | -------------------------------------------------------------------------------- /scripts/treeform.py: -------------------------------------------------------------------------------- 1 | import compas 2 | from compas.colors import Color 3 | from compas.datastructures import Mesh 4 | from compas.geometry import Box 5 | from compas.geometry import Frame 6 | from compas_viewer.viewer import Viewer 7 | from compas_viewer.components import Treeform 8 | 9 | 10 | viewer = Viewer() 11 | 12 | mesh = Mesh.from_off(compas.get("tubemesh.off")) 13 | obj = viewer.scene.add(mesh, show_points=True) 14 | 15 | N = 10 16 | M = 10 17 | 18 | for i in range(N): 19 | for j in range(M): 20 | viewer.scene.add( 21 | Box(0.5, 0.5, 0.5, Frame([i, j, 0], [1, 0, 0], [0, 1, 0])), 22 | linecolor=Color.white(), 23 | facecolor=Color(i / N, j / M, 0.0), 24 | name=f"Box_{i}_{j}", 25 | ) 26 | 27 | 28 | 29 | treeform = Treeform() 30 | viewer.ui.sidebar.add(treeform) 31 | 32 | def update_treeform(form, node): 33 | treeform.update_from_dict({"name": node.name, "objtype": node.__class__, "item": node.item, "settings": node.settings}) 34 | 35 | viewer.ui.sidebar.sceneform.action = update_treeform 36 | 37 | viewer.show() 38 | -------------------------------------------------------------------------------- /scripts/unit.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.geometry import Box 3 | from compas.geometry import Frame 4 | from compas_viewer.viewer import Viewer 5 | from compas_viewer.config import Config 6 | 7 | config = Config() 8 | config.unit = "mm" 9 | viewer = Viewer(config) 10 | 11 | for i in range(10): 12 | for j in range(10): 13 | viewer.scene.add( 14 | Box(500, 500, 500, Frame([i * 1000, j * 1000, 0], [1, 0, 0], [0, 1, 0])), 15 | show_lines=True, 16 | surfacecolor=Color(i / 10, j / 10, 0.0), 17 | name=f"Box_{i}_{j}", 18 | ) 19 | 20 | viewer.show() 21 | -------------------------------------------------------------------------------- /src/compas_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************************** 3 | compas_viewer 4 | ******************************************************************************** 5 | 6 | .. currentmodule:: compas_viewer 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | 13 | """ 14 | 15 | from __future__ import print_function 16 | 17 | import os 18 | 19 | 20 | __author__ = ["Li Chen"] 21 | __copyright__ = "COMPAS Association" 22 | __license__ = "MIT License" 23 | __email__ = "li.chen@arch.ethz.ch" 24 | __version__ = "2.0.1" 25 | 26 | 27 | HERE = os.path.dirname(__file__) 28 | 29 | HOME = os.path.abspath(os.path.join(HERE, "../../")) 30 | DATA = os.path.abspath(os.path.join(HOME, "data")) 31 | DOCS = os.path.abspath(os.path.join(HOME, "docs")) 32 | TEMP = os.path.abspath(os.path.join(HOME, "temp")) 33 | 34 | 35 | __all__ = ["HOME", "DATA", "DOCS", "TEMP"] 36 | 37 | __all_plugins__ = [ 38 | "compas_viewer.scene", 39 | ] 40 | 41 | from .viewer import Viewer # noqa: F401, E402 42 | -------------------------------------------------------------------------------- /src/compas_viewer/__main__.py: -------------------------------------------------------------------------------- 1 | # ========================================================================== 2 | # python -m compas_viewer -f "file path or directory path" 3 | # ========================================================================== 4 | 5 | import argparse 6 | from os import listdir 7 | from os import path 8 | 9 | from compas import json_load 10 | from compas import json_loadz 11 | from compas.scene.context import ITEM_SCENEOBJECT 12 | from compas_viewer import Viewer 13 | 14 | 15 | def validate_object(object): 16 | """Validate if the object is supported by the viewer.""" 17 | 18 | return isinstance(object, tuple(ITEM_SCENEOBJECT["Viewer"].keys())) 19 | 20 | 21 | ap = argparse.ArgumentParser() 22 | ap.add_argument( 23 | "-f", 24 | "--file", 25 | required=False, 26 | help=""" 27 | The compas.geometry's JSON file, or the compressed JSON file (ZIP).""", 28 | ) 29 | 30 | ap.add_argument( 31 | "--files", 32 | required=False, 33 | help=""" 34 | The path to a folder containing the JSON files for the viewer to load.""", 35 | ) 36 | 37 | args = vars(ap.parse_args()) 38 | 39 | 40 | # ========================================================================== 41 | # "-f" argument 42 | # ========================================================================== 43 | 44 | _geos = [] 45 | if args["file"]: 46 | if args["file"].endswith(".json"): 47 | _geos.append(json_load(args["file"])) 48 | elif args["file"].endswith(".zip"): 49 | _geos.append(json_loadz(args["file"])) 50 | else: 51 | raise ValueError(f"The file {args['file']} is not a JSON file or a compressed JSON file.") 52 | 53 | # ========================================================================== 54 | # "--files" argument 55 | # ========================================================================== 56 | 57 | if args["files"]: 58 | for file in listdir(args["files"]): 59 | if file.endswith(".json"): 60 | _geos.append(json_load(path.join(args["files"], file))) 61 | elif file.endswith(".zip"): 62 | _geos.append(json_loadz(path.join(args["files"], file))) 63 | else: 64 | print(f"The file {file} in the folder {args['files']} is not a JSON file or a compressed JSON file.") 65 | 66 | 67 | # ========================================================================== 68 | # Validate Geometries 69 | # ========================================================================== 70 | 71 | # Viewer needs to be launched for validating the geometries. 72 | viewer = Viewer() 73 | 74 | geometries = {} 75 | for _geo in _geos: 76 | if isinstance(_geo, list): 77 | for g in _geo: 78 | if validate_object(g): 79 | geometries[g] = g 80 | else: 81 | print(f"{g} is not supported by the viewer.") 82 | if isinstance(_geo, dict): 83 | for k, g in _geo.items(): 84 | if validate_object(g): 85 | geometries[k] = g 86 | else: 87 | print(f"{g} is not supported by the viewer.") 88 | if validate_object(_geo): 89 | geometries[_geo] = _geo 90 | else: 91 | print(f"{_geo} is not supported by the viewer.") 92 | 93 | # ========================================================================== 94 | # Launch 95 | # ========================================================================== 96 | 97 | print("Welcome to COMPAS Viewer!") 98 | print("Check out our page for more tutorials, documentations and api references: https://compas.dev/compas_viewer") 99 | 100 | 101 | for name, geometry in geometries.items(): 102 | viewer.scene.add(item=geometry, name=name) 103 | 104 | viewer.show() 105 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/fonts/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/src/compas_viewer/assets/fonts/FreeSans.ttf -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/camera_info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/compas_icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_viewer/HEAD/src/compas_viewer/assets/icons/compas_icon_white.png -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/delete_selected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/export_file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/gl_info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/import_file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/selection_info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/view_front.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/view_perspective.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/view_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/view_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/assets/icons/zoom_selected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/compas_viewer/base.py: -------------------------------------------------------------------------------- 1 | class Base: 2 | """ 3 | Base class for all components in the viewer, provides a global access to the viewer and scene. 4 | 5 | Attributes 6 | ---------- 7 | viewer : Viewer 8 | The viewer instance. 9 | scene : Scene 10 | The scene instance. 11 | """ 12 | 13 | @property 14 | def viewer(self): 15 | from compas_viewer.viewer import Viewer 16 | 17 | return Viewer() 18 | 19 | @property 20 | def scene(self): 21 | return self.viewer.scene 22 | -------------------------------------------------------------------------------- /src/compas_viewer/components/__init__.py: -------------------------------------------------------------------------------- 1 | from .button import Button 2 | from .camerasetting import CameraSetting 3 | from .slider import Slider 4 | from .textedit import TextEdit 5 | from .treeform import Treeform 6 | from .sceneform import Sceneform 7 | from .objectsetting import ObjectSetting 8 | from .tabform import Tabform 9 | from .component import Component 10 | 11 | __all__ = [ 12 | "Button", 13 | "CameraSetting", 14 | "Renderer", 15 | "Slider", 16 | "TextEdit", 17 | "Treeform", 18 | "Sceneform", 19 | "ObjectSetting", 20 | "Tabform", 21 | "Component", 22 | ] 23 | -------------------------------------------------------------------------------- /src/compas_viewer/components/booleantoggle.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | from typing import Union 3 | 4 | from PySide6.QtWidgets import QCheckBox 5 | from PySide6.QtWidgets import QHBoxLayout 6 | from PySide6.QtWidgets import QLabel 7 | from PySide6.QtWidgets import QWidget 8 | 9 | from .boundcomponent import BoundComponent 10 | from .component import Component 11 | 12 | 13 | class BooleanToggle(BoundComponent): 14 | """ 15 | This component creates a labeled checkbox that can be bound to an object's boolean attribute 16 | (either a dictionary key or object attribute). When the checkbox state changes, it automatically 17 | updates the bound attribute and optionally calls a action function. 18 | 19 | Parameters 20 | ---------- 21 | obj : Union[object, dict] 22 | The object or dictionary containing the boolean attribute to be edited. 23 | attr : str 24 | The name of the attribute/key to be edited. 25 | title : str, optional 26 | The label text to be displayed next to the checkbox. If None, uses the attr name. 27 | action : Callable[[Component, bool], None], optional 28 | A function to call when the checkbox state changes. Receives the component and new boolean value. 29 | 30 | Attributes 31 | ---------- 32 | obj : Union[object, dict] 33 | The object or dictionary containing the boolean attribute being edited. 34 | attr : str 35 | The name of the attribute/key being edited. 36 | action : Callable[[Component, bool], None] or None 37 | The action function to call when the checkbox state changes. 38 | widget : QWidget 39 | The main widget containing the layout. 40 | layout : QHBoxLayout 41 | The horizontal layout containing the label and the checkbox. 42 | label : QLabel 43 | The label displaying the title. 44 | checkbox : QCheckBox 45 | The checkbox widget for toggling the boolean value. 46 | 47 | Example 48 | ------- 49 | >>> class MyObject: 50 | ... def __init__(self): 51 | ... self.show_points = True 52 | >>> obj = MyObject() 53 | >>> component = BooleanToggle(obj, "show_points", title="Show Points") 54 | """ 55 | 56 | def __init__( 57 | self, 58 | obj: Union[object, dict], 59 | attr: str, 60 | title: str = None, 61 | action: Callable[[Component, bool], None] = None, 62 | ): 63 | super().__init__(obj, attr, action=action) 64 | 65 | self.widget = QWidget() 66 | self.layout = QHBoxLayout() 67 | 68 | title = title if title is not None else attr 69 | self.label = QLabel(title) 70 | self.checkbox = QCheckBox() 71 | self.checkbox.setMaximumSize(85, 25) 72 | 73 | # Set the initial state from the bound attribute 74 | initial_value = self.get_attr() 75 | if not isinstance(initial_value, bool): 76 | raise ValueError(f"Attribute '{attr}' must be a boolean value, got {type(initial_value)}") 77 | self.checkbox.setChecked(initial_value) 78 | 79 | self.layout.addWidget(self.label) 80 | self.layout.addWidget(self.checkbox) 81 | self.widget.setLayout(self.layout) 82 | 83 | # Connect the checkbox state change signal to the action 84 | self.checkbox.stateChanged.connect(self.on_state_changed) 85 | 86 | def on_state_changed(self, state): 87 | """Handle checkbox state change events by updating the bound attribute and calling the action.""" 88 | # Convert Qt checkbox state to boolean 89 | is_checked = state == 2 # Qt.Checked = 2 90 | self.set_attr(is_checked) 91 | if self.action: 92 | self.action(self, is_checked) 93 | -------------------------------------------------------------------------------- /src/compas_viewer/components/button.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import Callable 3 | from typing import Optional 4 | from typing import Union 5 | 6 | from PySide6 import QtCore 7 | from PySide6 import QtGui 8 | from PySide6 import QtWidgets 9 | 10 | from .component import Component 11 | 12 | 13 | def set_icon_path(icon_name: str) -> str: 14 | path = QtGui.QIcon(str(pathlib.Path(__file__).parent.parent / "assets" / "icons" / icon_name)) 15 | return path 16 | 17 | 18 | class Button(Component): 19 | """ 20 | This component creates a button widget that can be customized with text, icon, tooltip, and action. 21 | 22 | Parameters 23 | ---------- 24 | text : str, optional 25 | The text to display on the button. 26 | icon_path : Union[str, pathlib.Path], optional 27 | The path to the icon file to display on the button. 28 | tooltip : str, optional 29 | The tooltip text to show when hovering over the button. 30 | action : Callable[[], None], optional 31 | A function to call when the button is clicked. 32 | 33 | Attributes 34 | ---------- 35 | widget : QPushButton 36 | The push button widget. 37 | action : Callable[[], None] or None 38 | The callback function to call when the button is clicked. 39 | 40 | Example 41 | ------- 42 | >>> def my_action(): 43 | ... print("Button clicked!") 44 | >>> component = Button(text="Click Me", action=my_action) 45 | """ 46 | 47 | def __init__( 48 | self, 49 | text: Optional[str] = None, 50 | icon_path: Optional[Union[str, pathlib.Path]] = None, 51 | tooltip: Optional[str] = None, 52 | action: Optional[Callable[[], None]] = None, 53 | ): 54 | super().__init__() 55 | 56 | self.action = action 57 | self.widget = QtWidgets.QPushButton() 58 | 59 | if text: 60 | self.widget.setText(text) 61 | if icon_path: 62 | self.widget.setIcon(QtGui.QIcon(set_icon_path(icon_path))) 63 | self.widget.setIconSize(QtCore.QSize(17, 17)) 64 | if tooltip: 65 | self.widget.setToolTip(tooltip) 66 | if action: 67 | self.widget.clicked.connect(action) 68 | -------------------------------------------------------------------------------- /src/compas_viewer/components/component.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget 2 | 3 | from compas_viewer.base import Base 4 | 5 | 6 | class Component(Base): 7 | """A base class for all UI components in the viewer. 8 | 9 | Attributes 10 | ---------- 11 | widget : QWidget 12 | The main widget that contains all child components. 13 | 14 | """ 15 | 16 | def __init__(self): 17 | super().__init__() 18 | self.widget = QWidget() 19 | self._show = True 20 | 21 | def update(self): 22 | self.widget.update() 23 | 24 | @property 25 | def show(self): 26 | return self._show 27 | 28 | @show.setter 29 | def show(self, value: bool): 30 | self._show = value 31 | self.widget.setVisible(value) 32 | -------------------------------------------------------------------------------- /src/compas_viewer/components/mainwindow.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMainWindow 2 | 3 | from compas_viewer.components.component import Component 4 | 5 | 6 | class MainWindow(Component): 7 | def __init__(self): 8 | super().__init__() 9 | self.widget = QMainWindow() 10 | self.title = self.viewer.config.window.title 11 | 12 | @property 13 | def title(self): 14 | return self.widget.windowTitle() 15 | 16 | @title.setter 17 | def title(self, title: str): 18 | self._title = title 19 | self.widget.setWindowTitle(title) 20 | 21 | def resize(self, w: int, h: int) -> None: 22 | self.widget.resize(w, h) 23 | rect = self.viewer.app.primaryScreen().availableGeometry() 24 | x = 0.5 * (rect.width() - w) 25 | y = 0.5 * (rect.height() - h) 26 | self.widget.setGeometry(x, y, w, h) 27 | -------------------------------------------------------------------------------- /src/compas_viewer/components/menubar.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Any 3 | from typing import Callable 4 | from typing import Optional 5 | 6 | from PySide6.QtGui import QAction 7 | from PySide6.QtGui import QActionGroup 8 | from PySide6.QtWidgets import QWidget 9 | 10 | from compas_viewer.commands import Command 11 | from compas_viewer.components.component import Component 12 | 13 | from .mainwindow import MainWindow 14 | 15 | 16 | class MenuBar(Component): 17 | def __init__(self, window: MainWindow) -> None: 18 | super().__init__() 19 | self.widget = window.widget.menuBar() 20 | self.load_items() 21 | 22 | @property 23 | def items(self): 24 | return self.viewer.config.ui.menubar.items 25 | 26 | def load_items(self, items=None, parent=None) -> list[QAction]: 27 | items = items or self.items 28 | parent = parent or self.widget 29 | 30 | actions = [] 31 | 32 | for item in items: 33 | text = item.get("title", None) 34 | itemtype = item.get("type", None) 35 | action = item.get("action", None) 36 | args = item.get("args") or [] 37 | kwargs = item.get("kwargs") or {} 38 | 39 | if itemtype == "separator": 40 | parent.addSeparator() 41 | 42 | elif action: 43 | a = self.add_action(text=text, action=action, parent=parent, args=args, kwargs=kwargs) 44 | actions.append(a) 45 | 46 | if itemtype == "checkbox": 47 | state = item.get("checked", False) 48 | a.setCheckable(True) 49 | a.setChecked(state if not callable(state) else state(self.viewer)) 50 | 51 | if isinstance(action, Command): 52 | if action.keybinding is not None: 53 | a.setShortcut(action.keybinding) 54 | 55 | else: 56 | if items := item.get("items"): 57 | if not itemtype or itemtype == "menu": 58 | menu = parent.addMenu(text) 59 | self.load_items(items=items, parent=menu) 60 | 61 | elif itemtype == "group": 62 | group = QActionGroup(self.widget) 63 | group.setExclusive(item.get("exclusive", True)) 64 | 65 | menu = parent.addMenu(text) 66 | for i, a in enumerate(self.load_items(items=items, parent=menu)): 67 | a.setCheckable(True) 68 | if i == item.get("selected", 0): 69 | a.setChecked(True) 70 | group.addAction(a) 71 | 72 | else: 73 | raise NotImplementedError 74 | 75 | else: 76 | menu = parent.addMenu(text) 77 | self.load_items(items=[{"title": "PLACEHOLDER", "action": lambda: print("PLACEHOLDER")}], parent=menu) 78 | 79 | return actions 80 | 81 | def add_action( 82 | self, 83 | *, 84 | text: str, 85 | action: Callable, 86 | parent: QWidget, 87 | args: Optional[list[Any]] = None, 88 | kwargs: Optional[dict] = None, 89 | icon: Optional[str] = None, 90 | ): 91 | args = args or [] 92 | kwargs = kwargs or {} 93 | if icon: 94 | raise NotImplementedError 95 | return parent.addAction(text, partial(action, *args, **kwargs)) 96 | -------------------------------------------------------------------------------- /src/compas_viewer/components/objectsetting.py: -------------------------------------------------------------------------------- 1 | from compas_viewer.scene import ViewerSceneObject 2 | 3 | from .booleantoggle import BooleanToggle 4 | from .colorpicker import ColorPicker 5 | from .container import Container 6 | from .numberedit import NumberEdit 7 | from .textedit import TextEdit 8 | 9 | 10 | class ObjectSetting(Container): 11 | """ 12 | A component to manage the settings of objects in the viewer. 13 | """ 14 | 15 | def __init__(self): 16 | super().__init__(container_type="scrollable") 17 | 18 | @property 19 | def selected(self): 20 | return [obj for obj in self.scene.objects if obj.is_selected] 21 | 22 | def update(self): 23 | """Update the layout with the latest object settings.""" 24 | if len(self.selected) == 1: 25 | self.populate(self.selected[0]) 26 | elif len(self.selected) > 1: 27 | self.display_text("Multiple objects selected") 28 | else: 29 | self.display_text("No object selected") 30 | 31 | def populate(self, obj: ViewerSceneObject) -> None: 32 | """Populate the layout with the settings of the selected object.""" 33 | 34 | self.reset() 35 | 36 | def _update_obj_settings(*arg): 37 | obj.update() 38 | self.viewer.renderer.update() 39 | 40 | def _update_obj_settings(*arg): 41 | obj.update(update_data=True) 42 | self.viewer.renderer.update() 43 | 44 | def _update_sceneform(*arg): 45 | self.viewer.ui.sidebar.sceneform.update(refresh=True) 46 | 47 | if hasattr(obj, "name"): 48 | name_edit = TextEdit(obj, "name", action=_update_sceneform) 49 | self.add(name_edit) 50 | 51 | if hasattr(obj, "show_points"): 52 | self.add(BooleanToggle(obj=obj, attr="show_points", action=_update_obj_settings)) 53 | 54 | if hasattr(obj, "show_lines"): 55 | self.add(BooleanToggle(obj=obj, attr="show_lines", action=_update_obj_settings)) 56 | 57 | if hasattr(obj, "show_faces"): 58 | self.add(BooleanToggle(obj=obj, attr="show_faces", action=_update_obj_settings)) 59 | 60 | if hasattr(obj, "pointcolor") and obj.pointcolor is not None: 61 | self.add(ColorPicker(obj=obj, attr="pointcolor", action=_update_obj_settings)) 62 | 63 | if hasattr(obj, "linecolor") and obj.linecolor is not None: 64 | self.add(ColorPicker(obj=obj, attr="linecolor", action=_update_obj_settings)) 65 | 66 | if hasattr(obj, "facecolor") and obj.facecolor is not None: 67 | self.add(ColorPicker(obj=obj, attr="facecolor", action=_update_obj_settings)) 68 | 69 | if hasattr(obj, "linewidth"): 70 | linewidth_edit = NumberEdit(obj, "linewidth", title="line width", min_val=0.0, max_val=10.0, action=_update_obj_settings) 71 | self.add(linewidth_edit) 72 | 73 | if hasattr(obj, "pointsize"): 74 | pointsize_edit = NumberEdit(obj, "pointsize", title="point size", min_val=0.0, max_val=10.0, action=_update_obj_settings) 75 | self.add(pointsize_edit) 76 | 77 | if hasattr(obj, "opacity"): 78 | opacity_edit = NumberEdit(obj, "opacity", title="opacity", min_val=0.0, max_val=1.0, action=_update_obj_settings) 79 | self.add(opacity_edit) 80 | -------------------------------------------------------------------------------- /src/compas_viewer/components/sidebar.py: -------------------------------------------------------------------------------- 1 | from compas_viewer.components import Sceneform 2 | from compas_viewer.components.camerasetting import CameraSetting 3 | from compas_viewer.components.container import Container 4 | from compas_viewer.components.objectsetting import ObjectSetting 5 | from compas_viewer.components.tabform import Tabform 6 | 7 | 8 | class SideBarRight(Container): 9 | """Sidebar for the right side of the window, containing commonly used forms like sceneform and objectsetting. 10 | 11 | Parameters 12 | ---------- 13 | items : list[dict[str, Callable]] 14 | List of items to be added to the sidebar. 15 | 16 | Attributes 17 | ---------- 18 | sceneform : Sceneform 19 | Sceneform component, if it is in the items list. 20 | tabform : Tabform 21 | TabForm component containing ObjectSetting and CameraSetting, if they are in the items list. 22 | object_setting : ObjectSetting 23 | ObjectSetting component nested within the TabForm, if it is in the items list. 24 | camera_setting : CameraSetting 25 | CameraSetting component nested within the TabForm, if it is in the items list. 26 | 27 | """ 28 | 29 | def __init__(self) -> None: 30 | super().__init__(container_type="splitter") 31 | self.tabform = Tabform(tab_position="top") 32 | self.load_items() 33 | 34 | @property 35 | def items(self): 36 | return self.viewer.config.ui.sidebar.items 37 | 38 | def load_items(self): 39 | tabform_added = False 40 | 41 | for item in self.items: 42 | itemtype = item.get("type", None) 43 | 44 | if itemtype == "Sceneform": 45 | columns = item.get("columns", None) 46 | if columns is None: 47 | raise ValueError("Please setup config for Sceneform") 48 | self.sceneform = Sceneform(columns=columns) 49 | self.add(self.sceneform) 50 | 51 | if itemtype == "ObjectSetting": 52 | self.object_setting = ObjectSetting() 53 | self.tabform.add_tab("Object", container=self.object_setting) 54 | if not tabform_added: 55 | self.add(self.tabform) 56 | tabform_added = True 57 | 58 | if itemtype == "CameraSetting": 59 | self.camera_setting = CameraSetting() 60 | self.tabform.add_tab("Camera", container=self.camera_setting) 61 | if not tabform_added: 62 | self.add(self.tabform) 63 | tabform_added = True 64 | -------------------------------------------------------------------------------- /src/compas_viewer/components/sidedock.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtCore 2 | from PySide6 import QtWidgets 3 | from PySide6.QtWidgets import QDockWidget 4 | 5 | from compas_viewer.components.container import Container 6 | 7 | from .mainwindow import MainWindow 8 | 9 | 10 | class SideDock(Container): 11 | locations = { 12 | "left": QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, 13 | "right": QtCore.Qt.DockWidgetArea.RightDockWidgetArea, 14 | } 15 | 16 | def __init__(self, window: MainWindow) -> None: 17 | super().__init__(container_type="scrollable") 18 | self.widget = QDockWidget() 19 | self.widget.setMinimumWidth(200) 20 | self.scroll = QtWidgets.QScrollArea() 21 | self.scroll.setStyleSheet("QScrollArea { border: none; }") 22 | self.widget.setWidget(self.scroll) 23 | content = QtWidgets.QWidget() 24 | self.scroll.setWidget(content) 25 | self.scroll.setWidgetResizable(True) 26 | self.layout = QtWidgets.QVBoxLayout(content) 27 | self.layout.setAlignment(QtCore.Qt.AlignTop) 28 | self.widget.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea | QtCore.Qt.DockWidgetArea.RightDockWidgetArea) 29 | self.widget.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetFloatable | QDockWidget.DockWidgetFeature.DockWidgetMovable) 30 | window.widget.addDockWidget(self.locations["left"], self.widget) 31 | -------------------------------------------------------------------------------- /src/compas_viewer/components/statusbar.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel 2 | 3 | from compas_viewer.components.component import Component 4 | 5 | from .mainwindow import MainWindow 6 | 7 | 8 | class StatusBar(Component): 9 | def __init__(self, window: MainWindow) -> None: 10 | super().__init__() 11 | self.widget = window.widget.statusBar() 12 | self.widget.addWidget(QLabel(text="Ready...")) 13 | -------------------------------------------------------------------------------- /src/compas_viewer/components/textedit.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | from typing import Union 3 | 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtWidgets import QHBoxLayout 6 | from PySide6.QtWidgets import QLabel 7 | from PySide6.QtWidgets import QSizePolicy 8 | from PySide6.QtWidgets import QTextEdit 9 | from PySide6.QtWidgets import QWidget 10 | 11 | from .boundcomponent import BoundComponent 12 | from .component import Component 13 | 14 | 15 | class TextEdit(BoundComponent): 16 | """ 17 | This component creates a labeled text edit widget that can be bound to an object's attribute 18 | (either a dictionary key or object attribute). When the text changes, it automatically 19 | updates the bound attribute and optionally calls a action function. 20 | 21 | Parameters 22 | ---------- 23 | obj : Union[object, dict] 24 | The object or dictionary containing the attribute to be edited. 25 | attr : str 26 | The name of the attribute/key to be edited. 27 | title : str, optional 28 | The label text to be displayed next to the text edit. If None, uses the attr name. 29 | action : Callable[[Component, str], None], optional 30 | A function to call when the text changes. Receives the component and new text value. 31 | 32 | Attributes 33 | ---------- 34 | obj : Union[object, dict] 35 | The object or dictionary containing the attribute being edited. 36 | attr : str 37 | The name of the attribute/key being edited. 38 | action : Callable[[Component, str], None] or None 39 | The action function to call when the text changes. 40 | widget : QWidget 41 | The main widget containing the layout. 42 | layout : QHBoxLayout 43 | The horizontal layout containing the label and the text edit. 44 | label : QLabel 45 | The label displaying the title. 46 | text_edit : QTextEdit 47 | The text edit widget for editing the text. 48 | 49 | Example 50 | ------- 51 | >>> class MyObject: 52 | ... def __init__(self): 53 | ... self.name = "Hello World" 54 | >>> obj = MyObject() 55 | >>> component = TextEdit(obj, "name", title="Name") 56 | """ 57 | 58 | def __init__( 59 | self, 60 | obj: Union[object, dict], 61 | attr: str, 62 | title: str = None, 63 | action: Callable[[Component, str], None] = None, 64 | ): 65 | super().__init__(obj, attr, action=action) 66 | 67 | self.widget = QWidget() 68 | self.layout = QHBoxLayout() 69 | 70 | title = title if title is not None else attr 71 | self.label = QLabel(title) 72 | self.text_edit = QTextEdit() 73 | self.text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 74 | self.text_edit.setMaximumSize(85, 25) 75 | self.text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 76 | 77 | self.text_edit.setText(str(self.get_attr())) 78 | self.layout.addWidget(self.label) 79 | self.layout.addWidget(self.text_edit) 80 | self.widget.setLayout(self.layout) 81 | 82 | # Connect the text change signal to the action 83 | self.text_edit.textChanged.connect(self.on_text_changed) 84 | 85 | def on_text_changed(self): 86 | """Handle text change events by updating the bound attribute and calling the action.""" 87 | new_text = self.text_edit.toPlainText() 88 | self.on_value_changed(new_text) 89 | -------------------------------------------------------------------------------- /src/compas_viewer/components/toolbar.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Any 3 | from typing import Callable 4 | from typing import Optional 5 | 6 | from compas_viewer.components import Button 7 | from compas_viewer.components.component import Component 8 | 9 | from .mainwindow import MainWindow 10 | 11 | 12 | class ToolBar(Component): 13 | def __init__(self, window: MainWindow) -> None: 14 | super().__init__() 15 | self.widget = window.widget.addToolBar("Tools") 16 | self.widget.clear() 17 | self.widget.setMovable(False) 18 | self.widget.setObjectName("Tools") 19 | self.load_items() 20 | 21 | @property 22 | def items(self): 23 | return self.viewer.config.ui.toolbar.items 24 | 25 | def load_items(self): 26 | for item in self.items: 27 | text = item.get("title", None) 28 | tooltip = item.get("tooltip", None) 29 | itemtype = item.get("type", None) 30 | action = item.get("action", None) 31 | icon = item.get("icon", None) 32 | args = item.get("args") or [] 33 | kwargs = item.get("kwargs") or {} 34 | 35 | if itemtype == "separator": 36 | raise NotImplementedError 37 | elif itemtype == "button": 38 | self.add_action(tooltip=tooltip, icon=icon, action=action, args=args, kwargs=kwargs) 39 | elif itemtype == "action": 40 | self.add_action(text=text, action=action, args=args, kwargs=kwargs) 41 | # elif itemtype == "select": 42 | # self.add_combobox(item["items"], item["action"]) 43 | elif action: 44 | self.add_action(text=text, action=action, args=args, kwargs=kwargs) 45 | 46 | def add_action( 47 | self, 48 | *, 49 | tooltip: str, 50 | text: Optional[str] = None, 51 | action: Callable, 52 | args: Optional[list[Any]] = None, 53 | kwargs: Optional[dict] = None, 54 | icon: Optional[str] = None, 55 | ): 56 | args = args or [] 57 | kwargs = kwargs or {} 58 | return self.widget.addWidget(Button(text=text, tooltip=tooltip, icon_path=icon, action=partial(action, *args, **kwargs))) 59 | 60 | # def add_combobox(self, items, action, title=None): 61 | # combobox = QComboBox() 62 | # for item in items: 63 | # combobox.addItem(item["title"], item.get("value", item["title"])) 64 | # combobox.currentIndexChanged.connect(lambda index: action(combobox.itemData(index))) 65 | # self.widget.addWidget(combobox) 66 | -------------------------------------------------------------------------------- /src/compas_viewer/components/viewport.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QSplitter 2 | 3 | from compas_viewer.components.component import Component 4 | from compas_viewer.renderer import Renderer 5 | 6 | from .mainwindow import MainWindow 7 | from .sidebar import SideBarRight 8 | 9 | 10 | class ViewPort(Component): 11 | def __init__(self, window: MainWindow): 12 | super().__init__() 13 | self.widget = QSplitter() 14 | self.renderer = Renderer() 15 | self.widget.addWidget(self.renderer) 16 | self.sidebar = SideBarRight() 17 | self.widget.addWidget(self.sidebar.widget) 18 | self.widget.setSizes([800, 200]) 19 | window.widget.setCentralWidget(self.widget) 20 | 21 | self._unit = None 22 | self.unit = self.viewer.config.unit 23 | 24 | @property 25 | def unit(self): 26 | return self._unit 27 | 28 | @unit.setter 29 | def unit(self, unit: str): 30 | if self.viewer.running: 31 | raise NotImplementedError("Changing the unit after the viewer is running is not yet supported.") 32 | if unit != self._unit: 33 | previous_scale = self.viewer.config.camera.scale 34 | if unit == "m": 35 | self.viewer.config.renderer.gridsize = (10.0, 10, 10.0, 10) 36 | self.renderer.camera.scale = 1.0 37 | elif unit == "cm": 38 | self.viewer.config.renderer.gridsize = (1000.0, 10, 1000.0, 10) 39 | self.renderer.camera.scale = 100.0 40 | elif unit == "mm": 41 | self.viewer.config.renderer.gridsize = (10000.0, 10, 10000.0, 10) 42 | self.renderer.camera.scale = 1000.0 43 | else: 44 | raise ValueError(f"Invalid unit: {unit}. Valid units are 'm', 'cm', 'mm'.") 45 | self.renderer.camera.distance *= self.renderer.camera.scale / previous_scale 46 | 47 | self._unit = unit 48 | -------------------------------------------------------------------------------- /src/compas_viewer/mouse.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QPoint 2 | 3 | 4 | class Mouse: 5 | """ 6 | Class representing mouse actions and movements. 7 | 8 | Attributes 9 | ---------- 10 | pos : :QtCore:`QtCore.QPoint` 11 | The current position of the mouse on the screen. 12 | last_pos : :QtCore:`QtCore.QPoint` 13 | The last recorded position of the mouse on the screen. 14 | """ 15 | 16 | def __init__(self): 17 | self.pos = QPoint() 18 | self.last_pos = QPoint() 19 | self.window_start_point: QPoint = None 20 | self.window_end_point: QPoint = None 21 | self.is_tracing_a_window: bool = False 22 | 23 | def dx(self): 24 | return self.pos.x() - self.last_pos.x() 25 | 26 | def dy(self): 27 | return self.pos.y() - self.last_pos.y() 28 | -------------------------------------------------------------------------------- /src/compas_viewer/qt.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from typing import Union 3 | 4 | from PySide6.QtCore import Qt 5 | 6 | 7 | def key_mapper( 8 | key: str, 9 | type: Literal[0, 1, 2], 10 | ) -> Union[Qt.Key, Qt.KeyboardModifier, Qt.MouseButton]: 11 | """ 12 | Map the key string to the Qt key. 13 | 14 | Parameters 15 | ---------- 16 | key : str 17 | The key string which is the same as what it was called in the :QtCore:`PySide6.QtCore.Qt`, 18 | with **lowercases and with prefix&underscores removed**. 19 | Check out the reference page to find out the supported keys and the original names. 20 | type : Literal[0, 1, 2] 21 | The type of the key for mapping. 22 | 23 | * 0 for :QtCore:`PySide6.QtCore.Qt.Key`, 24 | * 1 for :QtCore:`PySide6.QtCore.Qt.KeyboardModifier`, 25 | * 2 for :QtCore:`PySide6.QtCore.Qt.MouseButton`. 26 | 27 | Returns 28 | ------- 29 | Literal[:QtCore:`PySide6.QtCore.Qt.Key`, :QtCore:`PySide6.QtCore.Qt.KeyboardModifier`, 30 | :QtCore:`PySide6.QtCore.Qt.MouseButton`] 31 | The mapped Qt key. 32 | 33 | Notes 34 | ----- 35 | This function provides the possibility to use all the supported keys in Qt for the viewer 36 | by mapping the key string to the Qt key. 37 | 38 | This function handles: 39 | 40 | * :QtCore:`PySide6.QtCore.Qt.Key` 41 | * :QtCore:`PySide6.QtCore.Qt.KeyboardModifier` 42 | * :QtCore:`PySide6.QtCore.Qt.MouseButton` 43 | 44 | Examples 45 | -------- 46 | >>> from compas_viewer.qt import key_mapper 47 | >>> key_mapper("a", 0) 48 | 49 | >>> key_mapper("h", 0) 50 | 51 | >>> key_mapper("f5", 0) 52 | 53 | >>> key_mapper("superl", 0) 54 | 55 | >>> key_mapper("bracketleft", 0) 56 | 57 | >>> key_mapper("colon", 0) 58 | 59 | >>> key_mapper("control", 1) 60 | 61 | >>> key_mapper("meta", 1) 62 | 63 | >>> key_mapper("left", 2) 64 | 65 | >>> key_mapper("middle", 2) 66 | 67 | >>> key_mapper("back", 2) 68 | 69 | >>> key_mapper("forward", 2) 70 | 71 | >>> key_mapper("task", 2) 72 | 73 | >>> key_mapper("extra4", 2) 74 | 75 | 76 | References 77 | ---------- 78 | * https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.Key 79 | * https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.KeyboardModifier 80 | * https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.MouseButton 81 | """ 82 | 83 | if type == 0: 84 | for v in Qt.Key: 85 | if v.name.replace("Key", "").replace("_", "").lower() == key: 86 | return Qt.Key(v.value) 87 | raise ValueError(f"Key mapping of {key} not found in Qt.Key. Check your typing?") 88 | 89 | elif type == 1: 90 | for v in Qt.KeyboardModifier: 91 | if v.name.replace("Modifier", "").lower() == key: 92 | return Qt.KeyboardModifier(v.value) 93 | if key == "no": 94 | return Qt.KeyboardModifier.NoModifier # Some times the no modifier is not recognized. 95 | raise ValueError(f"Key mapping of {key} not found in Qt.KeyboardModifier. Check your typing?") 96 | 97 | elif type == 2: 98 | for v in Qt.MouseButton: 99 | if v.name.replace("Button", "").lower() == key: 100 | return Qt.MouseButton(v.value) 101 | raise ValueError(f"Key mapping of {key} not found in Qt.MouseButton. Check your typing?") 102 | 103 | else: 104 | raise ValueError("The type should be 0, 1, or 2.") 105 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/__init__.py: -------------------------------------------------------------------------------- 1 | from .renderer import Renderer # noqa: F401 2 | from .camera import Camera # noqa: F401 3 | from .shaders.shader import Shader # noqa: F401 4 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/__init__.py: -------------------------------------------------------------------------------- 1 | from .shader import Shader # noqa: F401 2 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/model.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Inputs 4 | in vec4 vertex_color; 5 | in vec3 ec_pos; 6 | in float is_selected; 7 | in float show; 8 | in float show_points; 9 | in float show_lines; 10 | in float show_faces; 11 | in vec4 instance_color; 12 | in float object_opacity; 13 | 14 | // Uniforms 15 | uniform float opacity; 16 | uniform bool is_lighted; 17 | uniform vec3 selection_color; 18 | uniform int element_type; 19 | uniform bool is_instance; 20 | uniform bool is_grid; 21 | 22 | out vec4 fragColor; 23 | 24 | void main() { 25 | // Early visibility checks 26 | if (show == 0.0 || 27 | (element_type == 0 && show_points == 0.0) || 28 | (element_type == 1 && show_lines == 0.0) || 29 | (element_type == 2 && show_faces == 0.0) || 30 | (is_instance && is_grid)) { 31 | discard; 32 | } 33 | 34 | // Handle instanced objects 35 | if (is_instance) { 36 | fragColor = instance_color; 37 | return; 38 | } 39 | 40 | // Calculate color and alpha 41 | float alpha = opacity * object_opacity * vertex_color.a; 42 | vec3 color = vertex_color.rgb; 43 | 44 | // Handle selection highlighting 45 | if (is_selected > 0.5) { 46 | color = selection_color * (element_type == 0 ? 0.9 : 47 | element_type == 1 ? 0.8 : 48 | 1.0); 49 | alpha = max(alpha, 0.5); 50 | } 51 | 52 | // Draw circular points 53 | if (element_type == 0) { 54 | vec2 center = gl_PointCoord - vec2(0.5); 55 | if (length(center) > 0.5) { 56 | discard; 57 | } 58 | } 59 | 60 | // Apply lighting if needed 61 | if (is_lighted && !is_grid) { 62 | vec3 ec_normal = normalize(cross(dFdx(ec_pos), dFdy(ec_pos))); 63 | vec3 L = normalize(-ec_pos); 64 | fragColor = vec4(color * dot(ec_normal, L), alpha); 65 | } else { 66 | fragColor = vec4(color, alpha); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/model.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Inputs 4 | in vec3 position; 5 | in vec4 color; 6 | in float object_index; 7 | 8 | // Uniforms 9 | uniform mat4 projection; 10 | uniform mat4 viewworld; 11 | uniform samplerBuffer transformBuffer; 12 | uniform samplerBuffer settingsBuffer; 13 | uniform bool is_grid; 14 | uniform int element_type; 15 | 16 | // Outputs 17 | out vec4 vertex_color; 18 | out vec3 ec_pos; 19 | out float is_selected; 20 | out float show; 21 | out float show_points; 22 | out float show_lines; 23 | out float show_faces; 24 | out vec4 instance_color; 25 | out float object_opacity; 26 | 27 | float getEffectiveShow(float objectIndex) { 28 | float showValue = texelFetch(settingsBuffer, int(objectIndex * 3)).r; 29 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 30 | 31 | while (parentIndex >= 0.0 && showValue > 0.0) { 32 | showValue *= texelFetch(settingsBuffer, int(parentIndex * 3)).r; 33 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 34 | } 35 | return showValue; 36 | } 37 | 38 | float getEffectiveSelection(float objectIndex) { 39 | float selectionValue = texelFetch(settingsBuffer, int(objectIndex * 3) + 1).a; 40 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 41 | 42 | while (parentIndex >= 0.0 && selectionValue == 0.0) { // Continue until we find a selected parent 43 | selectionValue = max(selectionValue, texelFetch(settingsBuffer, int(parentIndex * 3) + 1).a); 44 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 45 | } 46 | return selectionValue; 47 | } 48 | 49 | mat4 getEffectiveTransform(float objectIndex) { 50 | mat4 transform = transpose(mat4( 51 | texelFetch(transformBuffer, int(objectIndex * 4) + 0), 52 | texelFetch(transformBuffer, int(objectIndex * 4) + 1), 53 | texelFetch(transformBuffer, int(objectIndex * 4) + 2), 54 | texelFetch(transformBuffer, int(objectIndex * 4) + 3) 55 | )); 56 | 57 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 58 | 59 | while (parentIndex >= 0.0) { 60 | mat4 parentTransform = transpose(mat4( 61 | texelFetch(transformBuffer, int(parentIndex * 4) + 0), 62 | texelFetch(transformBuffer, int(parentIndex * 4) + 1), 63 | texelFetch(transformBuffer, int(parentIndex * 4) + 2), 64 | texelFetch(transformBuffer, int(parentIndex * 4) + 3) 65 | )); 66 | transform = parentTransform * transform; 67 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 68 | } 69 | 70 | return transform; 71 | } 72 | 73 | void main() { 74 | // Initialize transform matrix and handle grid case 75 | mat4 transform = is_grid ? mat4(1.0) : getEffectiveTransform(object_index); 76 | 77 | float pointSize = 1.0; 78 | 79 | // Set visibility and display settings 80 | if (is_grid) { 81 | show = show_points = show_lines = show_faces = 1.0; 82 | is_selected = 0.0; 83 | object_opacity = 1.0; 84 | } else { 85 | vec4 settings_row1 = texelFetch(settingsBuffer, int(object_index * 3)); 86 | vec4 settings_row2 = texelFetch(settingsBuffer, int(object_index * 3) + 1); 87 | vec4 settings_row3 = texelFetch(settingsBuffer, int(object_index * 3) + 2); 88 | show = getEffectiveShow(object_index); 89 | show_points = settings_row1.g; 90 | show_lines = settings_row1.b; 91 | show_faces = settings_row1.a; 92 | instance_color = vec4(settings_row2.rgb, 1.0); 93 | is_selected = getEffectiveSelection(object_index); 94 | object_opacity = settings_row3.g; 95 | pointSize = settings_row3.b; 96 | } 97 | 98 | // Calculate final position 99 | vertex_color = color; 100 | vec4 worldPos = transform * vec4(position, 1.0); 101 | 102 | // Bypass matrix transformations 2D elements 103 | if (element_type == 4) { 104 | gl_Position = vec4(position, 1.0); 105 | ec_pos = position; 106 | } else { 107 | vec4 viewPos = viewworld * worldPos; 108 | gl_Position = projection * viewPos; 109 | ec_pos = vec3(viewPos); 110 | } 111 | 112 | gl_PointSize = pointSize; 113 | } 114 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/modellines.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Inputs 4 | in vec4 g_vertex_color; 5 | in vec3 g_ec_pos; 6 | in float g_is_selected; 7 | in float g_show; 8 | in float g_show_points; 9 | in float g_show_lines; 10 | in float g_show_faces; 11 | in vec4 g_instance_color; 12 | in float g_object_opacity; 13 | 14 | // Uniforms 15 | uniform float opacity; 16 | uniform bool is_lighted; 17 | uniform vec3 selection_color; 18 | uniform int element_type; 19 | uniform bool is_instance; 20 | uniform bool is_grid; 21 | 22 | out vec4 fragColor; 23 | 24 | void main() { 25 | // Early visibility checks 26 | if (g_show == 0.0 || 27 | (element_type == 0 && g_show_points == 0.0) || 28 | (element_type == 1 && g_show_lines == 0.0) || 29 | (element_type == 2 && g_show_faces == 0.0) || 30 | (is_instance && is_grid)) { 31 | discard; 32 | } 33 | 34 | // Handle instanced objects 35 | if (is_instance) { 36 | fragColor = g_instance_color; 37 | return; 38 | } 39 | 40 | // Calculate color and alpha 41 | float alpha = opacity * g_object_opacity * g_vertex_color.a; 42 | vec3 color = g_vertex_color.rgb; 43 | 44 | // Handle selection highlighting 45 | if (g_is_selected > 0.5) { 46 | color = selection_color * (element_type == 0 ? 0.9 : 47 | element_type == 1 ? 0.8 : 48 | 1.0); 49 | alpha = max(alpha, 0.5); 50 | } 51 | 52 | // Draw circular points 53 | if (element_type == 0) { 54 | vec2 center = gl_PointCoord - vec2(0.5); 55 | if (length(center) > 0.5) { 56 | discard; 57 | } 58 | } 59 | 60 | // Apply lighting if needed 61 | if (is_lighted && !is_grid) { 62 | vec3 ec_normal = normalize(cross(dFdx(g_ec_pos), dFdy(g_ec_pos))); 63 | vec3 L = normalize(-g_ec_pos); 64 | fragColor = vec4(color * dot(ec_normal, L), alpha); 65 | } else { 66 | fragColor = vec4(color, alpha); 67 | } 68 | } -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/modellines.geom: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (lines) in; 4 | layout (triangle_strip, max_vertices = 4) out; 5 | 6 | // from vertex shader 7 | in vec4 vertex_color[]; 8 | in vec3 ec_pos[]; 9 | in float is_selected[]; 10 | in float show[]; 11 | in float show_points[]; 12 | in float show_lines[]; 13 | in float show_faces[]; 14 | in vec4 instance_color[]; 15 | in float object_opacity[]; 16 | in float linewidth[]; 17 | 18 | // to fragment shader 19 | out vec4 g_vertex_color; 20 | out vec3 g_ec_pos; 21 | out float g_is_selected; 22 | out float g_show; 23 | out float g_show_points; 24 | out float g_show_lines; 25 | out float g_show_faces; 26 | out vec4 g_instance_color; 27 | out float g_object_opacity; 28 | 29 | uniform vec2 viewport; // The (width, height) of the viewport in pixels 30 | 31 | void main() { 32 | vec2 p1 = gl_in[0].gl_Position.xy / gl_in[0].gl_Position.w; 33 | vec2 p2 = gl_in[1].gl_Position.xy / gl_in[1].gl_Position.w; 34 | 35 | vec2 dir = normalize(p2 - p1); 36 | vec2 normal = vec2(-dir.y, dir.x); 37 | 38 | float width = linewidth[0]; 39 | vec2 offset = normal * width / viewport; 40 | 41 | // Vertex 1 42 | gl_Position = vec4((p1 - offset) * gl_in[0].gl_Position.w, gl_in[0].gl_Position.zw); 43 | g_vertex_color = vertex_color[0]; 44 | g_ec_pos = ec_pos[0]; 45 | g_is_selected = is_selected[0]; 46 | g_show = show[0]; 47 | g_show_points = show_points[0]; 48 | g_show_lines = show_lines[0]; 49 | g_show_faces = show_faces[0]; 50 | g_instance_color = instance_color[0]; 51 | g_object_opacity = object_opacity[0]; 52 | EmitVertex(); 53 | 54 | // Vertex 2 55 | gl_Position = vec4((p1 + offset) * gl_in[0].gl_Position.w, gl_in[0].gl_Position.zw); 56 | EmitVertex(); 57 | 58 | // Vertex 3 59 | gl_Position = vec4((p2 - offset) * gl_in[1].gl_Position.w, gl_in[1].gl_Position.zw); 60 | g_vertex_color = vertex_color[1]; 61 | g_ec_pos = ec_pos[1]; 62 | g_is_selected = is_selected[1]; 63 | g_show = show[1]; 64 | g_show_points = show_points[1]; 65 | g_show_lines = show_lines[1]; 66 | g_show_faces = show_faces[1]; 67 | g_instance_color = instance_color[1]; 68 | g_object_opacity = object_opacity[1]; 69 | EmitVertex(); 70 | 71 | // Vertex 4 72 | gl_Position = vec4((p2 + offset) * gl_in[1].gl_Position.w, gl_in[1].gl_Position.zw); 73 | EmitVertex(); 74 | 75 | EndPrimitive(); 76 | } -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/modellines.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // Inputs 4 | in vec3 position; 5 | in vec4 color; 6 | in float object_index; 7 | 8 | // Uniforms 9 | uniform mat4 projection; 10 | uniform mat4 viewworld; 11 | uniform samplerBuffer transformBuffer; 12 | uniform samplerBuffer settingsBuffer; 13 | uniform bool is_grid; 14 | uniform int element_type; 15 | 16 | // Outputs 17 | out vec4 vertex_color; 18 | out vec3 ec_pos; 19 | out float is_selected; 20 | out float show; 21 | out float show_points; 22 | out float show_lines; 23 | out float show_faces; 24 | out vec4 instance_color; 25 | out float object_opacity; 26 | out float linewidth; 27 | 28 | float getEffectiveShow(float objectIndex) { 29 | float showValue = texelFetch(settingsBuffer, int(objectIndex * 3)).r; 30 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 31 | 32 | while (parentIndex >= 0.0 && showValue > 0.0) { 33 | showValue *= texelFetch(settingsBuffer, int(parentIndex * 3)).r; 34 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 35 | } 36 | return showValue; 37 | } 38 | 39 | float getEffectiveSelection(float objectIndex) { 40 | float selectionValue = texelFetch(settingsBuffer, int(objectIndex * 3) + 1).a; 41 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 42 | 43 | while (parentIndex >= 0.0 && selectionValue == 0.0) { // Continue until we find a selected parent 44 | selectionValue = max(selectionValue, texelFetch(settingsBuffer, int(parentIndex * 3) + 1).a); 45 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 46 | } 47 | return selectionValue; 48 | } 49 | 50 | mat4 getEffectiveTransform(float objectIndex) { 51 | mat4 transform = transpose(mat4( 52 | texelFetch(transformBuffer, int(objectIndex * 4) + 0), 53 | texelFetch(transformBuffer, int(objectIndex * 4) + 1), 54 | texelFetch(transformBuffer, int(objectIndex * 4) + 2), 55 | texelFetch(transformBuffer, int(objectIndex * 4) + 3) 56 | )); 57 | 58 | float parentIndex = texelFetch(settingsBuffer, int(objectIndex * 3) + 2).r; 59 | 60 | while (parentIndex >= 0.0) { 61 | mat4 parentTransform = transpose(mat4( 62 | texelFetch(transformBuffer, int(parentIndex * 4) + 0), 63 | texelFetch(transformBuffer, int(parentIndex * 4) + 1), 64 | texelFetch(transformBuffer, int(parentIndex * 4) + 2), 65 | texelFetch(transformBuffer, int(parentIndex * 4) + 3) 66 | )); 67 | transform = parentTransform * transform; 68 | parentIndex = texelFetch(settingsBuffer, int(parentIndex * 3) + 2).r; 69 | } 70 | 71 | return transform; 72 | } 73 | 74 | void main() { 75 | // Initialize transform matrix and handle grid case 76 | mat4 transform = is_grid ? mat4(1.0) : getEffectiveTransform(object_index); 77 | 78 | float pointSize = 1.0; 79 | float line_width = 1.0; 80 | 81 | // Set visibility and display settings 82 | if (is_grid) { 83 | show = show_points = show_lines = show_faces = 1.0; 84 | is_selected = 0.0; 85 | object_opacity = 1.0; 86 | } else { 87 | vec4 settings_row1 = texelFetch(settingsBuffer, int(object_index * 3)); 88 | vec4 settings_row2 = texelFetch(settingsBuffer, int(object_index * 3) + 1); 89 | vec4 settings_row3 = texelFetch(settingsBuffer, int(object_index * 3) + 2); 90 | show = getEffectiveShow(object_index); 91 | show_points = settings_row1.g; 92 | show_lines = settings_row1.b; 93 | show_faces = settings_row1.a; 94 | instance_color = vec4(settings_row2.rgb, 1.0); 95 | is_selected = getEffectiveSelection(object_index); 96 | object_opacity = settings_row3.g; 97 | pointSize = settings_row3.b; 98 | line_width = settings_row3.a; 99 | } 100 | 101 | // Calculate final position 102 | vertex_color = color; 103 | vec4 worldPos = transform * vec4(position, 1.0); 104 | 105 | // Bypass matrix transformations 2D elements 106 | if (element_type == 4) { 107 | gl_Position = vec4(position, 1.0); 108 | ec_pos = position; 109 | } else { 110 | vec4 viewPos = viewworld * worldPos; 111 | gl_Position = projection * viewPos; 112 | ec_pos = vec3(viewPos); 113 | } 114 | 115 | gl_PointSize = pointSize; 116 | linewidth = line_width; 117 | } -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/tag.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform sampler2D tex; 4 | uniform bool is_selected; 5 | uniform vec3 text_color; 6 | 7 | in vec2 texcoord; 8 | 9 | out vec4 fragColor; 10 | 11 | void main() 12 | { 13 | vec2 xy = texcoord; 14 | xy.y = 1.0 - xy.y; 15 | float a = texture(tex, xy).r; 16 | if (is_selected) { 17 | fragColor = vec4(1.0, 1.0, 0.0, a); 18 | } else { 19 | fragColor = vec4(text_color, a); 20 | } 21 | if (a <= 0){ 22 | discard; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/compas_viewer/renderer/shaders/tag.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec3 position; 4 | 5 | uniform mat4 projection; 6 | uniform mat4 viewworld; 7 | uniform mat4 transform; 8 | uniform float screen_aspect; 9 | uniform float screen_height; 10 | uniform float text_aspect; 11 | uniform float text_height; 12 | uniform vec3 text_position; 13 | uniform int vertical_align; 14 | uniform int horizontal_align; 15 | 16 | out vec2 texcoord; 17 | 18 | void main() 19 | { 20 | texcoord = vec2(position.x, position.y); 21 | 22 | vec2 position = vec2(position.x, position.y); 23 | 24 | if (horizontal_align == 0) { 25 | position.x -= 0.5; 26 | } 27 | else if (horizontal_align == 1) { 28 | position.x -= 1.0; 29 | } 30 | 31 | if (vertical_align == 0) { 32 | position.y -= 0.5; 33 | } 34 | else if (vertical_align == 1) { 35 | position.y -= 1.0; 36 | } 37 | 38 | vec4 screen_position = projection * viewworld * transform * vec4(text_position, 1.0); 39 | vec2 adjustedPos = vec2(position.x / screen_aspect * text_aspect, position.y) * text_height / screen_height; 40 | vec4 offset = vec4(adjustedPos * screen_position.w, 0.0, 0.0); 41 | screen_position += offset; 42 | gl_Position = screen_position; 43 | } 44 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package provides scene object plugins for visualizing COMPAS objects in `compas_viewer`. 3 | """ 4 | 5 | from compas.scene import register 6 | from compas.plugins import plugin 7 | from compas.datastructures import Mesh 8 | from compas.datastructures import Graph 9 | from compas.geometry import ( 10 | Point, 11 | Pointcloud, 12 | Line, 13 | Vector, 14 | Circle, 15 | Box, 16 | Polyline, 17 | Torus, 18 | Polygon, 19 | Sphere, 20 | Plane, 21 | Cylinder, 22 | Ellipse, 23 | Cone, 24 | Capsule, 25 | Polyhedron, 26 | Frame, 27 | NurbsSurface, 28 | ) 29 | 30 | from .sceneobject import ViewerSceneObject 31 | from .meshobject import MeshObject 32 | from .graphobject import GraphObject 33 | from .pointobject import PointObject 34 | from .pointcloudobject import PointcloudObject 35 | from .lineobject import LineObject 36 | from .vectorobject import VectorObject 37 | from .tagobject import TagObject, Tag 38 | from .frameobject import FrameObject 39 | from .circleobject import CircleObject 40 | from .polylineobject import PolylineObject 41 | from .polygonobject import PolygonObject 42 | from .planeobject import PlaneObject 43 | from .ellipseobject import EllipseObject 44 | from .polyhedronobject import PolyhedronObject 45 | from .geometryobject import GeometryObject 46 | from .shapeobject import ShapeObject 47 | from .group import Group 48 | from .collectionobject import Collection 49 | from .collectionobject import CollectionObject 50 | from .bufferobject import BufferGeometry 51 | from .bufferobject import BufferObject 52 | from .scene import ViewerScene 53 | 54 | 55 | @plugin(category="drawing-utils", requires=["compas_viewer"]) 56 | def clear(guids: list[str]): 57 | for obj in guids: 58 | del obj 59 | 60 | 61 | @plugin(category="drawing-utils", requires=["compas_viewer"]) 62 | def redraw(): 63 | pass 64 | 65 | 66 | @plugin(category="factories", requires=["compas_viewer"]) 67 | def register_scene_objects(): 68 | register(Mesh, MeshObject, context="Viewer") 69 | register(Graph, GraphObject, context="Viewer") 70 | register(Point, PointObject, context="Viewer") 71 | register(Pointcloud, PointcloudObject, context="Viewer") 72 | register(Line, LineObject, context="Viewer") 73 | register(Tag, TagObject, context="Viewer") 74 | register(Frame, FrameObject, context="Viewer") 75 | register(Vector, VectorObject, context="Viewer") 76 | register(Circle, CircleObject, context="Viewer") 77 | register(Box, ShapeObject, context="Viewer") 78 | register(Polyline, PolylineObject, context="Viewer") 79 | register(Torus, ShapeObject, context="Viewer") 80 | register(Polygon, PolygonObject, context="Viewer") 81 | register(Sphere, ShapeObject, context="Viewer") 82 | register(Plane, PlaneObject, context="Viewer") 83 | register(Cylinder, ShapeObject, context="Viewer") 84 | register(Ellipse, EllipseObject, context="Viewer") 85 | register(Cone, ShapeObject, context="Viewer") 86 | register(Capsule, ShapeObject, context="Viewer") 87 | register(Polyhedron, PolyhedronObject, context="Viewer") 88 | register(Collection, CollectionObject, context="Viewer") 89 | register(BufferGeometry, BufferObject, context="Viewer") 90 | 91 | try: 92 | from compas.geometry import NurbsCurve 93 | from compas_occ.brep import OCCBrep 94 | 95 | from .brepobject import BRepObject 96 | from .nurbssurfaceobject import NurbsSurfaceObject 97 | from .nurbscurveobject import NurbsCurveObject 98 | 99 | register(OCCBrep, BRepObject, context="Viewer") 100 | register(NurbsSurface, NurbsSurfaceObject, context="Viewer") 101 | register(NurbsCurve, NurbsCurveObject, context="Viewer") 102 | 103 | except ImportError: 104 | pass 105 | 106 | 107 | __all__ = [ 108 | "ViewerSceneObject", 109 | "Mesh", 110 | "MeshObject", 111 | "Point", 112 | "PointObject", 113 | "Line", 114 | "LineObject", 115 | "Tag", 116 | "TagObject", 117 | "Frame", 118 | "FrameObject", 119 | "Vector", 120 | "VectorObject", 121 | "Circle", 122 | "CircleObject", 123 | "Polyline", 124 | "PolylineObject", 125 | "Box", 126 | "Torus", 127 | "Polygon", 128 | "PolygonObject", 129 | "Sphere", 130 | "Plane", 131 | "PlaneObject", 132 | "Cylinder", 133 | "Ellipse", 134 | "EllipseObject", 135 | "Cone", 136 | "Capsule", 137 | "NurbsSurface", 138 | "NurbsSurfaceObject", 139 | "GeometryObject", 140 | "ShapeObject", 141 | "Group", 142 | "Collection", 143 | "CollectionObject", 144 | "BufferGeometry", 145 | "BufferObject", 146 | "ViewerScene", 147 | ] 148 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/brepobject.py: -------------------------------------------------------------------------------- 1 | from compas_occ.brep import OCCBrep 2 | 3 | from compas.geometry import Line 4 | from compas.geometry import Point 5 | from compas.scene import GeometryObject 6 | from compas.tolerance import TOL 7 | 8 | from .geometryobject import GeometryObject as ViewerGeometryObject 9 | 10 | 11 | class BRepObject(ViewerGeometryObject, GeometryObject): 12 | """Viewer scene object for displaying COMPAS OCCBrep geometry. 13 | 14 | Attributes 15 | ---------- 16 | brep : :class:`compas_occ.brep.OCCBrep` 17 | The compas_occ Brep object. 18 | mesh : :class:`compas.datastructures.Mesh` 19 | The mesh representation of the Brep. 20 | 21 | See Also 22 | -------- 23 | :class:`compas_occ.brep.Brep` 24 | """ 25 | 26 | geometry: OCCBrep 27 | 28 | def __init__(self, **kwargs): 29 | super().__init__(**kwargs) 30 | self._viewmesh, self._boundaries = self.geometry.to_tesselation(TOL.lineardeflection) 31 | 32 | @property 33 | def points(self) -> list[Point]: 34 | return self.geometry.points 35 | 36 | @property 37 | def lines(self) -> list[Line]: 38 | lines = [] 39 | for polyline in self._boundaries: 40 | lines += polyline.lines 41 | return lines 42 | 43 | @property 44 | def viewmesh(self) -> tuple[list[Point], list[list[int]]]: 45 | return self._viewmesh.to_vertices_and_faces(triangulated=True) 46 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/circleobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Circle 2 | from compas.geometry import Line 3 | from compas.geometry import Point 4 | 5 | from .geometryobject import GeometryObject 6 | 7 | 8 | class CircleObject(GeometryObject): 9 | """Viewer scene object for displaying COMPAS Circle geometry.""" 10 | 11 | geometry: Circle 12 | 13 | def __init__(self, **kwargs): 14 | super().__init__(**kwargs) 15 | self.show_lines = True 16 | 17 | @property 18 | def points(self) -> list[Point]: 19 | return [self.geometry.center] 20 | 21 | @property 22 | def lines(self) -> list[Line]: 23 | return self.geometry.to_polyline(n=self.u).lines 24 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/collectionobject.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import numpy as np 4 | from numpy import array 5 | 6 | from compas.data import Data 7 | from compas.datastructures import Mesh 8 | from compas.geometry import Geometry 9 | from compas.scene import GeometryObject 10 | 11 | from .sceneobject import ShaderDataType 12 | from .sceneobject import ViewerSceneObject 13 | 14 | 15 | class Collection(Data): 16 | def __init__(self, items: list[Union[Geometry, Mesh]] = None, **kwargs): 17 | super().__init__(**kwargs) 18 | self.items = items 19 | 20 | @property 21 | def __data__(self): 22 | return {"items": self.items} 23 | 24 | 25 | class CollectionObject(ViewerSceneObject, GeometryObject): 26 | """Viewer scene object for displaying a collection of COMPAS geometries.""" 27 | 28 | def __init__(self, **kwargs): 29 | super().__init__(**kwargs) 30 | kwargs.pop("item") 31 | self.objects = [ViewerSceneObject(item=item, **kwargs) for item in self.collection.items] 32 | 33 | @property 34 | def collection(self) -> Collection: 35 | return self.item 36 | 37 | def _read_points_data(self) -> ShaderDataType: 38 | positions = [] 39 | colors = [] 40 | elements = [] 41 | count = 0 42 | for obj in self.objects: 43 | p, c, e = obj._read_points_data() or ([], [], []) 44 | positions += p 45 | colors += c 46 | elements += (array(e) + count).tolist() 47 | count += len(p) 48 | 49 | return positions, colors, elements 50 | 51 | def _read_lines_data(self) -> ShaderDataType: 52 | positions = [] 53 | colors = [] 54 | elements = [] 55 | count = 0 56 | for obj in self.objects: 57 | p, c, e = obj._read_lines_data() or ([], [], []) 58 | positions += p 59 | colors += c 60 | elements += (array(e) + count).tolist() 61 | count += len(p) 62 | 63 | return positions, colors, elements 64 | 65 | def _read_frontfaces_data(self) -> ShaderDataType: 66 | positions = [] 67 | colors = [] 68 | elements = [] 69 | count = 0 70 | for obj in self.objects: 71 | p, c, e = obj._read_frontfaces_data() or ([], [], []) 72 | positions += p 73 | colors += c 74 | elements += (np.array(e) + count).tolist() 75 | count += len(p) 76 | 77 | return positions, colors, elements 78 | 79 | def _read_backfaces_data(self) -> ShaderDataType: 80 | positions = [] 81 | colors = [] 82 | elements = [] 83 | count = 0 84 | for obj in self.objects: 85 | p, c, e = obj._read_backfaces_data() or ([], [], []) 86 | positions += p 87 | colors += c 88 | elements += (np.array(e) + count).tolist() 89 | count += len(p) 90 | 91 | return positions, colors, elements 92 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/ellipseobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Ellipse 2 | from compas.geometry import Line 3 | from compas.geometry import Point 4 | 5 | from .geometryobject import GeometryObject 6 | 7 | 8 | class EllipseObject(GeometryObject): 9 | """Viewer scene object for displaying COMPAS Ellipse geometry.""" 10 | 11 | geometry: Ellipse 12 | 13 | def __init__(self, **kwargs): 14 | super().__init__(**kwargs) 15 | self.show_lines = True 16 | 17 | @property 18 | def points(self) -> list[Point]: 19 | return [self.geometry.plane.point] 20 | 21 | @property 22 | def lines(self) -> list[Line]: 23 | return self.geometry.to_polyline(n=self.u).lines 24 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/frameobject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Frame 5 | from compas.geometry import Point 6 | from compas.geometry import Transformation 7 | 8 | from .sceneobject import ShaderDataType 9 | from .sceneobject import ViewerSceneObject 10 | 11 | 12 | class FrameObject(ViewerSceneObject): 13 | """ 14 | The scene object of the COMPAS Frame. 15 | With its modifiable cell size and dimension. 16 | 17 | Parameters 18 | ---------- 19 | frame : :class:`compas.geometry.Frame` 20 | The frame geometry. 21 | framesize : tuple[float, int, float, int] 22 | The size of the grid in [dx, nx, dy, ny] format. 23 | Notice that the `nx` and `ny` must be even numbers. 24 | linecolor : :class:`compas.colors.Color` 25 | The color of the grid lines. 26 | show_framez : bool 27 | If True, the Z axis of the grid will be shown. 28 | 29 | Attributes 30 | ---------- 31 | geometry : :class:`compas.geometry.Frame` 32 | The frame geometry. 33 | dx : float 34 | The size of the grid in the X direction. 35 | nx : int 36 | The number of grid cells in the X direction. 37 | dy : float 38 | The size of the grid in the Y direction. 39 | ny : int 40 | The number of grid cells in the Y direction. 41 | show_framez : bool 42 | If the Z axis of the grid is shown. 43 | 44 | See Also 45 | -------- 46 | :class:`compas.geometry.Frame` 47 | """ 48 | 49 | item: Frame 50 | 51 | def __init__(self, size: Optional[float] = 1, **kwargs): 52 | super().__init__(**kwargs) 53 | self.size = size 54 | 55 | def _read_lines_data(self) -> ShaderDataType: 56 | trans = Transformation.from_frame_to_frame(Frame.worldXY(), self.item) 57 | 58 | positions = [] 59 | colors = [] 60 | elements = [] 61 | 62 | # X direction 63 | colors.append(Color.red()) 64 | colors.append(Color.red()) 65 | positions.append(Point(0, 0, 0).transformed(trans)) 66 | positions.append(Point(self.size, 0, 0).transformed(trans)) 67 | elements.append([0, 1]) 68 | 69 | # Y direction 70 | colors.append(Color.green()) 71 | colors.append(Color.green()) 72 | positions.append(Point(0, 0, 0).transformed(trans)) 73 | positions.append(Point(0, self.size, 0).transformed(trans)) 74 | elements.append([2, 3]) 75 | 76 | # Z direction 77 | colors.append(Color.blue()) 78 | colors.append(Color.blue()) 79 | positions.append(Point(0, 0, 0).transformed(trans)) 80 | positions.append(Point(0, 0, self.size).transformed(trans)) 81 | elements.append([4, 5]) 82 | 83 | return positions, colors, elements 84 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/geometryobject.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from typing import Optional 3 | 4 | from compas.colors import Color 5 | from compas.geometry import Geometry 6 | from compas.itertools import flatten 7 | from compas.scene import GeometryObject as BaseGeometryObject 8 | from compas.scene.descriptors.color import ColorAttribute 9 | 10 | from .sceneobject import ShaderDataType 11 | from .sceneobject import ViewerSceneObject 12 | 13 | 14 | class GeometryObject(ViewerSceneObject, BaseGeometryObject): 15 | """Viewer scene object for displaying COMPAS Geometry. 16 | 17 | Parameters 18 | ---------- 19 | geometry : :class:`compas.geometry.Geometry` 20 | A COMPAS geometry. 21 | v : int, optional 22 | The number of vertices in the u-direction of non-OCC geometries. 23 | u : int, optional 24 | The number of vertices in the v-direction of non-OCC geometries. 25 | pointcolor : :class:`compas.colors.Color`, optional 26 | The color of the points. Default is the value of `pointcolor` in `viewer.config`. 27 | linecolor : :class:`compas.colors.Color`, optional 28 | The color of the lines. Default is the value of `linecolor` in `viewer.config`. 29 | surfacecolor : :class:`compas.colors.Color`, optional 30 | The color of the surfaces. Default is the value of `surfacecolor` in `viewer.config`. 31 | **kwargs : dict, optional 32 | Additional options for the :class:`compas_viewer.scene.ViewerSceneObject` 33 | and :class:`compas.scene.GeometryObject`. 34 | 35 | Attributes 36 | ---------- 37 | geometry : :class:`compas.geometry.Geometry` 38 | The COMPAS geometry. 39 | pointcolor : :class:`compas.colors.Color` 40 | The color of the points. 41 | linecolor : :class:`compas.colors.Color` 42 | The color of the lines. 43 | surfacecolor : :class:`compas.colors.Color` 44 | The color of the surfaces. 45 | mesh : :class:`compas.datastructures.Mesh` 46 | The triangulated mesh representation of the geometry. 47 | LINEARDEFLECTION : float 48 | The default linear deflection for the geometry. 49 | 50 | See Also 51 | -------- 52 | :class:`compas.geometry.Geometry` 53 | """ 54 | 55 | pointcolor = ColorAttribute(default=Color(0.2, 0.2, 0.2)) 56 | linecolor = ColorAttribute(default=Color(0.2, 0.2, 0.2)) 57 | surfacecolor = ColorAttribute(default=Color(0.9, 0.9, 0.9)) 58 | 59 | geometry: Geometry 60 | 61 | def __init__(self, u: Optional[int] = 16, v: Optional[int] = 16, **kwargs): 62 | super().__init__(**kwargs) 63 | self.u = u 64 | self.v = v 65 | 66 | @property 67 | def facecolor(self) -> Color: 68 | return self.surfacecolor 69 | 70 | @facecolor.setter 71 | def facecolor(self, color: Color) -> None: 72 | self.surfacecolor = color 73 | 74 | @property 75 | def points(self) -> Optional[list[Iterable[float]]]: 76 | return None 77 | 78 | @property 79 | def lines(self) -> Optional[list[Iterable[Iterable[float]]]]: 80 | return None 81 | 82 | @property 83 | def viewmesh(self) -> Optional[tuple[list[Iterable[float]], list[Iterable[int]]]]: 84 | return None 85 | 86 | def _read_points_data(self) -> ShaderDataType: 87 | if self.points is None: 88 | return [], [], [] 89 | positions = self.points 90 | colors = [self.pointcolor] * len(positions) 91 | elements = [[i] for i in range(len(positions))] 92 | return positions, colors, elements 93 | 94 | def _read_lines_data(self) -> ShaderDataType: 95 | if self.lines is None: 96 | return [], [], [] 97 | positions = [] 98 | elements = [] 99 | positions = list(flatten(self.lines)) 100 | colors = [self.linecolor] * len(positions) 101 | elements = [[2 * i, 2 * i + 1] for i in range(len(self.lines))] 102 | return positions, colors, elements 103 | 104 | def _read_frontfaces_data(self) -> ShaderDataType: 105 | if self.viewmesh is None: 106 | return [], [], [] 107 | positions, elements = self.viewmesh 108 | colors = [self.facecolor] * len(positions) 109 | return positions, colors, elements # type: ignore 110 | 111 | def _read_backfaces_data(self) -> ShaderDataType: 112 | if self.viewmesh is None: 113 | return [], [], [] 114 | positions, elements = self.viewmesh 115 | for element in elements: 116 | element.reverse() 117 | colors = [self.facecolor] * len(positions) 118 | return positions, colors, elements # type: ignore 119 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/graphobject.py: -------------------------------------------------------------------------------- 1 | from compas.datastructures import Graph 2 | from compas.scene import GraphObject as BaseGraphObject 3 | 4 | from .sceneobject import ShaderDataType 5 | from .sceneobject import ViewerSceneObject 6 | 7 | 8 | class GraphObject(ViewerSceneObject, BaseGraphObject): 9 | """Viewer scene object for displaying COMPAS Graph data.""" 10 | 11 | graph: Graph 12 | 13 | def _read_points_data(self) -> ShaderDataType: 14 | positions = [] 15 | colors = [] 16 | elements = [] 17 | i = 0 18 | 19 | for node in self.graph.nodes(): 20 | positions.append(self.graph.node_coordinates(node)) 21 | colors.append(self.nodecolor.default) 22 | elements.append([i]) 23 | i += 1 24 | return positions, colors, elements 25 | 26 | def _read_lines_data(self) -> ShaderDataType: 27 | positions = [] 28 | colors = [] 29 | elements = [] 30 | i = 0 31 | 32 | for u, v in self.graph.edges(): 33 | color = self.edgecolor.default 34 | positions.append(self.graph.node_coordinates(u)) 35 | positions.append(self.graph.node_coordinates(v)) 36 | colors.append(color) 37 | colors.append(color) 38 | elements.append([i + 0, i + 1]) 39 | i += 2 40 | return positions, colors, elements 41 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/group.py: -------------------------------------------------------------------------------- 1 | from compas.scene import Group as BaseGroup 2 | 3 | from .sceneobject import ViewerSceneObject 4 | 5 | 6 | class Group(ViewerSceneObject, BaseGroup): 7 | """A group of scene objects.""" 8 | 9 | pass 10 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/lineobject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from compas.geometry import Line 4 | from compas.geometry import Point 5 | 6 | from .geometryobject import GeometryObject 7 | 8 | 9 | class LineObject(GeometryObject): 10 | """Viewer scene object for displaying COMPAS Line geometry.""" 11 | 12 | geometry: Line 13 | 14 | def __init__(self, **kwargs): 15 | super().__init__(**kwargs) 16 | self.show_lines = True 17 | 18 | @property 19 | def points(self) -> Optional[list[Point]]: 20 | return [self.geometry.start, self.geometry.end] 21 | 22 | @property 23 | def lines(self) -> Optional[list[Line]]: 24 | return [self.geometry] 25 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/nurbscurveobject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from compas.geometry import Line 4 | from compas.geometry import NurbsCurve 5 | from compas.geometry import Point 6 | from compas.itertools import pairwise 7 | 8 | from .geometryobject import GeometryObject 9 | 10 | 11 | class NurbsCurveObject(GeometryObject): 12 | """Viewer scene object for displaying COMPAS NurbsCurve geometry.""" 13 | 14 | geometry: NurbsCurve 15 | 16 | @property 17 | def points(self) -> Optional[list[Point]]: 18 | return self.geometry.points 19 | 20 | @property 21 | def lines(self) -> Optional[list[Line]]: 22 | lines = [] 23 | polyline = self.geometry.to_polyline() 24 | for pair in pairwise(polyline.points): 25 | lines.append(Line(*pair)) 26 | return lines 27 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/nurbssurfaceobject.py: -------------------------------------------------------------------------------- 1 | from compas_occ.brep import OCCBrep 2 | 3 | from compas.geometry import Line 4 | from compas.geometry import NurbsSurface 5 | from compas.geometry import Point 6 | from compas.itertools import pairwise 7 | from compas.tolerance import TOL 8 | 9 | from .geometryobject import GeometryObject 10 | 11 | 12 | class NurbsSurfaceObject(GeometryObject): 13 | """Viewer scene object for displaying COMPAS NurbsSurface geometry.""" 14 | 15 | geometry: NurbsSurface 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__(**kwargs) 19 | self._brep: OCCBrep = OCCBrep.from_surface(self.geometry) 20 | self._viewmesh, self._boundaries = self._brep.to_tesselation(TOL.lineardeflection) 21 | 22 | @property 23 | def points(self) -> list[Point]: 24 | return self._brep.points 25 | 26 | @property 27 | def lines(self) -> list[Line]: 28 | lines = [] 29 | for polyline in self._boundaries: 30 | for pair in pairwise(polyline.points): 31 | lines.append(Line(*pair)) 32 | 33 | return lines 34 | 35 | @property 36 | def viewmesh(self) -> tuple[list[Point], list[list[int]]]: 37 | return self._viewmesh.to_vertices_and_faces(triangulated=True) 38 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/planeobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Frame 2 | from compas.geometry import Plane 3 | from compas.geometry import Point 4 | 5 | from .geometryobject import GeometryObject 6 | 7 | 8 | class PlaneObject(GeometryObject): 9 | """ 10 | Viewer scene object for displaying COMPAS Plane geometry. 11 | 12 | Parameters 13 | ---------- 14 | planesize : float 15 | The size of the plane. 16 | Default is 1. 17 | 18 | See Also 19 | -------- 20 | :class:`compas.geometry.Plane` 21 | """ 22 | 23 | def __init__(self, planesize: float = 1, **kwargs): 24 | super().__init__(**kwargs) 25 | self.planesize = planesize 26 | 27 | @property 28 | def plane(self) -> Plane: 29 | return self.item 30 | 31 | @property 32 | def points(self) -> list[Point]: 33 | return [self.plane.point] 34 | 35 | @property 36 | def lines(self) -> list[list[Point]]: 37 | frame = Frame.from_plane(self.plane) 38 | return [[frame.to_world_coordinates([0, 0, 0]), frame.to_world_coordinates([0, 0, self.planesize])]] 39 | 40 | @property 41 | def viewmesh(self) -> tuple[list[Point], list[list[int]]]: 42 | frame = Frame.from_plane(self.plane) 43 | vertices = [ 44 | frame.to_world_coordinates([-self.planesize, -self.planesize, 0]), 45 | frame.to_world_coordinates([self.planesize, -self.planesize, 0]), 46 | frame.to_world_coordinates([self.planesize, self.planesize, 0]), 47 | frame.to_world_coordinates([-self.planesize, self.planesize, 0]), 48 | ] 49 | return vertices, [[0, 1, 2], [0, 2, 3]] 50 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/pointcloudobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Point 2 | from compas.geometry import Pointcloud 3 | 4 | from .geometryobject import GeometryObject 5 | 6 | 7 | class PointcloudObject(GeometryObject): 8 | """Viewer scene object for displaying COMPAS Pointcloud geometry.""" 9 | 10 | geometry: Pointcloud 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | self.show_points = True 15 | 16 | @property 17 | def points(self) -> list[Point]: 18 | return self.geometry.points 19 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/pointobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Point 2 | 3 | from .geometryobject import GeometryObject 4 | 5 | 6 | class PointObject(GeometryObject): 7 | """Viewer scene object for displaying COMPAS Point geometry.""" 8 | 9 | geometry: Point 10 | 11 | def __init__(self, **kwargs): 12 | super().__init__(**kwargs) 13 | self.show_points = True 14 | 15 | @property 16 | def points(self) -> list[Point]: 17 | return [self.geometry] 18 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/polygonobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Line 2 | from compas.geometry import Point 3 | from compas.geometry import Polygon 4 | from compas.geometry import earclip_polygon 5 | 6 | from .geometryobject import GeometryObject 7 | 8 | 9 | class PolygonObject(GeometryObject): 10 | """Viewer scene object for displaying COMPAS Polygon geometry.""" 11 | 12 | geometry: Polygon 13 | 14 | @property 15 | def points(self) -> list[Point]: 16 | return self.geometry.points 17 | 18 | @property 19 | def lines(self) -> list[Line]: 20 | return self.geometry.lines 21 | 22 | @property 23 | def viewmesh(self) -> tuple[list[Point], list[list[int]]]: 24 | vertices = self.geometry.points 25 | faces = [list(range(len(vertices)))] 26 | faces = earclip_polygon(self.geometry) 27 | return vertices, faces 28 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/polyhedronobject.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Line 2 | from compas.geometry import Point 3 | from compas.geometry import Polyhedron 4 | 5 | from .geometryobject import GeometryObject 6 | 7 | 8 | class PolyhedronObject(GeometryObject): 9 | """Viewer scene object for displaying COMPAS Polyhedron geometry.""" 10 | 11 | geometry: Polyhedron 12 | 13 | @property 14 | def points(self) -> list[Point]: 15 | return self.geometry.points 16 | 17 | @property 18 | def lines(self) -> list[Line]: 19 | return self.geometry.lines 20 | 21 | @property 22 | def viewmesh(self) -> tuple[list[Point], list[list[int]]]: 23 | return self.geometry.to_vertices_and_faces(triangulated=True) 24 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/polylineobject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from compas.geometry import Line 4 | from compas.geometry import Point 5 | from compas.geometry import Polyline 6 | 7 | from .geometryobject import GeometryObject 8 | 9 | 10 | class PolylineObject(GeometryObject): 11 | """Viewer scene object for displaying COMPAS Polyline geometry.""" 12 | 13 | geometry: Polyline 14 | 15 | def __init__(self, **kwargs): 16 | super().__init__(**kwargs) 17 | self.show_lines = True 18 | 19 | @property 20 | def points(self) -> Optional[list[Point]]: 21 | return self.geometry.points 22 | 23 | @property 24 | def lines(self) -> Optional[list[Line]]: 25 | return self.geometry.lines 26 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/shapeobject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from compas.colors import Color 4 | from compas.geometry import Shape 5 | 6 | from .geometryobject import GeometryObject 7 | from .sceneobject import ShaderDataType 8 | 9 | 10 | class ShapeObject(GeometryObject): 11 | """Viewer scene object for displaying COMPAS Geometry. 12 | 13 | Parameters 14 | ---------- 15 | geometry : :class:`compas.geometry.Geometry` 16 | A COMPAS geometry. 17 | v : int, optional 18 | The number of vertices in the u-direction of non-OCC geometries. 19 | u : int, optional 20 | The number of vertices in the v-direction of non-OCC geometries. 21 | pointcolor : :class:`compas.colors.Color`, optional 22 | The color of the points. Default is the value of `pointcolor` in `viewer.config`. 23 | linecolor : :class:`compas.colors.Color`, optional 24 | The color of the lines. Default is the value of `linecolor` in `viewer.config`. 25 | surfacecolor : :class:`compas.colors.Color`, optional 26 | The color of the surfaces. Default is the value of `surfacecolor` in `viewer.config`. 27 | **kwargs : dict, optional 28 | Additional options for the :class:`compas_viewer.scene.ViewerSceneObject` 29 | and :class:`compas.scene.GeometryObject`. 30 | 31 | Attributes 32 | ---------- 33 | geometry : :class:`compas.geometry.Geometry` 34 | The COMPAS geometry. 35 | pointcolor : :class:`compas.colors.Color` 36 | The color of the points. 37 | linecolor : :class:`compas.colors.Color` 38 | The color of the lines. 39 | surfacecolor : :class:`compas.colors.Color` 40 | The color of the surfaces. 41 | mesh : :class:`compas.datastructures.Mesh` 42 | The triangulated mesh representation of the geometry. 43 | LINEARDEFLECTION : float 44 | The default linear deflection for the geometry. 45 | 46 | See Also 47 | -------- 48 | :class:`compas.geometry.Geometry` 49 | """ 50 | 51 | geometry: Shape 52 | 53 | def __init__(self, u: Optional[int] = 16, v: Optional[int] = 16, **kwargs): 54 | super().__init__(**kwargs) 55 | self.u = u 56 | self.v = v 57 | 58 | @property 59 | def u(self) -> int: 60 | return self.geometry.resolution_u 61 | 62 | @u.setter 63 | def u(self, u: int) -> None: 64 | self.geometry.resolution_u = u 65 | 66 | @property 67 | def v(self) -> int: 68 | return self.geometry.resolution_v 69 | 70 | @v.setter 71 | def v(self, v: int) -> None: 72 | self.geometry.resolution_v = v 73 | 74 | @property 75 | def facecolor(self) -> Color: 76 | return self.surfacecolor 77 | 78 | @facecolor.setter 79 | def facecolor(self, color: Color) -> None: 80 | self.surfacecolor = color 81 | 82 | def _read_points_data(self) -> ShaderDataType: 83 | positions = self.geometry.vertices 84 | colors = [self.pointcolor] * len(positions) 85 | elements = [[i] for i in range(len(positions))] 86 | return positions, colors, elements 87 | 88 | def _read_lines_data(self) -> ShaderDataType: 89 | vertices = self.geometry._vertices 90 | positions = [vertices[vertex] for edge in self.geometry.edges for vertex in edge] 91 | colors = [self.linecolor] * len(positions) 92 | elements = [[2 * i, 2 * i + 1] for i in range(len(self.geometry.edges))] 93 | return positions, colors, elements 94 | 95 | def _read_frontfaces_data(self) -> ShaderDataType: 96 | vertices = self.geometry._vertices 97 | positions = [vertices[vertex] for face in self.geometry.triangles for vertex in face] 98 | colors = [self.facecolor] * len(positions) 99 | elements = [[3 * i, 3 * i + 1, 3 * i + 2] for i in range(len(self.geometry.triangles))] 100 | return positions, colors, elements 101 | 102 | def _read_backfaces_data(self) -> ShaderDataType: 103 | vertices = self.geometry._vertices 104 | positions = [vertices[vertex] for face in self.geometry.triangles for vertex in face] 105 | colors = [self.facecolor] * len(positions) 106 | elements = [[3 * i + 2, 3 * i + 1, 3 * i] for i in range(len(self.geometry.triangles))] 107 | return positions, colors, elements 108 | -------------------------------------------------------------------------------- /src/compas_viewer/scene/vectorobject.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from compas.geometry import Point 4 | from compas.geometry import Vector 5 | from compas.scene import GeometryObject 6 | 7 | from .sceneobject import ShaderDataType 8 | from .sceneobject import ViewerSceneObject 9 | 10 | 11 | class VectorObject(ViewerSceneObject, GeometryObject): 12 | """Viewer scene object for displaying COMPAS Vector geometry. 13 | 14 | Parameters 15 | ---------- 16 | vector : :class:`compas.geometry.Vector` 17 | The vector geometry. 18 | anchor : :class:`compas.geometry.Point`, optional 19 | The anchor point of the vector. 20 | Default is the origin point. 21 | **kwargs : dict, optional 22 | Additional options for the :class:`compas_viewer.scene.ViewerSceneObject`. 23 | 24 | Notes 25 | ----- 26 | The frame object is always unselectable. 27 | Apart from the :attr:`compas_viewer.scene.vectorobject.VectorObject.config.linewidth` 28 | that controls the width of the vector, 29 | the :attr:`compas_viewer.scene.vectorobject.VectorObject.config.vectorsize` 30 | (float 0-1) controls the size of the arrow. 31 | 32 | See Also 33 | -------- 34 | :class:`compas.geometry.Vector` 35 | """ 36 | 37 | geometry: Vector 38 | 39 | # Fixed indices for the arrow faces: 40 | ARROW_FACE_INDICES = [[6, 2, 3], [6, 3, 4], [6, 4, 5], [6, 5, 2], [2, 4, 3], [2, 5, 4]] 41 | 42 | def __init__(self, anchor: Point = Point(0, 0, 0), **kwargs): 43 | self._anchor = anchor 44 | super().__init__(**kwargs) 45 | self.arrow_buffer: dict[str, Any] 46 | self._lines_buffer: dict[str, Any] 47 | self._arrow_vertices: list[float] = [] 48 | self._arrow_colors: list[float] = [] 49 | 50 | def _calculate_arrow_buffer_data(self): 51 | arrow_end = self._anchor + self.geometry * (1 - self.viewer.config.vectorsize) 52 | arrow_width = 5 * self.viewer.config.vectorsize * self.viewer.config.vectorsize * self.geometry.length 53 | self._arrow_vertices = [ 54 | self._anchor, # Arrow start 55 | arrow_end, # Arrow body end 56 | arrow_end + (self.geometry.cross([1, 0, 0]) / self.geometry.length) * arrow_width, # Arrow corner 1 57 | arrow_end + (self.geometry.cross([0, 1, 0]) / self.geometry.length) * arrow_width, # Arrow corner 2 58 | arrow_end + (self.geometry.cross([-1, 0, 0]) / self.geometry.length) * arrow_width, # Arrow corner 3 59 | arrow_end + (self.geometry.cross([0, -1, 0]) / self.geometry.length) * arrow_width, # Arrow corner 4 60 | self._anchor + self.geometry, # Arrow end 61 | ] 62 | 63 | self._arrow_colors = [self.linecolor or self.viewer.config.ui.display.linecolor] * len(self._arrow_vertices) 64 | 65 | def _read_lines_data(self) -> ShaderDataType: 66 | positions = self._arrow_vertices 67 | colors = self._arrow_colors 68 | elements = [[0, 1]] 69 | return positions, colors, elements 70 | 71 | def _read_frontfaces_data(self) -> ShaderDataType: 72 | positions = self._arrow_vertices 73 | colors = self._arrow_colors 74 | elements = self.ARROW_FACE_INDICES 75 | return positions, colors, elements 76 | 77 | def init(self): 78 | self._calculate_arrow_buffer_data() 79 | super().init() 80 | -------------------------------------------------------------------------------- /src/compas_viewer/singleton.py: -------------------------------------------------------------------------------- 1 | class SingletonMeta(type): 2 | _instances = {} 3 | 4 | def __call__(cls, *args, **kwargs): 5 | # Check if this class is a direct subclass of Singleton 6 | if cls.__base__ is Singleton: 7 | key_class = cls 8 | else: 9 | # Walk up the chain to find the first subclass of Singleton 10 | key_class = cls 11 | while key_class.__base__ is not Singleton: 12 | key_class = key_class.__base__ 13 | 14 | if key_class not in cls._instances: 15 | # Create the instance without calling __init__ 16 | instance = cls.__new__(cls) 17 | # Store it immediately so it's available during __init__ 18 | cls._instances[key_class] = instance 19 | # Now call __init__ on the stored instance 20 | instance.__init__(*args, **kwargs) 21 | return cls._instances[key_class] 22 | 23 | 24 | class Singleton(metaclass=SingletonMeta): 25 | """Singleton base class.""" 26 | -------------------------------------------------------------------------------- /src/compas_viewer/timer.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from PySide6.QtCore import QTimer 4 | 5 | 6 | class Timer: 7 | """ 8 | A simple timer that calls a function at specified intervals. 9 | 10 | Parameters 11 | ---------- 12 | interval : int 13 | Interval between subsequent calls to this function, in milliseconds. 14 | callback : Callable 15 | The function to call. 16 | singleshot : bool, optional 17 | If True, the timer is a singleshot timer. 18 | Default is False. 19 | 20 | """ 21 | 22 | def __init__(self, interval: int, callback: Callable, singleshot: bool = False): 23 | self.timer = QTimer() 24 | self.timer.setInterval(interval) 25 | self.timer.timeout.connect(callback) 26 | self.timer.setSingleShot(singleshot) 27 | self.timer.start() 28 | 29 | def stop(self): 30 | self.timer.stop() 31 | -------------------------------------------------------------------------------- /src/compas_viewer/ui.py: -------------------------------------------------------------------------------- 1 | from compas_viewer.base import Base 2 | 3 | from .components.mainwindow import MainWindow 4 | from .components.menubar import MenuBar 5 | from .components.sidedock import SideDock 6 | from .components.statusbar import StatusBar 7 | from .components.toolbar import ToolBar 8 | from .components.viewport import ViewPort 9 | 10 | 11 | class UI(Base): 12 | def __init__(self) -> None: 13 | self.window = MainWindow() 14 | self.menubar = MenuBar(self.window) 15 | self.toolbar = ToolBar(self.window) 16 | self.statusbar = StatusBar(self.window) 17 | self.sidedock = SideDock(self.window) 18 | self.viewport = ViewPort(self.window) 19 | 20 | self.menubar.show = self.viewer.config.ui.menubar.show 21 | self.toolbar.show = self.viewer.config.ui.toolbar.show 22 | self.sidebar.show = self.viewer.config.ui.sidebar.show 23 | self.sidedock.show = self.viewer.config.ui.sidedock.show 24 | 25 | @property 26 | def sidebar(self): 27 | return self.viewport.sidebar 28 | 29 | def init(self): 30 | self.window.resize(self.viewer.config.window.width, self.viewer.config.window.height) 31 | self.window.widget.show() 32 | 33 | self.sidebar.update() 34 | -------------------------------------------------------------------------------- /src/compas_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import Callable 4 | from typing import Optional 5 | 6 | from PySide6.QtCore import QTimer 7 | from PySide6.QtGui import QIcon 8 | from PySide6.QtWidgets import QApplication 9 | 10 | from compas.scene import Scene 11 | from compas_viewer import HERE 12 | from compas_viewer.config import Config 13 | from compas_viewer.events import EventManager 14 | from compas_viewer.mouse import Mouse 15 | from compas_viewer.renderer import Renderer 16 | from compas_viewer.scene import ViewerScene 17 | from compas_viewer.singleton import Singleton 18 | from compas_viewer.ui import UI 19 | 20 | 21 | class Viewer(Singleton): 22 | def __init__(self, config: Optional[Config] = None, **kwargs): 23 | self.app = self.create_app() 24 | self.running = False 25 | self._scene = None 26 | 27 | self.config = config or Config() 28 | self.timer = QTimer() 29 | self.mouse = Mouse() 30 | 31 | self.eventmanager = EventManager() 32 | self.ui = UI() 33 | 34 | @property 35 | def renderer(self) -> Renderer: 36 | return self.ui.viewport.renderer 37 | 38 | @property 39 | def scene(self) -> ViewerScene: 40 | if self._scene is None: 41 | self._scene = ViewerScene() 42 | return self._scene 43 | 44 | @scene.setter 45 | def scene(self, scene: Scene): 46 | self._scene = ViewerScene.__from_data__(scene.__data__) 47 | if self.running: 48 | for obj in self._scene.objects: 49 | obj.init() 50 | 51 | def create_app(self) -> QApplication: 52 | app = QApplication(sys.argv) 53 | app.setApplicationName("COMPAS Viewer") 54 | app.setApplicationDisplayName("COMPAS Viewer") 55 | app.setWindowIcon(QIcon(os.path.join(HERE, "assets", "icons", "compas_icon_white.png"))) 56 | return app 57 | 58 | @property 59 | def unit(self) -> str: 60 | return self.ui.viewport.unit 61 | 62 | @unit.setter 63 | def unit(self, unit: str): 64 | self.ui.viewport.unit = unit 65 | 66 | def show(self): 67 | self.running = True 68 | self.ui.init() 69 | self.app.exec() 70 | 71 | def on(self, interval: int, frames: Optional[int] = None) -> Callable: 72 | """Decorator for callbacks of a dynamic drawing process with fixed intervals. 73 | 74 | Parameters 75 | interval : int 76 | Interval between subsequent calls to this function, in milliseconds. 77 | frames : int, optional 78 | The number of frames of the process. 79 | If no frame number is provided, the process continues until the viewer is closed. 80 | 81 | Returns 82 | ------- 83 | Callable 84 | """ 85 | self.frame_count = 0 86 | 87 | def decorator(func: Callable): 88 | def wrapper(): 89 | if frames is not None and self.frame_count >= frames: 90 | self.timer.stop() 91 | return 92 | func(self.frame_count) 93 | self.frame_count += 1 94 | self.renderer.update() 95 | 96 | self.timer.timeout.connect(wrapper) 97 | self.timer.start(interval) 98 | return func 99 | 100 | return decorator 101 | -------------------------------------------------------------------------------- /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 | tests.testcodeblocks, 21 | build.prepare_changelog, 22 | build.clean, 23 | build.release, 24 | build.build_ghuser_components, 25 | ) 26 | ns.configure( 27 | { 28 | "base_folder": os.path.dirname(__file__), 29 | "ghuser": { 30 | "source_dir": "src/compas_ghpython/components", 31 | "target_dir": "src/compas_ghpython/components/ghuser", 32 | "prefix": "COMPAS: ", 33 | }, 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_placeholder.py: -------------------------------------------------------------------------------- 1 | def test_placeholder(): 2 | assert True 3 | -------------------------------------------------------------------------------- /tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------