diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 0ed6440..538a693 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,120 +1,69 @@ -name: CD Pipeline +name: CI -# The pipeline is triggered by tags being pushed. on: push: - tags: - - 'v*.*.*' # Library version - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false + branches: + - release + workflow_dispatch: jobs: - Continuous_Deployment: + build: runs-on: ubuntu-latest - - # 1. Extract information from the tag (version number, channel). - # 2. Build the python package. - # 3. Update the package to the repo. - steps: - # Checkout the source and the submodules - - name: Checkout repository and submodules - uses: actions/checkout@v3 + steps: + - name: Checkout code + uses: actions/checkout@v2 with: - submodules: recursive - token: ${{ secrets.API_TOKEN_GITHUB }} - persist-credentials: true + submodules: true + fetch-depth: 0 - ############## 1. Extract information from the tag ############## + - name: Update submodules + run: git submodule update --init --recursive + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' - # Check tag - - name: Check tag + - name: Install build dependencies run: | - echo "Checking tag: ${GITHUB_REF_NAME}" - source ./scripts/github-tag - CheckTag ${GITHUB_REF_NAME} - - # Get version number - - name: Tag -> Version + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Transpile Schema run: | - source scripts/github-tag - echo "PACKAGE_VERSION=$(VersionFromTag ${GITHUB_REF_NAME})" >> $GITHUB_ENV + python ./scripts/build_proto.py - # Get the release channel: stable vs testing - - name: Tag -> Channel - run: | - source scripts/github-tag - echo "PACKAGE_CHANNEL=$(ChannelFromTag ${GITHUB_REF_NAME})" >> $GITHUB_ENV + - name: Build the package + run: python -m build - # Print out info - - name: Package Info + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - echo "Version: $PACKAGE_VERSION" - echo "Channel: $PACKAGE_CHANNEL" - - - ############## 2. Build the python package. ############## + pip install twine + twine upload --verbose --repository pypi dist/* - # Install dependencies - - name: Install dependencies + - name: Commit and push changes run: | - pip3 install -r requirements.txt - pip3 install twine - - # Set version - - name: Set Version - run: echo "__version__ = \"${{ env.PACKAGE_VERSION }}\"" > maf_three/__init__.py - - # Build proto files - - name: Build proto files - run: python3 ./scripts/build-proto.py - - # Build - - name: Build - run: python3 -m build - - # Install our package (required for the tests to find the namespace : MF.V3) - - name: Install - run: pip3 install ./dist/maf_three-${{ env.PACKAGE_VERSION }}-py3-none-any.whl - - # Run the tests - - name: Test - run: python3 -m pytest - - # Build the documentation - - name: Documentation - run: python3 ./scripts/build-doc.py - - - ############## 3. Upload the package and the documentation. ############## - - # Upload python package to testpypi - - name: Upload to TestPyPi - run: twine upload --repository testpypi dist/* -u__token__ -p${{ secrets.TESTPYPI_TOKEN }} - #run: twine upload dist/* -u__token__ -p${{ secrets.PYPI_TOKEN }} - - - # Upload the documentation - - name: Setup Pages - uses: actions/configure-pages@v5 + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@matterandform.net' + git add . + git commit -m "Publish new version to PyPi [skip ci]" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version + id: extract_version + run: | + CURRENT_VERSION=$(grep -oP "(?<=__version__ = ')[^']*" ./three/__init__.py) + echo "TAG=$CURRENT_VERSION" >> $GITHUB_ENV - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + - name: Create a Release + uses: softprops/action-gh-release@v1 with: - path: './doc/build/html' - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - - + files: dist/* + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ env.TAG }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a424e3e..49448cc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,69 +1,76 @@ -name: CI Pipeline +name: CI -# Triggers of the Action: Push or Pull Request on: - pull_request: - branches: [ develop ] - types: [opened, review_requested, ready_for_review, synchronize] - - # Allows you to run this workflow manually from the Actions tab (when this yml is in the main branch) + push: + branches: + - develop workflow_dispatch: jobs: build: - if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} - strategy: - fail-fast: false # allow other OS to keep running when one fails - matrix: # Linux ; MacOS ; Windows - sys: - - { os: ubuntu-latest, shell: bash} - #- { os: windows-latest, shell: 'msys2 {0}' } - #- { os: macos-latest, shell: bash} - - # Set the default shell - defaults: - run: - shell: ${{ matrix.sys.shell }} - - runs-on: ${{ matrix.sys.os }} - -#################################################################### + runs-on: ubuntu-latest + steps: - - # Checkout the source and the submodules - - name: Checkout repository and submodules - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v2 with: - submodules: recursive - token: ${{ secrets.API_TOKEN_GITHUB }} - persist-credentials: true + submodules: true + fetch-depth: 0 - # Install dependencies - - name: Install dependencies - run: pip3 install -r requirements.txt - - # Set version - - name: Set Version - run: echo "__version__ = \"0.0.0\"" > maf_three/__init__.py + - name: Update submodules + run: git submodule update --init --recursive + + - name: Increment Version + run: | + ./scripts/incrementVersion.sh - # Build proto files - - name: Build proto files - run: python3 ./scripts/build-proto.py + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' - # Build - - name: Build - run: python3 -m build + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Transpile Schema + run: | + python ./scripts/build_proto.py - # Install our package (required for the tests to find the namespace : MF.V3) - - name: Install - run: pip3 install ./dist/maf_three-0.0.0-py3-none-any.whl + - name: Build the package + run: python -m build - # Run the tests - - name: Test - run: python3 -m pytest + - name: Publish to TestPyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: | + pip install twine + twine upload --verbose --repository testpypi dist/* - # Build the documentation - - name: Documentation - run: python3 ./scripts/build-doc.py + - name: Commit and push changes + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@matterandform.net' + git add . + git commit -m "CI: Version bump and transpiled code [skip ci]" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Extract version number + id: extract_version + run: | + CURRENT_VERSION=$(grep -oP "(?<=__version__ = ')[^']*" ./three/__init__.py) + echo "VERSION=$CURRENT_VERSION" >> $GITHUB_ENV + - name: Create Pull Request to merge develop into release + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "CI: Merge develop into release" + base: "develop" + branch: "release" + title: "New Release Candidate v${{ env.VERSION }}" + body: "This is an new release candidate for three-python. Merging this will distribute the new version to pypi." diff --git a/.gitignore b/.gitignore index d0f94f3..d3c20c4 100644 --- a/.gitignore +++ b/.gitignore @@ -160,9 +160,4 @@ cython_debug/ #.idea/ -## Matter And Form ## - -# Don't include protobuf generated files. -maf_three/MF - - +## Matter And Form ## \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e70e581 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Transpile", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/scripts/transpileProto.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "justMyCode": true + }, + { + "name": "Python: Simple Scanner Example", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/three/examples/simpleScanner.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "justMyCode": true + } + { + "name": "Python: Task Example", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/three/examples/task.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "justMyCode": true + }, + { + "name": "Python: Calibrate Turntable Example", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/three/examples/turntableCalibration.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "justMyCode": true + }, + { + "name": "Python: Setup Wheel", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/three/setup.py", + "args": ["sdist", "bdist_wheel"], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "justMyCode": true + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0cd4f3a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Matter and Form + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 71b4404..e4f2091 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,129 @@ +# Matter and Form THREE Library -# Setup +## Overview +The Matter and Form THREE library provides a comprehensive API for controlling and interacting with the Matter and Form THREE scanner. This library allows developers to build custom integrations, automate tasks, and create new front-end systems for 3D scanning. -## Install required packages +## Setup -``` -sudo apt install python3 python3-pip python3.12-venv -``` +### Install Required Packages +Ensure you have Python 3.10 or newer installed. You can download it from [python.org](https://www.python.org). -## Start and activate a virtual Python environment with the build dependencies +### Initialize Git Submodules +If you haven't already initialized the git submodules, run the following command: -``` -python3 -m venv .venv -source .venv/bin/activate -pip3 install -r requirements.txt +```sh +git submodule update --init --recursive ``` -# Build +### Start and Activate a Virtual Python Environment +Create and activate a virtual environment: -## Build python source from proto files +```sh +python -m venv .venv +source .venv/bin/activate # On Windows use `venv\Scripts\activate` ``` -python3 ./scripts/build-proto.py + +### Install Requirements +Install the required packages listed in the `requirements.txt` file: + +```sh +pip install -r requirements.txt ``` -## Package Build +### Build + +#### Build Python Source from Proto Files +This is only necessary if you have an update the schema. Keep in mind that schema's are tied to THREE server releases. Generated files are commited to this repo.To generate the Python source files again from the Schema files, run: + +```sh +python3 ./scripts/build_proto.py ``` -python3 -m build + +#### Package Build +To build the package, run: + +```sh +python setup.py sdist bdist_wheel ``` -## Install the package locally in editable mode +#### Install the Package Locally in Editable Mode +To install the package locally in editable mode, run: + +```sh +pip install -e . ``` -pip3 install --editable . + +#### Run the Tests +At the moment there are no unit tests + +#### Build the Documentation +To build the documentation, run: + +```sh +TODO ``` -## Run the tests +## How to Use the Library + +### Installation from PyPI +To install the library from PyPI, run: + +```sh +pip install matter-and-form-three ``` -python3 -m pytest + +### Connect to the Scanner +To connect to the scanner, you can use the provided examples. For instance, to run the connection example, execute: + +```sh +python examples/connect.py ``` -## Build the documentation +### Example Usage +Here is an example of how to use the library to connect to the scanner and control the projector: + +```python +from matter_and_form_three import Scanner + +# Create and connect to the scanner +scanner = Scanner(OnTask=None, OnMessage=None, OnBuffer=None) +# Use the Zeroconf address or replace with the ip +scanner.Connect("ws://matterandform.local:8081") + +# Simple request to list all projects +projectTask = scanner.list_projects() + +# Check the output from the task for errors +if projectTask.Error: + print('Error:', projectTask.Error) + return +# Do something with the output +for project_obj in projectTask.Output: + project = Project.Brief(**project_obj) + print('Project index:', project.index, ' - Name:', project.name) ``` -python3 ./scripts/build-doc.py + +### Available Examples +The library comes with several pre-made examples. You can find them in the [examples directory](https://github.com/Matter-and-Form/three-python-library/tree/develop/three/examples). + +To run a specific example, use: + +```sh +python three/examples/.py ``` +## Documentation +For detailed documentation, visit the TODO -> [official documentation](TODO). + +## Contributing +We welcome contributions! Please follow the standard GitHub workflow: +1. Fork the repository. +2. Create a new branch (`git checkout -b feature-branch`). +3. Make your changes. +4. Commit your changes (`git commit -am 'Add new feature'`). +5. Push to the branch (`git push origin feature-branch`). +6. Create a new Pull Request. +## License +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/V3Schema b/V3Schema index 32731d7..9edef30 160000 --- a/V3Schema +++ b/V3Schema @@ -1 +1 @@ -Subproject commit 32731d7f8de74c087f4d935b68b23716026cdf6b +Subproject commit 9edef307182fa13a1cac87968a9b342f5ba09194 diff --git a/doc/source/_static/css/custom.css b/doc/source/_static/css/custom.css deleted file mode 100644 index 6f0d66c..0000000 --- a/doc/source/_static/css/custom.css +++ /dev/null @@ -1,7 +0,0 @@ - - -/* Override css for the toctree items generated by autodoc. */ -.rst-content code,.rst-content tt,code { - font-size: 100%; - font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace; -} \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index f4a62bb..0000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,41 +0,0 @@ - - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'maf_three library' -copyright = '2024, Matter and Form' -author = 'Matter and Form' - -# Add the path to maf_three (see autodoc can find it) -import os -import sys -sys.path.insert(0, os.path.abspath('../..')) - - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [ - 'myst_parser', - 'sphinx.ext.doctest', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - #'sphinx.ext.napoleon' -] - -# Keep the member order as defined in the source -autodoc_member_order='bysource' - -source_suffix = ['.rst', '.md'] - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' - -# These folders are copied to the documentation's HTML output -html_static_path = ['_static'] - -# These paths are relative to html_static_path -html_css_files = [ - 'css/custom.css', -] \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index a7bc641..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,19 +0,0 @@ - -Welcome to maf_three library's documentation! -============================================= - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - ./pages/quickStart.md - ./pages/examples.md - ./pages/modules.rst - - -.. Indices and tables -.. ================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/source/pages/examples.md b/doc/source/pages/examples.md deleted file mode 100644 index 2d93dad..0000000 --- a/doc/source/pages/examples.md +++ /dev/null @@ -1,47 +0,0 @@ - -# Examples - -**maf_three** comes with pre made examples. To list the available examples, call: - -`python3 -m maf_three.examples list` - -To run a specific example call: - -`python3 -m maf_three.examples example_name` - -## connection -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/connection.py - :linenos: -``` - -## projector -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/projector.py - :linenos: -``` - -## turntable -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/turntable.py - :linenos: -``` - -## task -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/task.py - :linenos: -``` -## turntableCalibration -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/turntableCalibration.py - :linenos: -``` - -## simpleScanner -```{eval-rst} -.. literalinclude:: ../../../maf_three/examples/simpleScanner.py - :linenos: -``` - - diff --git a/doc/source/pages/modules.rst b/doc/source/pages/modules.rst deleted file mode 100644 index 80bf817..0000000 --- a/doc/source/pages/modules.rst +++ /dev/null @@ -1,63 +0,0 @@ - -============================================= -Modules -============================================= - - ----------------------------- - maf_three ----------------------------- -.. automodule:: maf_three.scanner - :members: - :undoc-members: - -.. automodule:: maf_three.V3Task - :members: - :undoc-members: - -.. automodule:: maf_three.task - :members: - :undoc-members: - -.. automodule:: maf_three.buffer - :members: - :undoc-members: - -.. automodule:: maf_three.serialization - :members: - - ----------------------------- -MF.V3.Settings ----------------------------- -.. automodule:: maf_three.MF.V3.Settings - :members: - :undoc-members: - - ----------------------------- -MF.V3.Descriptors ----------------------------- -.. automodule:: maf_three.MF.V3.Descriptors - :members: - :undoc-members: - -.. automodule:: maf_three.MF.V3.Descriptors.Calibration - :members: - :undoc-members: - -.. automodule:: maf_three.MF.V3.Descriptors.Network - :members: - :undoc-members: - - ----------------------------- -MF.V3.Descriptors.Settings ----------------------------- - -.. automodule:: maf_three.MF.V3.Descriptors.Settings - :members: - :undoc-members: - - - diff --git a/doc/source/pages/quickStart.md b/doc/source/pages/quickStart.md deleted file mode 100644 index d3c64fb..0000000 --- a/doc/source/pages/quickStart.md +++ /dev/null @@ -1,11 +0,0 @@ - - - -# Quick start - -## Installation - -`pip install maf_three` - -## Connect to the scanner -`python3 -m maf_three.examples connection` \ No newline at end of file diff --git a/maf_three/V3Task.py b/maf_three/V3Task.py deleted file mode 100644 index 1a17c58..0000000 --- a/maf_three/V3Task.py +++ /dev/null @@ -1,212 +0,0 @@ - - -from enum import Enum - -class V3Task(str, Enum): - - """Create a new test scan""" - NewTestScan = 'NewTestScan', - - """Download a file from the server workspace""" - DownloadFile = 'DownloadFile', - - - - - """Get filesystem capacity and availability""" - DiskSpace = 'DiskSpace', - - """Software installed version.""" - SoftwareVersionInstalled = 'SoftwareVersionInstalled', - - """Software available version""" - SoftwareVersionAvailable = 'SoftwareVersionAvailable', - - """Get a still image from each camera""" - StillImage = 'StillImage', - - """Get a stream image from each camera""" - StreamImage = 'StreamImage', - - """Get the camera calibration descriptor for the current focus values""" - CameraCalibration = 'CameraCalibration', - - """Get the turntable calibration descriptor""" - TurntableCalibration = 'TurntableCalibration', - - """Get the camera calibration capture targets""" - CalibrationCaptureTargets = 'CalibrationCaptureTargets', - - """Calibrate cameras""" - CalibrateCameras = 'CalibrateCameras', - - """Calibrate turntable""" - CalibrateTurntable = 'CalibrateTurntable', - - """Start video stream""" - StartVideo = 'StartVideo', - - """Stop video stream""" - StopVideo = 'StopVideo', - - """Apply camera settings cameras""" - SetCameras = 'SetCameras', - - """Apply projector settings""" - SetProjector = 'SetProjector', - - """Check if the server can connect to cameras""" - HasCameras = 'HasCameras', - - """Check if the server can connect to a projector""" - HasProjector = 'HasProjector', - - """Check if the server can connect to a turntable""" - HasTurntable = 'HasTurntable', - - """Auto focus the cameras""" - AutoFocus = 'AutoFocus', - - """Rotate the turntable""" - RotateTurntable = 'RotateTurntable', - - """Detect the calibration card""" - DetectCalibrationCard = 'DetectCalibrationCard', - - """Get the set of projects in the workspace""" - ListProjects = 'ListProjects', - - """Download a project from the scanner""" - DownloadProject = 'DownloadProject', - - """Upload a project to the scanner""" - UploadProject = 'UploadProject', - - """Set project properties""" - SetProject = 'SetProject', - - """Create a new project""" - NewProject = 'NewProject', - - """Open an existing project""" - OpenProject = 'OpenProject', - - """Close an opened project""" - CloseProject = 'CloseProject', - - """Remove selected projects from the workspace""" - RemoveProjects = 'RemoveProjects', - - """Clear the available project undo and redo actions""" - ClearProjectActions = 'ClearProjectActions', - - """List the available project undo and redo actions""" - ListProjectActions = 'ListProjectActions', - - """Undo a number of project actions""" - UndoProjectActions = 'UndoProjectActions', - - """Redo a number of project actions""" - RedoProjectActions = 'RedoProjectActions', - - """List the scans in the current project""" - ListScans = 'ListScans', - - """Get geometry data for a selected scan""" - ScanData = 'ScanData', - - """Capture and process a new scan""" - NewScan = 'NewScan', - - """List the groups in the current project.""" - ListGroups = 'ListGroups', - - """Set scan group properties.""" - SetGroup = 'SetGroup', - - """Create a scan group.""" - NewGroup = 'NewGroup', - - """Move a scan group.""" - MoveGroup = 'MoveGroup', - - """Flatten a scan group such that it only consists of single scans.""" - FlattenGroup = 'FlattenGroup', - - """Split a scan group (ie move its subgroups to its parent group).""" - SplitGroup = 'SplitGroup', - - """Remove scan groups.""" - RemoveGroups = 'RemoveGroups', - - """Apply a rigid transformation to a group.""" - TransformGroup = 'TransformGroup', - - """Get the bounding box of a set of scan groups.""" - BoundingBox = 'BoundingBox', - - """Align two scan groups.""" - Align = 'Align', - - """Align two scan groups.""" - Merge = 'Merge', - - """Get geometry data for a the current merged scan.""" - MergeData = 'MergeData', - - """Add a merged scan to the current project.""" - AddMergeToProject = 'AddMergeToProject', - - """List export formats and their supported geometry elements.""" - ListExportFormats = 'ListExportFormats', - - """Export a group of scan.""" - Export = 'Export', - - """Export a merged scan.""" - ExportMerge = 'ExportMerge', - - """Export a backend logs.""" - ExportLogs = 'ExportLogs', - - """Get the global settings.""" - ListSettings = 'ListSettings', - - """Get the global settings.""" - PushSettings = 'PushSettings', - - """Get the global settings.""" - PopSettings = 'PopSettings', - - """Save the global settings.""" - UpdateSettings = 'UpdateSettings', - - """Reboot the scanner.""" - Reboot = 'Reboot', - - """Shutdown the scanner.""" - Shutdown = 'Shutdown', - - """List the available Wifi networks.""" - ListWifi = 'ListWifi', - - """Connect to a specific Wifi network.""" - ConnectWifi = 'ConnectWifi', - - """Query the access point status.""" - AccessPointStatus = 'AccessPointStatus', - - """List the network interfaces.""" - ListNetworkInterfaces = 'ListNetworkInterfaces', - - """Remove Vertices.""" - RemoveVertices = 'RemoveVertices', - - """Disable ethernet connection.""" - DisableEthernet = 'DisableEthernet', - - """Disable wifi connection.""" - DisableWifi = 'DisableWifi', - - """Forget wifi connection.""" - ForgetWifi = 'ForgetWifi' diff --git a/maf_three/__init__.py b/maf_three/__init__.py deleted file mode 100644 index 8c0d5d5..0000000 --- a/maf_three/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.0.0" diff --git a/maf_three/buffer.py b/maf_three/buffer.py deleted file mode 100644 index ab7c8b8..0000000 --- a/maf_three/buffer.py +++ /dev/null @@ -1,23 +0,0 @@ -# buffer.py - - -from maf_three.task import Task - - -class Buffer: - """Class representing a buffer.""" - - Index: int - """The index of the buffer.""" - - totalBufferSize: int - """The total size of the buffer that will be sent.""" - - task:Task - """The related task.""" - - - def __init__(self, bufferIndex:int,totalBufferSize:int, task:Task, Input = None): - self.Index = bufferIndex - self.totalBufferSize = totalBufferSize - self.Task = task diff --git a/maf_three/examples/projector.py b/maf_three/examples/projector.py deleted file mode 100644 index 8c46ad2..0000000 --- a/maf_three/examples/projector.py +++ /dev/null @@ -1,79 +0,0 @@ -# Projector - -import time -import numpy as np - -from maf_three.scanner import Scanner -from maf_three.V3Task import V3Task - -from MF.V3.Settings.Projector import Projector -from MF.V3.Settings.Rectangle import Rectangle -from MF.V3.Settings.Video import Video - -def main(): - - try: - scanner = Scanner(OnTask=None, OnMessage=None, OnBuffer=None) - scanner.Connect("ws://matterandform.local:8081") - - #### Turn ON - print('Turn ON') - scanner.SendTask(0, V3Task.SetProjector, Projector(on=True, brightness=1.0, color=[1,1,1])) - time.sleep(1) - - #### Project Red - print('Project Red') - scanner.SendTask(1, V3Task.SetProjector, Projector(color=[1,0,0])) - time.sleep(1) - - #### Project Green - print('Project Green') - scanner.SendTask(2, V3Task.SetProjector, Projector(color=[0,1,0])) - time.sleep(1) - - #### Project Blue - print('Project Blue') - scanner.SendTask(3, V3Task.SetProjector, Projector(color=[0,0,1])) - time.sleep(1) - - #### Project Vertical Pattern - print('Project Vertical Pattern (Identical image columns)') - scanner.SendTask(4, V3Task.SetProjector, Projector(pattern=Projector.Pattern(orientation=Projector.Orientation.Vertical,frequency=4,phase=1))) - time.sleep(1) - - #### Project Horizontal Pattern - print('Project Horizontal Pattern (Identical image rows)') - scanner.SendTask(5, V3Task.SetProjector, Projector(pattern=Projector.Pattern(orientation=Projector.Orientation.Horizontal,frequency=4,phase=1))) - time.sleep(1) - - ### Project an image - print('Project Image') - width = 640 - height = 480 - image = np.zeros([height, width, 3], np.uint8) - for y in range(height): - for x in range(0, width): - image[y,x] = ( - 255 * y / height , # Blue - 255 * x / width , # Green - 255 - 255 * y / height # Red - ) - source = Projector.Image.Source(format = Video.Format.BGR888, width=width, height=height, step=3*width, fixAspectRatio=True) - scanner.SendTaskWithBuffer(6, V3Task.SetProjector, image.tobytes(), Projector(image=Projector.Image(source=source, target=Rectangle(x=100,y=100,width=640, height=480))) ) - time.sleep(1) - - #### Turn OFF - print('Turn OFF') - scanner.SendTask(7, V3Task.SetProjector, Projector(on=False)) - - except Exception as error: - print('Error: ', error) - except: - print('Error') - - finally: - if scanner.IsConnected(): - scanner.Disconnect() - -if __name__ == "__main__": - main() diff --git a/maf_three/examples/simpleScanner.py b/maf_three/examples/simpleScanner.py deleted file mode 100644 index c84f3fa..0000000 --- a/maf_three/examples/simpleScanner.py +++ /dev/null @@ -1,165 +0,0 @@ -# Simple Scanner - -import numpy as np - -# Three library -from maf_three.V3Task import V3Task -from maf_three.scanner import Scanner -from maf_three.task import Task, TaskState - -from MF.V3.Settings.Camera import Camera -from MF.V3.Settings.Capture import Capture -from MF.V3.Settings.Projector import Projector -from MF.V3.Settings.Turntable import Turntable -from MF.V3.Settings.Scan import Scan - -# Two frames for the video stream -frame0 = np.zeros((0,0,3), np.uint8) -frame1 = np.zeros((0,0,3), np.uint8) - -# Camera/Projector settings -camera = Camera(exposure=50000, digitalGain=256, analogGain=256.0) -projector = Projector(brightness=0.5) -turntable = Turntable(use=False) - -def main(): - - # OpenCV - try: - import cv2 - except ModuleNotFoundError as error: - print('###############################################') - print('This example required OpenCV for Python') - print('To install (apt or pip):') - print(' * sudo apt install python3-opencv') - print(' * pip3 install opencv-python') - print('###############################################') - exit(1) - - ControlsWindow = 'Controls' - Camera0Window = 'Camera0' - Camera1Window = 'Camera1' - - # Task update - def OnTask(task:Task): - - if task.State == TaskState.Completed: - # New Test Scan - if(task.Type == V3Task.NewTestScan): - if task.Output: - print('Scan Completed -> Requesting the data') - index = task.Output[0] - filePath = f'TestScans/Scan-{index}/Scan-{index}.ply' - scanner.SendTask(1, V3Task.DownloadFile, filePath ) - - elif task.State == TaskState.Failed: - print('Failed Task: ', task) - - # Buffer received - def OnBuffer(descriptor, buffer:bytes): - global frame0, frame1 - - # Video task - if descriptor['Task']['Index'] == -1: - if descriptor['Index'] == 0: - frame0 = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR) - else: - frame1 = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR) - - # DownloadFile - elif descriptor['Task']['Index'] == 1: - with open('scan.ply', 'wb') as binary_file: - binary_file.write(buffer) - print('Scan saved into scan.ply') - - - def OnTrackbarExposure(value): - global camera - camera.exposure = value - if scanner.IsConnected(): scanner.SendTask(99, V3Task.SetCameras, Camera(exposure=value)) - - def OnTrackbarAnalogGain(value): - global camera - camera.analogGain = value - if scanner.IsConnected(): scanner.SendTask(99, V3Task.SetCameras, Camera(analogGain=value)) - - def OnTrackbarDigitalGain(value): - global camera - camera.digitalGain = value - if scanner.IsConnected(): scanner.SendTask(99, V3Task.SetCameras, Camera(digitalGain=value)) - - def OnTrackbarProjectorBrightness(value): - global projector - projector.brightness = float(value / 100) - if scanner.IsConnected(): scanner.SendTask(112, V3Task.SetProjector, Projector(brightness=value/100)) - - def OnTrackbarUseTurntable(value): - global turntable - turntable.use = value == 1 - - def OnTrackbarTurntableSweep(value): - global turntable - turntable.sweep = value - - def OnTrackbarTurntableSteps(value): - global turntable - turntable.steps = value - - try: - # Connect to the scanner - scanner = Scanner(OnTask=OnTask, OnBuffer=OnBuffer, OnMessage=None) - scanner.Connect("ws://matterandform.local:8081") - - # Create the UI - cv2.namedWindow(ControlsWindow) - cv2.namedWindow(Camera0Window) - cv2.namedWindow(Camera1Window) - cv2.moveWindow(ControlsWindow,0, 550) - cv2.moveWindow(Camera0Window,0,100) - cv2.moveWindow(Camera1Window,550,100) - cv2.createTrackbar('Exposure', ControlsWindow , camera.exposure, 100000, OnTrackbarExposure) - cv2.createTrackbar('Analog Gain', ControlsWindow , int(camera.analogGain), 1024, OnTrackbarAnalogGain) - cv2.createTrackbar('Digital Gain', ControlsWindow , camera.digitalGain, 1024, OnTrackbarDigitalGain) - cv2.createTrackbar('Projector Brightness', ControlsWindow , int(100 * projector.brightness), 100, OnTrackbarProjectorBrightness) - cv2.createTrackbar('Use Turntable', ControlsWindow , 1 if turntable.use else 0, 1, OnTrackbarUseTurntable) - cv2.createTrackbar('Turntable Sweep', ControlsWindow , 180, 360, OnTrackbarTurntableSweep) - cv2.createTrackbar('Turntable Steps', ControlsWindow , 0, 32, OnTrackbarTurntableSteps) - - # Turn on the projector and start the video - scanner.SendTask(112, V3Task.SetProjector, Projector(color=[1,1,1], on=True, brightness=projector.brightness)) - scanner.SendTask(-1, V3Task.StartVideo) - - # User input loop - print('Press "Esc" to quit.') - while True: - - # If present => Show the frames - if frame0.size > 0: - cv2.imshow(Camera0Window,frame0) - if frame1.size > 0: - cv2.imshow(Camera1Window,frame1) - - # User input - key = cv2.waitKey(1) - if(key != -1): - - if key == 27: # Esc => Break the loop - break - - elif key == 115: # 's' => Create a new Test Scan - scan = Scan( - camera=camera, - capture=Capture(), - projector=projector, - turntable=turntable) - scanner.SendTask(115, V3Task.NewTestScan, scan) - - except Exception as error: - print('Error: ', error) - - - scanner.Disconnect() - cv2.destroyAllWindows() - -if __name__ == "__main__": - main() diff --git a/maf_three/examples/turntable.py b/maf_three/examples/turntable.py deleted file mode 100644 index 2f03fc6..0000000 --- a/maf_three/examples/turntable.py +++ /dev/null @@ -1,70 +0,0 @@ -# Turntable - -from maf_three.scanner import Scanner -from maf_three.task import Task, TaskState -from maf_three.V3Task import V3Task - -import time - -hasTurntable = None -rotating = False - -def main(): - global hasTurntable, rotating - - def OnTask(task:Task): - global hasTurntable, rotating - - # HasTurntable - if task.Type == V3Task.HasTurntable and task.State == TaskState.Completed: - print('Completed : ', task.Output) - hasTurntable = task.Output - - # RotateTurntable - elif task.Type == V3Task.RotateTurntable: - if task.State == TaskState.Started: - rotating = True - elif task.State == TaskState.Completed: - rotating = False - - try: - scanner = Scanner(OnTask=OnTask, OnMessage=None, OnBuffer=None) - scanner.Connect("ws://matterandform.local:8081") - - # Check if a Turntable is connected to the scanner - scanner.SendTask(0, V3Task.HasTurntable) - while hasTurntable == None: - #print(hasTurntable) - pass - if hasTurntable == False: - raise Exception('There is no turntable connected to the scanner') - - while True: - - while rotating: - time.sleep(0.1) - - userInput = input('Enter an angle or "q" to quit\n') - - if userInput.lower() == 'q': - break - - try: - angle = float(userInput) - rotating = True - scanner.SendTask(0, V3Task.RotateTurntable, angle) - except: - pass - - - except Exception as error: - print('Error: ', error) - except: - print('Error') - - finally: - if scanner.IsConnected(): - scanner.Disconnect() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/maf_three/examples/turntableCalibration.py b/maf_three/examples/turntableCalibration.py deleted file mode 100644 index d89fd38..0000000 --- a/maf_three/examples/turntableCalibration.py +++ /dev/null @@ -1,114 +0,0 @@ -# Turntable calibration - -import time - -from maf_three.scanner import Scanner -from maf_three.task import Task, TaskState -from maf_three.V3Task import V3Task - - -done = False -cornersDetected_0 = 0 # Amount of corners detected on camera 0 -cornersDetected_1 = 0 # Amount of corners detected on camera 1 -cornersTotal = 0 # Total number of corners to be detected - -def main(): - - def OnTask(task:Task): - global done - - # Calibrate turntable task - if task.Type == V3Task.CalibrateTurntable: - # Task progress - if task.Progress != None: - progress = task.Progress['CalibrateTurntable'] - print(progress['current'] , '/', progress['total'], '-', progress['step']) - - # Task completed ? - if task.State == TaskState.Completed: - print('Calibration Completed') - print(task.Output) - done = True - # Task failed ? - elif task.State == TaskState.Failed: - print('Calibration Failed') - print('Calibration error:', task.Error) - done = True - - def OnBuffer(descriptor, buffer:bytes): - global cornersDetected_0, cornersDetected_1, cornersTotal - - # Video task ? - if descriptor['Task']['Index'] == -1: - # Calibration card present in the descriptor - if "calibrationCard" in descriptor['Descriptor']: - calibrationCard = descriptor['Descriptor']['calibrationCard'] - - # Total amount of corners - if cornersTotal == 0: - cardWidth = calibrationCard['size'][0] - cardHeight = calibrationCard['size'][1] - cornersTotal = (cardWidth - 1) * (cardHeight - 1) - - - detectedCorners = len(calibrationCard['corners']) - # Camera 0 - if descriptor['Index'] == 0: - cornersDetected_0 = detectedCorners - # Camera 1 - else: - cornersDetected_1 = detectedCorners - - # No calibration card in the descriptor - else: - if descriptor['Index'] == 0: - cornersDetected_0 = 0 - else: - cornersDetected_1 = 0 - - print(f'Camera 0: {cornersDetected_0}/{cornersTotal} ; Camera 1: {cornersDetected_1}/{cornersTotal} ', end="\r", flush=True) - - try: - # Connect - scanner = Scanner(OnTask=OnTask, OnMessage=None, OnBuffer=OnBuffer) - scanner.Connect("ws://matterandform.local:8081") - - # Start the video - scanner.SendTask(-1, V3Task.StartVideo) - - # Detect the calibration card - print('******* Detecting the calibration card') - scanner.SendTask(0, V3Task.DetectCalibrationCard, 3) - - # Wait for the calibration card to be detected - while cornersTotal == 0: - time.sleep(0.1) - - # Detect the calibration card for 5sec - time.sleep(5) - - # Stop the video - scanner.SendTask(-1, V3Task.StopVideo) - time.sleep(1) - - # Calibration the turntable - print('\n******* Calibrating the turntable') - scanner.SendTask(1, V3Task.CalibrateTurntable) - - # Wait for the tasks to finish - while not done: - time.sleep(0.1) - - - except Exception as error: - print('Error: ', error) - except: - print('Error') - - finally: - if scanner.IsConnected(): - scanner.Disconnect() - - -if __name__ == "__main__": - main() diff --git a/maf_three/serialization.py b/maf_three/serialization.py deleted file mode 100644 index 01d7087..0000000 --- a/maf_three/serialization.py +++ /dev/null @@ -1,42 +0,0 @@ -# serialization.py - -from array import * -import json - -from google.protobuf.json_format import MessageToDict - - -def Serializer(object): - # Array is not JSON serializable => Convert to list - if isinstance(object, array): - return object.tolist() - - # Protobuf object - if hasattr(object, "DESCRIPTOR"): - dic = MessageToDict( - object, - preserving_proto_field_name=True, - including_default_value_fields=True - ) - return dict(filter(lambda tup:tup[1] is not None, dic.items())) - - # Our objects - return dict(filter(lambda tup:tup[1] is not None, object.__dict__.items())) - - -def TO_JSON(object) -> str: - """ - Serialize an object into a json string. - - Args: - object: the object to serialize. - - Returns: - The string representing the object. - - """ - - return json.dumps( - object, - default=Serializer, - allow_nan=False) diff --git a/maf_three/task.py b/maf_three/task.py deleted file mode 100644 index fb3ded5..0000000 --- a/maf_three/task.py +++ /dev/null @@ -1,83 +0,0 @@ - - -from enum import Enum - -from maf_three.V3Task import V3Task - - -class TaskState(str, Enum): - - - Sent = 'Sent', - """The task has been sent by the client.""" - - Received = 'Received', - """The task has been received by the server.""" - - Started = 'Started', - """The task started by the server.""" - - Completed = 'Completed', - """The task is completed by the server.""" - - Cancelled = 'Cancelled', - """The task has been cancelled by the client.""" - - Failed = 'Failed', - """The task has failed, A string describing the error is returned with the task.""" - - Dropped = 'Dropped', - """The task has not been received by the server, or task IDs were sent out of sequence.""" - - Disconnected = 'Disconnected' - """The client has been disconnected from the server before the task could finish.""" - -class Task: - """ - Class representing a task. - - The typical lifecycle is : - - - Client creates the task. - - Client sends it to the Server. - - Server starts processing the task. - - Server notifies the Client that the processing has started. - - Server finishes processing the task. - - Server notifies the Client that the processing is done. - - Client handles an optional output associated with the task. - """ - - Index: int - """The index of the task. Set by the client only.""" - - Type:V3Task - """The type of the task. Set by the client only.""" - - Input = None - """The input of the task. Set by the client only.""" - - Output = None - """The output of the task. Set by the server only.""" - - State: TaskState | None - """The state of the task. Set by the server only.""" - - Progress: None - """The progress of the task. Set by the server only.""" - - Error: None - """Error related to the task. Set by the server only.""" - - def __init__(self, Index:int, Type:V3Task, Input=None, Output=None, State=None, Progress=None, Error=None ): - self.Index = Index - self.Type = Type - - self.Input = Input - self.Output = Output - - self.State = State - self.Progress = Progress - self.Error = Error - - def __str__(self): - return f'"{self.Index} - {self.Type}" State: {self.State}; Input: {self.Input}; Output: {self.Output}' \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0d915fc..a9d5e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,9 @@ build-backend = "setuptools.build_meta" # Package info and dependencies [project] -name = "maf_three" +name = "mfthree" description = "Matter and Form - THREE - Library" +readme = "./three/PYPI_README.md" dynamic = ["version"] classifiers = [ "Programming Language :: Python :: 3", @@ -15,29 +16,31 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ 'importlib-metadata; python_version<"3.10"', - 'protobuf == 4.25.3', + 'protobuf >= 5.0.0', 'websocket-client >= 1.7.0', 'numpy >= 1.26.4' ] # URLs [project.urls] -Homepage = "https://matterandform.net" -Documentation = "https://matterandform.net" +Homepage = "https://github.com/Matter-and-Form/three-python-library" +Documentation = "https://github.com/Matter-and-Form/three-python-library/wiki" # Package source [tool.setuptools.package-dir] -maf_three = "maf_three" -"MF" = "maf_three/MF" +three = "three" +"MF" = "three/MF" # Dynamic input [tool.setuptools.dynamic] -version = {attr = "maf_three.__version__"} +version = {attr = "three.__version__"} # Command line accessible scripts # Usage: -# python3 -m maf_three.examples connection -# python3 -m maf_three.examples projector -# python3 -m maf_three.examples simpleScanner +# python3 -m three.examples connection +# python3 -m three.examples projector +# python3 -m three.examples simpleScanner +# python3 -m three.examples task +# python3 -m three.examples turntableCalibration [project.scripts] -examples = "maf_three.examples:main_cli" \ No newline at end of file +examples = "three.examples:main_cli" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c68b7d6..7780874 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,9 @@ -alabaster==0.7.16 -Babel==2.15.0 -betterproto==2.0.0b6 -black==24.4.2 -build==1.2.1 -certifi==2024.2.2 -charset-normalizer==3.3.2 -click==8.1.7 -docutils==0.20.1 -exceptiongroup==1.2.1 -grpcio==1.63.0 -grpcio-tools==1.62.0 -grpclib==0.4.7 -h2==4.1.0 -hpack==4.0.0 -hyperframe==6.0.1 -idna==3.7 -imagesize==1.4.1 -iniconfig==2.0.0 -isort==5.13.2 -Jinja2==3.1.4 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdit-py-plugins==0.4.0 -mdurl==0.1.2 -multidict==6.0.5 -mypy-extensions==1.0.0 -myst-parser==3.0.1 -packaging==24.0 -pathspec==0.12.1 -platformdirs==4.2.1 -pluggy==1.5.0 -protobuf==4.25.3 -Pygments==2.18.0 -pyproject_hooks==1.1.0 -pytest==8.2.0 -python-dateutil==2.9.0.post0 -PyYAML==6.0.1 -requests==2.31.0 -six==1.16.0 -snowballstemmer==2.2.0 -setuptools==69.5.1 -Sphinx==7.3.7 -sphinx-rtd-theme==2.0.0 -sphinxcontrib-applehelp==1.0.8 -sphinxcontrib-devhelp==1.0.6 -sphinxcontrib-htmlhelp==2.0.5 -sphinxcontrib-jquery==4.1 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.7 -sphinxcontrib-serializinghtml==1.1.10 -tomli==2.0.1 -typing_extensions==4.11.0 -urllib3==2.2.1 +flake8==7.1.1 +mccabe==0.7.0 +numpy==2.1.3 +protobuf==5.28.3 +pycodestyle==2.12.1 +pyflakes==3.2.0 +websocket-client==1.8.0 +wheel +build \ No newline at end of file diff --git a/scripts/build-proto.py b/scripts/build-proto.py deleted file mode 100644 index f3c6870..0000000 --- a/scripts/build-proto.py +++ /dev/null @@ -1,81 +0,0 @@ -# Build proto files - -import os -import subprocess - -# Paths -scriptPath = os.path.dirname(os.path.realpath(__file__)) -protoInputPath = scriptPath + "/../V3Schema" -protoOutputPath = scriptPath + "/../maf_three" - -print("*****************") -print("Building Proto files from: " + protoInputPath) -print("Output directory: " + protoOutputPath) -print("*****************") - -def BuildProtoFile(protoFile, inputDir, outputDir): - print("---> Building: " + file) - - # Build the proto file - status = subprocess.run([ - 'python3', - '-m', - 'grpc_tools.protoc', - protoFile, - f'-I={inputDir}', - f'--python_out={outputDir}', - f'--pyi_out={outputDir}', - '--experimental_allow_proto3_optional' - ], capture_output=True) - - return status - - -GREEN = '\033[92m' -RED = '\033[91m' -ENDC = '\033[0m' - -# Find and build all the proto files -fileError = 0 -fileCount = 0 -import glob -files = glob.glob(protoInputPath+"/**/*.proto", recursive=True) -for file in files: - - # Build - result = BuildProtoFile(file, protoInputPath, protoOutputPath) - - # Inspect result - fileCount += 1 - if result.returncode != 0: - fileError += 1 - print(RED + result.stderr.decode('utf-8') + ENDC) - -# Print results -print("*****************") -print(GREEN + 'Built: ' + str(fileCount - fileError) + " / " + str(fileCount) + " files." + ENDC) -if fileError > 0: - print(RED + 'Error: ' + str(fileError) + " / " + str(fileCount) + " files." + ENDC) -print("*****************") - -# Let the caller know if everything was built -if fileError > 0: - exit(1) - -# Remove the _pb2 at the end of the generated files -generatedFiles = glob.glob(protoOutputPath+"/MF/**/*_pb2.*", recursive=True) -for file in generatedFiles: - print('Updating generated file:', file) - - # Get the content - with open(file, "r") as f: - lines = f.readlines() - - # Remove '_pb2' - with open(file, "w") as f: - for line in lines: - line = line.replace('_pb2', '') - f.write(line) - - # Rename the file - os.rename(file, file.replace('_pb2', '')) diff --git a/scripts/build-doc.py b/scripts/build_doc.py similarity index 95% rename from scripts/build-doc.py rename to scripts/build_doc.py index ddf03b8..1559aa5 100644 --- a/scripts/build-doc.py +++ b/scripts/build_doc.py @@ -8,7 +8,7 @@ # Paths scriptPath = os.path.dirname(os.path.realpath(__file__)) protoInputPath = scriptPath + '/../V3Schema/' -protoOutputPath = scriptPath + '/../maf_three' +protoOutputPath = scriptPath + '/../three' docInputPath = scriptPath + '/../doc/source/' docOutputPath = scriptPath + '/../doc/build/' @@ -58,9 +58,9 @@ def CleanUpGeneratedInit(file): if badImport and ')' in line: badImport = False -# Save the maf_three/__init__.py +# Save the three/__init__.py # It will be overridden by the betterprotoc compiler -initFilePath = scriptPath + "/../maf_three/__init__.py" +initFilePath = scriptPath + "/../three/__init__.py" initFilePathBack = initFilePath+'.back' os.rename(initFilePath, initFilePathBack) @@ -68,7 +68,7 @@ def CleanUpGeneratedInit(file): protoFiles = glob.glob(protoInputPath+"/**/*.proto", recursive=True) result = BuildProtoFiles(protoFiles, protoInputPath, protoOutputPath) -# Restore maf_three/__init__.py +# Restore three/__init__.py os.rename(initFilePathBack, initFilePath) # Inspect the results diff --git a/scripts/build_proto.py b/scripts/build_proto.py new file mode 100644 index 0000000..d8e3b27 --- /dev/null +++ b/scripts/build_proto.py @@ -0,0 +1,32 @@ +import subprocess +import os +import sys +import shutil + +from transpileProto import transpile +from checkFiles import check_files + +if __name__ == "__main__": + # Remove the folder three/MF if it exists + mf_folder = "./three/MF" + if os.path.exists(mf_folder): + shutil.rmtree(mf_folder) + + # Transpile the proto files + transpile('./V3Schema','./three/') + + # Check the python files for formatting and linting issues + check_files('./three/MF/V3/') + + # These have to be done in subprocesses because the imports are not available in the current process. Chicken and egg problem + # Add the scripts folder to the system path + scripts_folder = os.path.abspath(os.path.dirname(__file__)) + sys.path.insert(0, scripts_folder) + + # Install the package in editable mode + subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-e', '.']) + + # Generate the proto files + subprocess.check_call([sys.executable, f'{scripts_folder}/generatePyi.py']) + + exit(0) \ No newline at end of file diff --git a/scripts/checkFiles.py b/scripts/checkFiles.py new file mode 100644 index 0000000..5b96e95 --- /dev/null +++ b/scripts/checkFiles.py @@ -0,0 +1,32 @@ +import os +import argparse + +from flake8.api import legacy as flake8 + +def run_flake8(file_path): + style_guide = flake8.get_style_guide(select=['E', 'F'], ignore=['E501']) + report = style_guide.check_files([file_path]) + return report.get_statistics('E') + report.get_statistics('F') + +def check_files(directory): + print("Checking python files...") + hasError = False + for root, _, files in os.walk(directory): + for file in files: + if file.endswith('.py') and file != '__init__.py': + filepath = os.path.join(root, file) + # print(f"Running flake8 on {filepath}...") + flake8_output = run_flake8(filepath) + if flake8_output: + print(f"flake8 issues in {filepath}:\n{flake8_output}") + hasError = True + else: + print(f"Clean: {filepath}") + if hasError: + raise Exception("Formatting Or Linting Issues Found") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check python files for formatting and linting issues.") + parser.add_argument('input_dir', type=str, nargs='?', default='./three/MF/V3/', help='The directory to check for python files.') + args = parser.parse_args() + check_files(args.input_dir) \ No newline at end of file diff --git a/scripts/generatePyi.py b/scripts/generatePyi.py new file mode 100644 index 0000000..ffac677 --- /dev/null +++ b/scripts/generatePyi.py @@ -0,0 +1,94 @@ +import inspect +import importlib +import ast +import argparse + +def get_imports_from_file(file_path): + with open(file_path, 'r') as file: + tree = ast.parse(file.read(), filename=file_path) + + imports = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + parts = alias.name.split('.') + for i in range(1, len(parts) + 1): + imports.add('.'.join(parts[:i])) + elif isinstance(node, ast.ImportFrom): + module = node.module + if module: + parts = module.split('.') + for i in range(1, len(parts) + 1): + imports.add('.'.join(parts[:i])) + return sorted(imports) + +def adjust_signature(signature): + # Replace NoneType with None in the signature + return signature.replace('NoneType', 'None') + +def generate_pyi(scanner_module_name, three_module_name, output_file): + print("Generating .pyi file...") + # Import the modules + three_module = importlib.import_module(three_module_name) + scanner_module = importlib.import_module(scanner_module_name) + + # Get the class from scanner.py + scanner_class = getattr(scanner_module, 'Scanner') + + # Get functions from three.py + three_functions = inspect.getmembers(three_module, inspect.isfunction) + + # Get import statements from scanner.py and three.py + scanner_imports = get_imports_from_file(scanner_module.__file__) + three_imports = get_imports_from_file(three_module.__file__) + + # Combine and deduplicate imports + all_imports = sorted(set(scanner_imports + three_imports)) + + # Start generating the .pyi content + pyi_content = [] + + # Add imports + for imp in all_imports: + pyi_content.append(f"import {imp}") + + # Add specific imports from typing + pyi_content.append("from typing import Optional, Callable, Any, Union, List") + + + # Add class definition + pyi_content.append("\n\nclass Scanner:") + + # Add __init__ method + init_method = scanner_class.__init__ + init_sig = str(inspect.signature(init_method)) + init_sig = adjust_signature(init_sig) + pyi_content.append(f" def __init__{init_sig}: ...") + + # Add other methods from Scanner class + for name, method in inspect.getmembers(scanner_class, inspect.isfunction): + if not name.startswith('_'): # Skip private methods + sig = str(inspect.signature(method)) + sig = adjust_signature(sig) + pyi_content.append(f" def {name}{sig}: ...") + + # Add dynamically bound functions from three.py + for name, func in three_functions: + sig = str(inspect.signature(func)) + sig = adjust_signature(sig) + pyi_content.append(f" def {name}{sig}: ...") + + # Write to the output file + with open(output_file, 'w') as f: + f.write('\n'.join(pyi_content)) + print("Completed!") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate .pyi file for a given module.") + parser.add_argument('scanner_module', type=str, nargs='?', default='three.scanner', help='The module name for the scanner class.') + parser.add_argument('three_module', type=str, nargs='?', default='three.MF.V3.Three', help='The module name for the three functions.') + parser.add_argument('output_file', type=str, nargs='?', default='./three/scanner.pyi', help='The output file for the .pyi content.') + args = parser.parse_args() + + generate_pyi(args.scanner_module, args.three_module, args.output_file) + exit(0) \ No newline at end of file diff --git a/scripts/incrementVersion.sh b/scripts/incrementVersion.sh new file mode 100755 index 0000000..61d9061 --- /dev/null +++ b/scripts/incrementVersion.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Path to the __init__.py file +INIT_FILE="./three/__init__.py" + +# Extract the current version +CURRENT_VERSION=$(grep -oP "(?<=__version__ = ')[^']*" "$INIT_FILE") +echo "Current version: $CURRENT_VERSION" + +# Split the version into its components +IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" + +# Increment the build number +VERSION_PARTS[2]=$((VERSION_PARTS[2] + 1)) + +# Construct the new version +NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}" + +# Update the __init__.py file with the new version +sed -i "s/__version__ = '$CURRENT_VERSION'/__version__ = '$NEW_VERSION'/" "$INIT_FILE" + +echo "Version updated from $CURRENT_VERSION to $NEW_VERSION" \ No newline at end of file diff --git a/scripts/interpretProto.py b/scripts/interpretProto.py new file mode 100644 index 0000000..5a5c0c0 --- /dev/null +++ b/scripts/interpretProto.py @@ -0,0 +1,214 @@ +import re +import argparse +import os +import glob +from typing import List, Optional, Tuple, Set + + + +class ProtoProperty: + def __init__(self, type_: str, name: str, optional: bool, comment: str, repeated) -> None: + self.type: str = type_ + self.name: str = name + self.optional: bool = optional + self.repeated: bool = repeated + self.comment: str = comment + +class MessageType: + def __init__(self, type_: str, name: str, comment: str, parent: str, namespace: str) -> None: + self.type: str = type_ + self.name: str = name + self.comment: str = comment + self.namespace = namespace + self.properties: List[ProtoProperty] = [] + self.nested_messages = [] + self.parent = parent + self.path = "" + + + def add_property(self, type_: str, name: str, optional: bool, comment: str, repeated: bool) -> None: + self.properties.append(ProtoProperty(type_, name, optional, comment, repeated)) + + def add_nested_message(self, message): + self.nested_messages.append(message) + +class ProcedureType: + def __init__(self, name: str, comment: str, request: str, response: str) -> None: + self.name: str = name + self.comment: str = comment + self.request: str = request + self.response: str = response + +class ServiceType: + def __init__(self, name: str, comment: str, namespace: str) -> None: + self.name: str = name + self.comment: str = comment + self.namespace = namespace + self.procedures: List[ProcedureType] = [] + + def add_procedure(self, name: str, comment: str, request: str, response: str) -> None: + self.procedures.append(ProcedureType(name, comment, request, response)) + +def get_parent_name_from_stack(stack: List[MessageType]) -> str: + if len(stack) == 0: + return "" + # Concatinate all the names in the stack with a . + return ".".join([message.name for message in stack]) + + +def parse_proto(proto_file: str, base_dir: str) -> Tuple[List[str], List[MessageType]]: + + with open(proto_file, 'r') as file: + lines: List[str] = file.readlines() + + imports: Set[str] = set() + messages: List[MessageType] = [] + comments: List[str] = [] + namespace: str = "" + current_message_stack = [] + services :List[ServiceType] = [] + service_object = None + + for line in lines: + line = line.strip() + + if line.startswith("package"): + match = re.findall(r'package\s+([\w\.]+);', line) + if match: + namespace = match[0] + else: + namespace = '' + continue + + elif line.startswith("import"): + match = re.findall(r'import\s+"([^"]+)";', line) + if match: + imports.add(match[0]) + continue + + elif line.startswith("//") or line.startswith("/*") or line.startswith("*"): + cleaned_line = line.lstrip() + #clean out /* * and // from the comments + cleaned_line = re.sub(r'^/\*|\*/|^\*|//', '', cleaned_line) + + comments.append(cleaned_line) + continue + + elif line == "": + continue + + elif line.startswith("message") or line.startswith("enum"): + comment = "\n".join(comments) + + if line.startswith("enum"): + message_name = re.findall(r'enum (\w+)', line)[0] + message_type = "enum" + imports.add("enum") + else: + message_name: str = re.findall(r'message (\w+)', line)[0] + message_type = "message" + + + parent = get_parent_name_from_stack(current_message_stack) + new_message = MessageType(message_type, message_name, comment, parent, namespace) + + if current_message_stack: + current_message_stack[-1].add_nested_message(new_message) + else: + messages.append(new_message) + + current_message_stack.append(new_message) + + comments = [] + continue + elif "service" in line: + comment = "\n".join(comments) + comments = [] + name = re.findall(r'service (\w+)', line)[0] + service_object = ServiceType(name, comment, namespace) + + elif "proto3" in line or "{" in line: + continue + elif "rpc" in line: + comment = "\n".join(comments) + comments = [] + name = re.findall(r'rpc (\w+)', line)[0] + rr = re.findall(r'\(\s*([^)]+?)\s*\)', line) + request = rr[0] + response = rr[1] + service_object.add_procedure(name, comment, request, response) + elif "}" in line and service_object: + services.append(service_object) + service_object = None + # elif current_message_stack is not empty, then we are in a message + elif len(current_message_stack) > 0: + if line == "}": + current_message_stack.pop() + else: + if current_message_stack[-1].type == "enum": + match = re.findall(r'(\w+)\s*=\s*(\d+);', line) + if match: + name, value = match[0] + comment = "\n".join(comments) + current_message_stack[-1].add_property("string", name, False, comment, False) + comments = [] + else: + print(f"Error parsing enum: {line}") + else: + match = re.findall(r'(optional\s+|repeated\s+)?([\w\.]+)\s+(\w+)\s*=\s*(\d+);', line) + if match: + keyword_str, type_, name, _ = match[0] + optional: bool = keyword_str.strip() == 'optional' + repeated: bool = keyword_str.strip() == 'repeated' + comment: str = "\n".join(comments) + current_message_stack[-1].add_property(type_, name, optional, comment, repeated) + comments = [] + else : + print(f"Error parsing message: {line}") + else: + print(f"Error parsing line: {line}") + + return imports, messages, services, namespace + +def create_proto_objects(directory: str): + proto_files = glob.glob(os.path.join(directory, '**', '*.proto'), recursive=True) + all_objs = [] + + for proto_file in proto_files: + imports, messages, services, namespace = parse_proto(proto_file, directory) + # Get relative path of the file + proto_file = os.path.relpath(proto_file, directory) + + # create object that has imports, and messages, and namespace to keep them together + all_objs.append({ + "imports": imports, + "messages": messages, + "namespace": namespace, + "filename": proto_file, + "services": services + }) + return all_objs + +# def main() -> None: +# parser = argparse.ArgumentParser(description="Parse a directory of protobuf files and generate Python objects.") +# parser.add_argument('directory', type=str, help='Path to the directory containing protobuf files') +# args = parser.parse_args() +# +# proto_files = glob.glob(os.path.join(args.directory, '**', '*.proto'), recursive=True) +# +# for proto_file in proto_files: +# print(f"Parsing file: {proto_file}") +# imports, enums, messages = parse_proto(proto_file, args.directory) +# +# print("Imports:") +# for imp in imports: +# print(f" {imp}") +# +# for enum in enums: +# print_object(enum) +# +# for message in messages: +# print_object(message) +# +# if __name__ == "__main__": +# main() \ No newline at end of file diff --git a/scripts/transpileProto.py b/scripts/transpileProto.py new file mode 100644 index 0000000..2940b4f --- /dev/null +++ b/scripts/transpileProto.py @@ -0,0 +1,698 @@ +import os +import argparse +from interpretProto import create_proto_objects, MessageType, parse_proto +from typing import List, Dict,Tuple, Set +from enum import Enum +from tree import TreeNode, Tree, TreeProperty, NodeType, ImportDescriptor, get_descriptor_by_partial_filename +import ast +import os +import subprocess + +# Mapping of special types to Python types +type_mapping = { + "int32": "int", + "Int32": "int", + "int64": "int", + "Int64": "int", + "uint64": "int", + "UInt64": "int", + "uint32": "int", + "UInt32": "int", + "bool": "bool", + "Bool": "bool", + "float": "float", + "Float": "float", + "double": "float", + "Double": "float", + "string": "str", + "String": "str", + "google.protobuf.Any": "_any_pb2" +} + +python_types = [ + "int", + "float", + "bool", + "str", + "bytes" +] + +required_upload_procedures = [ + "UploadProject", + "RemoveVertices" +] + +optional_upload_procedures = [ + "SetProjector" +] + +required_download_procedures = [ + "ScanData", + "MergeData", + "Export", + "ExportMerge", + "ExportLogs", + "StartVideo", + "DepthMap", + "DownloadProject" +] + + +def generate_python_code(output_dir: str, tree:Tree) -> set: + """ + Generate Python code from the tree structure. + This method looks at messages, enums, and services and generates Python code for them. + Then produces the imports for the files + Then finally writes the code to the files. + """ + # create a unique set of paths + paths = set() + + branches = tree.get_branches_by_filespace() + + for key, branch in branches.items(): + + # Get the file path + file_path = os.path.join(output_dir, key.replace(".", "/") + ".py") + path = os.path.dirname(file_path) + + print(f"Parsing file: {file_path}") + + # Create the directory if it doesn't exist + os.makedirs(path, exist_ok=True) + # Add the path to a unique set + paths.add(path) + + # Generate code for messages + class_code = "" + service_code = "" + + class_code = "" + for node in branch: + if node.type == NodeType.Class or node.type == NodeType.Enum: + class_code += generate_class_code(node) + "\n\n" + elif node.type == NodeType.Service: + service_code += generate_service_code(node, tree) + "\n\n" + + # Get imports + imports = get_imports_from_nodes(branch) + + import_lines = generate_import_lines(imports, branch[0].filespace) + + # Combine imports, message code, and enum code + combined_code = "" + if (import_lines != ""): + combined_code = import_lines + "\n\n\n" + if (class_code != ""): + combined_code += class_code + if (service_code != ""): + combined_code += service_code + + # Write the combined code to a file with the name from the file_path + with open(file_path, 'w') as f: + f.write(combined_code) + + return paths + +def get_imports_from_nodes(nodes:List[TreeNode]) -> Dict[str, List[ImportDescriptor]]: + """ + Get the import descriptors from a list of nodes. + This will also create base descriptors for the properties + """ + imports = [] + for node in nodes: + def nested_imports(node) -> List[ImportDescriptor]: + imports = [] + for imp in node.imports: + imports.append(imp) + for prop in node.properties: + if prop.import_descriptor != None: + imports.append(prop.import_descriptor) + for child in node.children.values(): + imports += nested_imports(child) + return imports + + imports += nested_imports(node) + + # Group import descriptors by their file property + grouped_imports: Dict[str, List[ImportDescriptor]] = {} + for imp in imports: + if imp.file not in grouped_imports: + grouped_imports[imp.file] = [] + grouped_imports[imp.file].append(imp) + + # Convert the dictionary to a list of lists + unique_imports = list(grouped_imports.values()) + + return unique_imports + +class ImportList: + def __init__(self): + self.file:str = "" + self.types:List[Dict[str,str]] = [] + + +def generate_import_lines(descriptorsLists:Dict[str, List[ImportDescriptor]], file_path:str) -> str: + """ + Generate import lines from a list of import descriptors. + """ + import_lines = [] + + ImportListArray = [] + for combined in descriptorsLists: + importList = ImportList() + importList.file = combined[0].file + if (importList.file == file_path): + continue + for descriptor in combined: + importList.types.append({"type":descriptor.type, "replacement":descriptor.replacement}) + ImportListArray.append(importList) + + ImportListArray.sort(key=lambda x: x.file) + + for importList in ImportListArray: + + if importList.file == "enum": + import_lines.append(f"from enum import Enum") + continue + if "google" in importList.file: + split = importList.file.split('.') + import_lines.append(f"from {'.'.join(split[:-1])} import {split[-1]}_pb2 as _{split[-1]}_pb2") + continue + + # remove duplicates from types + types = {} + for imp in importList.types: + if imp["type"] == "": + continue + # if types already has the type, check to see if the values will be the same. Throw an error if they are not + # if imp["type"] in types: + # if types[imp["type"]] != imp["replacement"]: + # raise Exception(f"Type {imp['type']} has multiple replacements {types[imp['type']]} and {imp['replacement']}") + types[imp["type"]] = imp["replacement"] + + # If there are no types, just import the file + if not types: + import_lines.append(f"import {importList.file}") + continue + + import_line = f"from {importList.file} import " + for i, (t, replacement) in enumerate(types.items()): + if i > 0: + import_line += ", " + import_line += f"{t}" + if replacement != "": + import_line += f" as {replacement}" + import_lines.append(import_line) + + return "\n".join(import_lines) + + +def get_imports(imports: List[str]) -> List[ImportDescriptor]: + """ + Get the import descriptors from a list of import paths. + This is the first step in linking the properties in the tree. + """ + module_parts_list:List[ImportDescriptor] = [] + + for imp in imports: + # Split the import path into parts + module_path = os.path.splitext(imp.replace('/', '.'))[0] + # Check module_parts_list for existing module by filename + foundModule = None + for module in module_parts_list: + if module.file == module_path: + foundModule = module + break + if foundModule == None: + module_parts_list.append(ImportDescriptor(module_path, "", "")) + return module_parts_list + +def get_tree(proto_objects: List)-> Tree: + """ + Get's the tree structure of the proto objects + We use this for a proper reference to the objects that are created afterwards + """ + + tree = Tree() + + # We need to go through the proto_objects twice to build the tree properly. + # First to build the base tree without properties + for obj in proto_objects: + namespace = obj['namespace'] + imports = obj['imports'] + importDescs = get_imports(imports) + filespace = obj['filename'].replace('/', '.').replace('.proto', '') + + for msg in obj['messages']: + + def get_nested_messages(message, nested_name_space): + # concat message.name with message.parent + nested_name_space = f"{nested_name_space}.{message.name}" + #convert filename to namespace + node = tree.add_path(nested_name_space, filespace) + if message.type == "enum": + node.type = NodeType.Enum + elif message.type == "message": + node.type = NodeType.Class + node.imports = importDescs + node.comment = parseComment(message.comment) + for nested in message.nested_messages: + get_nested_messages(nested, nested_name_space) + get_nested_messages(msg, namespace) + for service in obj['services']: + # I think services are top level definitions, so no parent needed + service_path = f"{namespace}.{service.name}" + node = tree.add_path(service_path, filespace) + node.type = NodeType.Service + node.imports = importDescs + node.comment = parseComment(service.comment) + + # Then Loop again to to add the properties to the objects in the tree (self referencing) + for obj in proto_objects: + namespace = obj['namespace'] + for msg in obj['messages']: + def parse_message_props(message): + message.path = f"{namespace}.{message.parent + '.' if message.parent else ''}{message.name}" + node = tree.search(message.path) + for prop in message.properties: + property = get_property(prop, tree, node, namespace) + node.properties.append(property) + for nested in message.nested_messages: + parse_message_props(nested) + + parse_message_props(msg) + for service in obj['services']: + service_path = f"{namespace}.{service.name}" + node = tree.search(service_path) + for procedure in service.procedures: + + # Remove only the last word from procedure.request and procedure.response + request_base = procedure.request.rsplit('.', 1)[0] + response_base = procedure.response.rsplit('.', 1)[0] + + import_descriptor_request = get_descriptor_by_partial_filename(request_base, node.imports) + import_descriptor_response = get_descriptor_by_partial_filename(response_base, node.imports) + + # Check if the import descriptor is found and throw a message if not + assert import_descriptor_request != None, f"Descriptor not found for {procedure.request}" + assert import_descriptor_response != None, f"Descriptor not found for {procedure.response}" + + request_root_node = tree.search(import_descriptor_request.file) + response_root_node = tree.search(import_descriptor_response.file) + + # Check if the nodes are valid. Otherwise we're making some big assumptions + assert request_root_node != None, f"Node not found for {procedure.request}" + assert response_root_node != None, f"Node not found for {procedure.response}" + + request_node = request_root_node.get_child("Request") + response_node = response_root_node.get_child("Response") + + assert request_node, f"Request node not found for {procedure.request}" + assert response_node, f"Response node not found for {procedure.response}" + + # Add the procedure + node.add_procedure(procedure.name, request_node, response_node, parseComment(procedure.comment), import_descriptor_request, import_descriptor_response) + return tree + +def parseComment(comment: str) -> str: + """ + Simply parse the comment and return it, wrapping multi lines or single lines appropriately + """ + if comment != "": + if len(comment.split('\n')) > 1: + comment_code = f'"""{comment}"""\n' + else: + # remove initial whitespace in comment + comment = comment.strip() + comment_code = f'# {comment}\n' + else: + return "" + return comment_code + +def add_indents(code: str, indent: int) -> str: + """ + Add indents to the code + """ + # Indent the code by adding spaces if the line is not empty + return "\n".join([f"{' ' * indent}{line}" if line.strip() else line for line in code.split("\n")]) + +def get_property(property, tree: Tree, node:TreeNode, message_namespace:str) -> TreeProperty: + """ + Get the property from the property object. This is complicated as the property could be self referencing, part of the parent class, or from an external import + """ + tree_property = TreeProperty(property.type, property.name, property.optional, parseComment(property.comment), property.repeated, None) + + # 1 Check to see if property is a python type + if property.type in type_mapping: + tree_property.type = type_mapping.get(property.type, property.type) + return tree_property + + import_descriptor = ImportDescriptor(node.filespace, property.type.split(".")[-1], "") + tree_property.import_descriptor = import_descriptor + + # 2 Check to see if the property is directly accessible + direct_node = tree.search(property.type) + if (direct_node): + import_descriptor.file = direct_node.filespace + relative_path = direct_node.get_relative_path_from_filespace() + import_descriptor.type = relative_path if relative_path != "" else direct_node.name + import_descriptor.type = import_descriptor.type.split(".")[0] + # if import_descriptor.file != node.filespace: + tree_property.type = relative_path + return tree_property + + # 3 Check to see if the property type is in this file (get root node) + # - Is it a child of the node? + if node.has_child(property.type): + return tree_property + + # - Check if the property is a node of a parent + root_node = node + while root_node.parent.name != "root": + root_node = root_node.parent + if root_node.has_child(property.type) or root_node.name == property.type: + property_node = root_node.get_child(property.type) + import_descriptor.file = property_node.filespace + relative_path = property_node.get_relative_path_from_filespace() + full_type = relative_path if relative_path != "" else property_node.name + + split = full_type.split(".") + import_descriptor.type = split[0] + remaining = ".".join(split[1:]) + + tree_property.type = full_type + if import_descriptor.file != node.filespace: + # replace all . with _ in the type + import_descriptor.replacement = get_replacement_name( property_node.filespace + "." + import_descriptor.type ) + tree_property.type = import_descriptor.replacement + if (remaining != ""): + tree_property.type += "." + remaining + + return tree_property + + # - Check if the property shares a namespace + property_type_with_namespace = f"{message_namespace}.{property.type}" + property_node = tree.search(property_type_with_namespace) + if property_node: + import_descriptor.file = property_node.filespace + return tree_property + + raise Exception("Property Type could not be resolved", property.type) + + +def generate_class_code(current_node:TreeNode) -> str: + """ + Generate the class code for a node in the tree. This is recursive for children nodes. + """ + if current_node.type == NodeType.Enum: + return generate_enum_code(current_node) + + class_code = f"class {current_node.name}:\n" + + class_code += add_indents(current_node.comment,1) + + # Generate code for nested messages + for child in current_node.children.values(): + nested_class_code = generate_class_code(child) + nested_class_code = add_indents(nested_class_code,1) + class_code += f"{nested_class_code}\n" + + class_code += " def __init__(self" + + if current_node.properties: + # sort properties so optionals are last + properties = sorted(current_node.properties, key=lambda x: x.optional) + # Class init arguments + for prop in properties: + + prop_type = prop.type + # Wrap the type in single quotes if it is a self referencing type + if prop.import_descriptor != None: + if prop.import_descriptor.file == current_node.filespace: + prop_type = f"'{prop.type}'" + + # handle repeated + if prop.repeated: + # Get importDescriptor for List "typing" + + descriptor = ImportDescriptor("typing", "List", "") + current_node.imports.append(descriptor) + class_code += f", {prop.name}: List[{prop_type}]" + else: + class_code += f", {prop.name}: {prop_type}" + # handle optionals + if prop.optional: + class_code += " = None" + class_code += "):\n" + # Internal properties assignment + for prop in properties: + # Add comments with spaces + if prop.comment: + # TODO Check for types that are not python types and add a isinstance call to cast to that type. This will help unwrapping + + class_code += f"{add_indents(prop.comment,2)}" + class_code += add_indents(f"self.{prop.name} = {prop.name}\n",2) + else: + class_code += "):\n pass" + + return class_code + +name_mapping = { + "None": "Empty", +} + +def generate_enum_code(enum:TreeNode) -> str: + """ + Generate the enum code for a node in the tree. + """ + enum_code = enum.comment + enum_code += f"class {enum.name}(Enum):\n" + for value in enum.properties: + name = name_mapping.get(value.name, value.name) + enum_code += f" {name} = \"{value.name}\"" + if value.comment != "": + enum_code += f" {value.comment}" + else: + enum_code += "\n" + return enum_code + + +def get_replacement_name(path:str) -> str: + return path.replace(".", "_") + +def generate_service_code( current_node:TreeNode, tree:Tree) -> str: + """ + Generate the service code for a node in the tree. + """ + name = current_node.name + + task_descriptor = get_descriptor_by_partial_filename("Task", current_node.imports) + if task_descriptor == None: + task_descriptor = ImportDescriptor("MF.V3", "Task", "") + current_node.imports.append(task_descriptor) + task_name = task_descriptor.replacement if task_descriptor.replacement != '' else task_descriptor.type + + service_code = "" + + for procedure in current_node.procedures: + + request_node = procedure.request + response_node = procedure.response + + # create a method name from the name value, by adding underscores between camel case + method_name = "" + for i, c in enumerate(procedure.name): + if c.isupper() and i != 0: + method_name += "_" + method_name += c.lower() + + # loop over all the properties from the request node to get the input node + + method_properties = [] + # Parse the request properties for convenience to the user + for prop in request_node.properties: + if prop.name == "Input": + if prop.import_descriptor != None: + # Get the input node from the tree + input_node = tree.search(prop.import_descriptor.file) + + # Create a new import descriptor because we're going to change the replacement name to avoid conflicts + if prop.import_descriptor.replacement == "": + prop.import_descriptor.replacement = get_replacement_name(input_node.get_path()); + prop.type = prop.import_descriptor.replacement + # Add the import descriptor to the current node imports + current_node.imports.append(prop.import_descriptor) + + # Loop over the properties of the input node + # We need to make the convenience call easier than just the Request itself + for input_prop in input_node.properties: + # Check if the input_prop.type is not a python type + if input_prop.type in python_types: + method_properties.append(input_prop) + continue; + assert(input_prop.import_descriptor != None) + if input_prop.import_descriptor != None and input_prop.import_descriptor.replacement == "": + import_file_node = tree.search(input_prop.import_descriptor.file) + import_node = import_file_node.get_child(input_prop.type) + assert(import_node != None) + relative_path = import_node.get_relative_path_from_filespace() + replacement_name = get_replacement_name(import_file_node.get_path()) + for imp in current_node.imports: + if imp.file == import_node.filespace: + if imp.replacement != "": + replacement_name = imp.replacement + + new_descriptor = ImportDescriptor(import_node.filespace, relative_path.split(".")[0], replacement_name) + + #make a new property name by replacing the first part of the relative_path with replacement_name + split_relative_path = relative_path.split(".") + split_relative_path[0] = replacement_name + new_property_name = ".".join(split_relative_path) + + prop = TreeProperty(new_property_name,input_prop.name, input_prop.optional, input_prop.comment, input_prop.repeated, new_descriptor) + method_properties.append(prop) + continue; + else: + assert(input_prop.import_descriptor != None) + method_properties.append(input_prop) + else: + method_properties.append(prop) + + + service_code += f"def {method_name}(self" + + # Sort the properties so that optionals are last + method_properties = sorted(method_properties, key=lambda x: x.optional) + + descriptor = ImportDescriptor("typing", "List", "") + current_node.imports.append(descriptor) + + if procedure.name in required_upload_procedures: + service_code += ", buffer: bytes" + + for prop in method_properties: + if prop.repeated: + service_code += f", {prop.name}: List[{prop.type}]" + else: + service_code += f", {prop.name}: {prop.type}" + if prop.optional: + service_code += " = None" + if prop.import_descriptor != None: + current_node.imports.append(prop.import_descriptor) + + if procedure.name in optional_upload_procedures: + service_code += ", buffer: bytes = None" + + service_code += f") -> {task_name}:\n" + service_code += add_indents(procedure.comment,1) + + + def create_object_code(node:TreeNode, postfix:str, ignore_optionals:bool)->str: + code = "" + + # Get the Request Or Response node from the tree + filespace_node = tree.search(node.filespace) + filespace_replacement_name = get_replacement_name(filespace_node.get_path()) + + # Get the relative path of the node + rel_path = node.get_relative_path_from_filespace() + + # Replace the first part of the relative path with the replacement name + split_relative_request_path = rel_path.split(".") + split_relative_request_path[0] = filespace_replacement_name + new_request_property_name = ".".join(split_relative_request_path) + + # Update the node import_descriptor to have a replacement name + procedure.request_import.replacement = filespace_replacement_name + procedure.request_import.type = filespace_node.name + + current_node.imports.append(procedure.request_import) + + code += f" {method_name}_{postfix} = {new_request_property_name}(" + for i, prop in enumerate(node.properties): + if (prop.optional and ignore_optionals): + continue + if i>0: + code += "," + code += "\n" + if prop.name == "Input": + # some property types are python types, so we need to handle them differently + if len(method_properties) == 1 and method_properties[0].type in python_types: + code += f" {method_properties[0].name}={method_properties[0].name}" + else: # multiple properties will result in a complicated type + code += f" {prop.name}={prop.type}(\n" + for input_prop in method_properties: + code += f" {input_prop.name}={input_prop.name},\n" + code += " )" + elif prop.name == "Type": + code += f" {prop.name}=\"{procedure.name}\"" + elif prop.name == "Index": + code += f" {prop.name}=0" + else: + code += f" {prop.name}=None" + code += "\n )\n" + return code + + + service_code += create_object_code(request_node, "request", False) + service_code += create_object_code(response_node, "response", True) + + + service_code += f" task = {task_name}(Index=0, Type=\"{procedure.name}\", Input={method_name}_request, Output={method_name}_response)\n" + if procedure.name in required_upload_procedures or procedure.name in optional_upload_procedures: + service_code += f" self.SendTask(task, buffer)\n" + else: + service_code += f" self.SendTask(task)\n" + service_code += f" return task\n\n\n" + + return service_code + +def generate_init_files(paths: set, tree: Tree, output_dir: str): + """ + Generates init files for use when accessing the classes and enums as a package + """ + for path in paths: + init_file = os.path.join(path, "__init__.py") + + # Remove output_dir from the path + relative_path = path.replace(output_dir+"/", "") + relative_path = relative_path.replace("/", ".") + node = tree.search(relative_path) + + imports:Set[str] = set() + + for child in node.children.values(): + if child.filespace: + imports.add(child.filespace) + + sorted_imports = sorted(imports) + + with open(init_file, 'w') as f: + f.write("") #guarantee a file + for import_path in sorted_imports: + f.write(f"from {import_path} import * \n") + + + +def transpile(input_dir:str, output_dir:str): + print("Building python files...") + # Check to see if input_dir and output_dir contain a trailing slash + if input_dir[-1] == '/': + input_dir = input_dir[:-1] + if output_dir[-1] == '/': + output_dir = output_dir[:-1] + + proto_objects = create_proto_objects(input_dir) + tree= get_tree(proto_objects) + paths = generate_python_code(output_dir, tree) + generate_init_files(paths, tree, output_dir) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate Python classes and enums from protobuf schema objects.") + parser.add_argument('input_dir', type=str, nargs='?', default= './V3Schema', help='The input directory containing the protobuf schema objects.') + parser.add_argument('output_dir', type=str, nargs='?', default='./three/', help='The output directory to write the generated Python classes and enums.') + args = parser.parse_args() + transpile(args.input_dir, args.output_dir) + exit(0) \ No newline at end of file diff --git a/scripts/tree.py b/scripts/tree.py new file mode 100644 index 0000000..51e0313 --- /dev/null +++ b/scripts/tree.py @@ -0,0 +1,162 @@ +from enum import Enum +from typing import List, Dict + +class TreeProperty: + def __init__(self, type_: str, name: str, optional: bool, comment: str, repeated: bool, import_descriptor:'ImportDescriptor') -> None: + self.type: str = type_ + self.name: str = name + self.optional: bool = optional + self.repeated: bool = repeated + self.comment: str = comment + self.import_descriptor: ImportDescriptor = import_descriptor + if self.repeated: + self.optional = True + + +class TreeProcedure: + def __init__(self, name: str, request: 'TreeNode', response: 'TreeNode', comment: str, request_import: 'ImportDescriptor', response_import: 'ImportDescriptor') -> None: + self.name: str = name + self.request: str = request + self.response: str = response + self.comment: str = comment + self.request_import: ImportDescriptor = request_import + self.response_import: ImportDescriptor = response_import + +class NodeType(Enum): + Class = "Class" + Enum = "Enum" + Service = "Service" + Directory = "Directory" + +class TreeNode: + def __init__(self, name: str, nodeType: NodeType = NodeType.Directory, parent = None, filespace = None): + self.name = name + self.type = nodeType + self.parent = parent + self.children = {} + self.properties = [] + self.procedures = [] + self.imports = [] + self.filespace = filespace + self.comment = None + + def add_child(self, child_node): + self.children[child_node.name] = child_node + + def add_property(self, type_: str, name: str, optional: bool, comment: str, repeated: bool) -> None: + self.properties.append(TreeProperty(type_, name, optional, comment, repeated)) + + def add_procedure(self, name: str, request: 'TreeNode', response: 'TreeNode', comment: str, request_import: 'ImportDescriptor', response_import: 'ImportDescriptor') -> None: + self.procedures.append(TreeProcedure(name, request, response, comment, request_import, response_import)) + + def get_child(self, name: str): + parts = name.split('.') + # Check each part of the name to see if it is a child + temp = self + while temp and len(parts) > 0: + temp = temp.children.get(parts[0], None) + parts = parts[1:] + if temp: + return temp + return None + + def has_child(self, name: str): + temp = self.get_child(name) + if temp: + return True + return False + + def get_path(self): + path = [] + current_node = self + while current_node: + if current_node.name == "root": + break + path.append(current_node.name) + current_node = current_node.parent + return '.'.join(reversed(path)) + + def get_relative_path_from_filespace(self): + path = [] + current_node = self + while current_node: + if current_node.name == "root": + break + if current_node.filespace == None: + break + path.append(current_node.name) + current_node = current_node.parent + return '.'.join(reversed(path)) + +class Tree: + def __init__(self): + self.root = TreeNode("root") + + def add_path(self, path: str, filespace) -> TreeNode: + parts = path.split('.') + current_node = self.root + for part in parts: + if part not in current_node.children: + new_node = TreeNode(part, parent=current_node) + current_node.add_child(new_node) + current_node = current_node.get_child(part) + current_node.filespace = filespace + return current_node + + def search(self, path: str) -> TreeNode: + parts = path.split('.') + current_node = self.root + for part in parts: + current_node = current_node.get_child(part) + if current_node is None: + return None + return current_node + + def get_nodes_with_filespace(self, filespace: str) -> TreeNode: + nodes = [] + def get_nodes(node): + if node.filespace == filespace: + nodes.append(node) + for child in node.children.values(): + get_nodes(child) + get_nodes(self.root) + return nodes + + def get_branches_by_filespace(self) -> Dict[str, List[TreeNode]]: + """ + Traverse the tree and collect nodes by their filespace. + + Returns: + Dict[str, List[TreeNode]]: A dictionary where the keys are filespace + identifiers and the values are lists of TreeNode objects that belong + to that filespace. + """ + branch_nodes = {} + + def traverse_for_filespace(node: TreeNode): + if node.filespace: + if node.filespace not in branch_nodes: + branch_nodes[node.filespace] = [] + branch_nodes[node.filespace].append(node) + else: + for child in node.children.values(): + traverse_for_filespace(child) + + traverse_for_filespace(self.root) + + return branch_nodes + +class ImportDescriptor: + def __init__(self, file:str, type:str, replacement:str): + self.file = file + self.type = type + self.replacement = replacement + +def get_descriptor_by_partial_filename(filename, import_descriptors) -> ImportDescriptor: + + for descriptor in import_descriptors: + split_descriptor = descriptor.file.split('.') + # match nameAsPath against the last part of descriptor.file + if split_descriptor[-1] == filename: + return descriptor + return None \ No newline at end of file diff --git a/three/LICENSE b/three/LICENSE new file mode 100644 index 0000000..0cd4f3a --- /dev/null +++ b/three/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Matter and Form + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/three/MF/V3/Buffer.py b/three/MF/V3/Buffer.py new file mode 100644 index 0000000..9927532 --- /dev/null +++ b/three/MF/V3/Buffer.py @@ -0,0 +1,66 @@ +from MF.V3.Task import Task as MF_V3_Task_Task +from google.protobuf import any_pb2 as _any_pb2 + + +class Buffer: + """* + Generic buffer message for the Three Scanner. + + Some tasks require the server and/or client to transfer binary data. In such cases the _buffer message_ is sent to inform the server/client what the data is and what task it belongs to. The binary data it refers to is sent immediately following the buffer message. + + For example, `DownloadProject` requires the server to transfer a ZIP file containing the project data to the client. + + > First, the client sends the task request to the server: + + ```json + { + "Task":{ + "Index":1, + "Type":"DownloadProject", + "Input":5 + } + } + ``` + + > The server sends the buffer message telling the client to expect a binary data transfer and what to do with it. Note that the buffer message `Task` field echoes the task request, making it clear which request this data is a response to. + + ```json + { + "Buffer":{ + "Descriptor":"Project-5.zip", + "Index":0, + "Size":15682096, + "Task":{ + "Index":1, + "Type":"DownloadProject", + "Input":5 + } + } + } + ``` + + > The server then sends the 15682096 byte data buffer of the project ZIP file. + > Finally, the server sends a task completion message. + + ```json + { + "Task":{ + "Index":1, + "Type":"DownloadProject" + "Input":5, + "State":"Completed" + } + } + ``` + """ + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task, Descriptor: _any_pb2 = None): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The task associated with the data buffer. This informs the client which request this data buffer corresponds to. + self.Task = Task + # Optional data buffer descriptor. See each task definition for details. + self.Descriptor = Descriptor + + diff --git a/three/MF/V3/Descriptors/BoundingBox.py b/three/MF/V3/Descriptors/BoundingBox.py new file mode 100644 index 0000000..c7652ec --- /dev/null +++ b/three/MF/V3/Descriptors/BoundingBox.py @@ -0,0 +1,24 @@ +from typing import List + + +class BoundingBox: + # BoundingBox descriptor. + def __init__(self, center: List[float] = None, size: List[float] = None, rotation: List[float] = None, transform: List[float] = None): + # The center of the bounding box. + self.center = center + # The size of the bounding box. + self.size = size + """ + The 3x3 rotation matrix of the bounding box. + The first, second and third column vectors are the x, y and z axes of the bounding box. + """ + self.rotation = rotation + """ + The 4x4 matrix that transforms the canonical cube with corners [±1, ±1, ±1] to the + bounding box in world coordinates. + The transform can be used as the model matrix for rendering the bounding box with an + OpenGL shader. + """ + self.transform = transform + + diff --git a/three/MF/V3/Descriptors/Calibration.py b/three/MF/V3/Descriptors/Calibration.py new file mode 100644 index 0000000..3df31dc --- /dev/null +++ b/three/MF/V3/Descriptors/Calibration.py @@ -0,0 +1,79 @@ +from enum import Enum +from typing import List + + +# Calibration quality. +class Quality(Enum): + Empty = "None" # The calibration does not exist. + Poor = "Poor" # Poor calibration quality. + Fair = "Fair" # Fair calibration quality. + Good = "Good" # Good calibration quality. + Excellent = "Excellent" # Excellent calibration quality. + + +class Camera: + # Camera calibration descriptor. + def __init__(self, quality: 'Quality', date: List[int] = None): + # Calibration quality. + self.quality = quality + # Calibration date and time [year, month, day, hour, minute, second]. + self.date = date + + +class Turntable: + # Turntable calibration descriptor. + def __init__(self, quality: 'Quality', date: List[int] = None, focus: List[int] = None): + # Calibration quality. + self.quality = quality + # Calibration date and time [year, month, day, hour, minute, second]. + self.date = date + # Focus values of each camera during calibration. + self.focus = focus + + +class CaptureTarget: + """ + Calibration capture target. + + The camera calibration capture targets are used to draw quad overlays on the video stream to guide a user as to where to position the calibration card for each capture during camera calibration. + """ + def __init__(self, camera: int, quads: List[float] = None): + # Index of the camera that is displayed to the user for this capture. + self.camera = camera + """ + The target quad for each camera. + This is a set of 16 numbers defining the quad coordinates on the left and right camera. + The first 4 pairs of numbers define the quad on the left camera. + The last 4 pairs of numbers define the quad on the right camera. + """ + self.quads = quads + + +class DetectedCard: + # Detected calibration card descriptor. + class Target: + # Calibration capture target properties. + def __init__(self, match: float, hold: float): + """ + A normalized value indicating how closely the calibration card matches the target + overlay. 0 indicates a poor match. 1 indicates a good match. + """ + self.match = match + """ + A normalized value indicating how long the user has held the calibration card steady over + the target overlay. When the value reaches 1, the user has held the calibration card + steady for the complete required duration. + """ + self.hold = hold + + def __init__(self, size: List[int] = None, quad: List[float] = None, corners: List[float] = None, target: 'Target' = None): + # The calibration card columns and rows. + self.size = size + # The calibration card bounding quadrilateral. + self.quad = quad + # The detected corners of the calibration card. + self.corners = corners + # The capture target properties, if a capture target is specified. + self.target = target + + diff --git a/three/MF/V3/Descriptors/Export.py b/three/MF/V3/Descriptors/Export.py new file mode 100644 index 0000000..258340c --- /dev/null +++ b/three/MF/V3/Descriptors/Export.py @@ -0,0 +1,38 @@ +from MF.V3.Settings.Export import Export as MF_V3_Settings_Export_Export +from enum import Enum +from typing import List + + +class Export: + # Scan data descriptor. + # Geometry face types. + class Face(Enum): + NoFace = "NoFace" # No faces. + Point = "Point" # Point faces. + Line = "Line" # Line faces. + Triangle = "Triangle" # Triangle faces. + Quad = "Quad" # Quad faces. + + # Texture support types. + class Texture(Enum): + Empty = "None" # The format does not support textures. + Single = "Single" # The format supports a single texture only. + Multiple = "Multiple" # The format supports multiple textures. + + def __init__(self, format: MF_V3_Settings_Export_Export.Format, extension: str, description: str, normals: bool, colors: bool, textures: 'Texture', faces: List['Face'] = None): + # Export format. + self.format = format + # Export file extension. e.g. ".ply" + self.extension = extension + # Export format description. e.g. "Polygon format" + self.description = description + # Vertex normal support. + self.normals = normals + # Vertex color support. + self.colors = colors + # Texture (UV) support. + self.textures = textures + # Types of supported faces. + self.faces = faces + + diff --git a/three/MF/V3/Descriptors/Image.py b/three/MF/V3/Descriptors/Image.py new file mode 100644 index 0000000..1e830d4 --- /dev/null +++ b/three/MF/V3/Descriptors/Image.py @@ -0,0 +1,13 @@ +class Image: + # Image descriptor. + def __init__(self, width: int, height: int, step: int, type: int): + # Image width. + self.width = width + # Image height. + self.height = height + # Image row step in bytes. + self.step = step + # OpenCV image [type](https:gist.github.com/yangcha/38f2fa630e223a8546f9b48ebbb3e61a). + self.type = type + + diff --git a/three/MF/V3/Descriptors/Merge.py b/three/MF/V3/Descriptors/Merge.py new file mode 100644 index 0000000..2ebdd84 --- /dev/null +++ b/three/MF/V3/Descriptors/Merge.py @@ -0,0 +1,34 @@ +from typing import List + + +class Merge: + # Merge descriptor. + class Mesh: + # Mesh descriptor. + def __init__(self, name: str, triangles: int, quads: int, positions: int, normals: int, uvs: int, size: int): + # The mesh name. + self.name = name + # Number of mesh triangle faces. + self.triangles = triangles + # Number of quad faces. + self.quads = quads + # Number of vertex positions. + self.positions = positions + # Number of vertex normals. + self.normals = normals + # Number of UV coordinates. + self.uvs = uvs + # Total mesh size in bytes. + self.size = size + + def __init__(self, scans: int, textures: int, maxSimplifyCount: int, meshes: List['Mesh'] = None): + # The number of input scans. + self.scans = scans + # The number of input textures. + self.textures = textures + # The maximum number of faces for the simplify merge step. + self.maxSimplifyCount = maxSimplifyCount + # The set of merged mesh descriptors. + self.meshes = meshes + + diff --git a/three/MF/V3/Descriptors/Network.py b/three/MF/V3/Descriptors/Network.py new file mode 100644 index 0000000..9f5ce91 --- /dev/null +++ b/three/MF/V3/Descriptors/Network.py @@ -0,0 +1,11 @@ +class Interface: + # Network interface descriptor. + def __init__(self, name: str, ip: str, ssid: str): + # The name of the interface. + self.name = name + # The address associated with the interface. + self.ip = ip + # The ssid or name of the network. + self.ssid = ssid + + diff --git a/three/MF/V3/Descriptors/Project.py b/three/MF/V3/Descriptors/Project.py new file mode 100644 index 0000000..90e35bc --- /dev/null +++ b/three/MF/V3/Descriptors/Project.py @@ -0,0 +1,47 @@ +from typing import List + + +class Project: + # V3 project descriptor. + class Brief: + # V3 project brief descriptor. + def __init__(self, index: int, name: str, size: int, modified: List[int] = None): + # Project index. + self.index = index + # Project name. + self.name = name + # Size in bytes. + self.size = size + # Project last modified date and time [year, month, day, hour, minute, second]. + self.modified = modified + + class Group: + # V3 project scan group tree descriptor. + def __init__(self, index: int, name: str, visible: bool, collapsed: bool, color: List[float] = None, rotation: List[float] = None, translation: List[float] = None, scan: int = None, groups: List['Project.Group'] = None): + # Group index. + self.index = index + # Group name. + self.name = name + # Visibility in the renderer. + self.visible = visible + # Collapsed state in the group tree. + self.collapsed = collapsed + # Color in the renderer. + self.color = color + # Axis-angle rotation vector. The direction of the vector is the rotation axis. The magnitude of the vector is rotation angle in radians. + self.rotation = rotation + # Translation vector. + self.translation = translation + # The scan index. If defined this group is a scan and cannot have subgroups. + self.scan = scan + # Subgroups. + self.groups = groups + + def __init__(self, index: int, name: str, groups: 'Group'): + # Project index. + self.index = index + # Project name. + self.name = name + self.groups = groups + + diff --git a/three/MF/V3/Descriptors/ProjectActions.py b/three/MF/V3/Descriptors/ProjectActions.py new file mode 100644 index 0000000..57485bc --- /dev/null +++ b/three/MF/V3/Descriptors/ProjectActions.py @@ -0,0 +1,37 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from typing import List + + +class ProjectAction: + # Descriptor for a project undo/redo action. + class Scan: + # Scan vertices removal/insertion metadata. + def __init__(self, index: int, vertices: int, triangles: int): + # The scan index. + self.index = index + # The number of vertices after undo or redo. + self.vertices = vertices + # The number of triangles after undo or redo. + self.triangles = triangles + + def __init__(self, task: str, project: MF_V3_Descriptors_Project_Project = None, scans: List['Scan'] = None): + # The original websocket task that the action is undoing or redoing. + self.task = task + """ + The updated project data after undo or redo. + If undefined, then there was no change to the project. + """ + self.project = project + # The list of scans whose vertex/triangle elements were changed by the undo/redo action. + self.scans = scans + + +class ProjectActions: + # Project undo and redo action descriptors. + def __init__(self, undo: List[str] = None, redo: List[str] = None): + # Project undo action descriptors. + self.undo = undo + # Project redo action descriptors. + self.redo = redo + + diff --git a/three/MF/V3/Descriptors/RemoveVertices.py b/three/MF/V3/Descriptors/RemoveVertices.py new file mode 100644 index 0000000..2aa3ac3 --- /dev/null +++ b/three/MF/V3/Descriptors/RemoveVertices.py @@ -0,0 +1,26 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from typing import List + + +class RemoveVertices: + # Descriptor a remove vertices task. + class Scan: + # Scan vertex and triangle removal metadata. + def __init__(self, index: int, vertices: int, triangles: int): + # The scan index. + self.index = index + # The number of vertices after the removal. + self.vertices = vertices + # The number of triangles after the removal. + self.triangles = triangles + + def __init__(self, scans: List['Scan'] = None, groups: MF_V3_Descriptors_Project_Project.Group = None): + # The list of scans whose vertices were removed. + self.scans = scans + """ + The updated project data after undo or redo. + If undefined, then there was no change to the project. + """ + self.groups = groups + + diff --git a/three/MF/V3/Descriptors/ScanData.py b/three/MF/V3/Descriptors/ScanData.py new file mode 100644 index 0000000..0442683 --- /dev/null +++ b/three/MF/V3/Descriptors/ScanData.py @@ -0,0 +1,53 @@ +from enum import Enum +from typing import List + + +class ScanData: + # Scan data descriptor. + class Buffer: + # Scan buffer descriptor. + class Component: + # Scan buffer component descriptor. + # Scan buffer component types. + class Type(Enum): + Position = "Position" # Vertex position. + Normal = "Normal" # Vertex normal. + Color = "Color" # Vertex color. + UV = "UV" # Vertex texture coordinate. + Triangle = "Triangle" # Triangle index. + Texture = "Texture" # Texture. + + def __init__(self, type: 'Type', size: int, offset: int, normalized: bool): + # Scan buffer component type. + self.type = type + # Scan buffer component size (ie. the number of elements). + self.size = size + """ + Scan buffer component offset. + This is the starting element for this component at every stride of the buffer. + """ + self.offset = offset + # Indicates if the data is normalized. + self.normalized = normalized + + def __init__(self, stride: int, components: List['Component'] = None): + # Scan buffer stride. This should be greater or equal to the sum of the component sizes. + self.stride = stride + # Scan buffer components. + self.components = components + + def __init__(self, index: int, name: str, buffers: List['Buffer'] = None, mean: List[float] = None, stddev: List[float] = None, axisAlignedBoundingBox: List[float] = None): + # Scan index. + self.index = index + # Scan name. + self.name = name + # Scan buffer descriptors. + self.buffers = buffers + # The mean (centroid) of the vertex positions. + self.mean = mean + # The standard deviation of the vertex positions. + self.stddev = stddev + # The axis-aligned bounding box of the vertex positions. + self.axisAlignedBoundingBox = axisAlignedBoundingBox + + diff --git a/three/MF/V3/Descriptors/Settings/Advanced.py b/three/MF/V3/Descriptors/Settings/Advanced.py new file mode 100644 index 0000000..423fb3e --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Advanced.py @@ -0,0 +1,284 @@ +from MF.V3.Settings.Scan import Scan as MF_V3_Settings_Scan_Scan +from typing import List + + +class Advanced: + # Advanced settings descriptor. + class Use: + # Use advanced settings. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + class Capture: + # Capture settings descriptor. + class HorizontalFrequencies: + def __init__(self, min: int, max: int, value: List[int] = None, default: List[int] = None): + self.min = min + self.max = max + self.value = value + self.default = default + + class VerticalFrequencies: + def __init__(self, min: int, max: int, value: List[int] = None, default: List[int] = None): + self.min = min + self.max = max + self.value = value + self.default = default + + def __init__(self, use: 'Advanced.Use', horizontalFrequencies: 'HorizontalFrequencies', verticalFrequencies: 'VerticalFrequencies'): + self.use = use + self.horizontalFrequencies = horizontalFrequencies + self.verticalFrequencies = verticalFrequencies + + class Sampling: + # Sampling settings descriptor. + class ProjectorSampleRate: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class ImageSampleRate: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, use: 'Advanced.Use', projectorSampleRate: 'ProjectorSampleRate', imageSampleRate: 'ImageSampleRate'): + # Use sampling settings. + self.use = use + self.projectorSampleRate = projectorSampleRate + self.imageSampleRate = imageSampleRate + + class EdgeDetection: + # Edge detection settings descriptor. + class Threshold: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class LaplacianKernelRadius: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class GaussianBlurRadius: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class GaussianBlurStdDev: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class MaximumWidthForProcessing: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, use: 'Advanced.Use', threshold: 'Threshold', laplacianKernelRadius: 'LaplacianKernelRadius', gaussianBlurRadius: 'GaussianBlurRadius', gaussianBlurStdDev: 'GaussianBlurStdDev', maximumWidthForProcessing: 'MaximumWidthForProcessing'): + self.use = use + self.threshold = threshold + self.laplacianKernelRadius = laplacianKernelRadius + self.gaussianBlurRadius = gaussianBlurRadius + self.gaussianBlurStdDev = gaussianBlurStdDev + self.maximumWidthForProcessing = maximumWidthForProcessing + + class PhaseFilter: + # Phase filter settings descriptor. + class KernelRadius: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class SpatialWeightStdDev: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, use: 'Advanced.Use', kernelRadius: 'KernelRadius', spatialWeightStdDev: 'SpatialWeightStdDev'): + self.use = use + self.kernelRadius = kernelRadius + self.spatialWeightStdDev = spatialWeightStdDev + + class AdaptiveSampling: + # Adaptive sampling settings descriptor. + class Type: + def __init__(self, value: MF_V3_Settings_Scan_Scan.Processing.AdaptiveSampling.Type, default: MF_V3_Settings_Scan_Scan.Processing.AdaptiveSampling.Type): + self.value = value + self.default = default + + class Rate: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, use: 'Advanced.Use', type: 'Type', rate: 'Rate'): + self.use = use + self.type = type + self.rate = rate + + class NormalEstimation: + # Normal estimation settings descriptor. + class Method: + def __init__(self, value: MF_V3_Settings_Scan_Scan.Processing.NormalEstimation.Method, default: MF_V3_Settings_Scan_Scan.Processing.NormalEstimation.Method): + self.value = value + self.default = default + + class MaximumNeighbourCount: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class MaximumNeighbourRadius: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class UseMaximumNeighbourCount: + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + class UseMaximumNeighbourRadius: + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, use: 'Advanced.Use', method: 'Method', maximumNeighbourCount: 'MaximumNeighbourCount', maximumNeighbourRadius: 'MaximumNeighbourRadius', useMaximumNeighbourCount: 'UseMaximumNeighbourCount', useMaximumNeighbourRadius: 'UseMaximumNeighbourRadius'): + self.use = use + self.method = method + self.maximumNeighbourCount = maximumNeighbourCount + self.maximumNeighbourRadius = maximumNeighbourRadius + self.useMaximumNeighbourCount = useMaximumNeighbourCount + self.useMaximumNeighbourRadius = useMaximumNeighbourRadius + + class OutlierRemoval: + # Outlier removal settings descriptor. + class NeighbourCount: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class NeighbourRadius: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, use: 'Advanced.Use', neighbourCount: 'NeighbourCount', neighbourRadius: 'NeighbourRadius'): + self.use = use + self.neighbourCount = neighbourCount + self.neighbourRadius = neighbourRadius + + class Remesh: + # Remesh settings descriptor. + class VoxelSize: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class Depth: + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class Scale: + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class LinearInterpolation: + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, use: 'Advanced.Use', voxelSize: 'VoxelSize', depth: 'Depth', scale: 'Scale', linearInterpolation: 'LinearInterpolation'): + self.use = use + self.voxelSize = voxelSize + self.depth = depth + self.scale = scale + self.linearInterpolation = linearInterpolation + + class Camera: + # Camera settings descriptor. + class UseContinuousExposureValues: + # Use continuous exposure values settings descriptor. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, useContinuousExposureValues: 'UseContinuousExposureValues'): + # Use continuous exposure values settings descriptor. + self.useContinuousExposureValues = useContinuousExposureValues + + class Turntable: + # Turntable settings descriptor. + class RampAngle: + # The angle in degrees to slow down the turntable at the end of a rotation. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, rampAngle: 'RampAngle'): + # The angle in degrees to slow down the turntable at the end of a rotation. + self.rampAngle = rampAngle + + def __init__(self, capture: 'Capture', sampling: 'Sampling', edgeDetection: 'EdgeDetection', phaseFilter: 'PhaseFilter', adaptiveSampling: 'AdaptiveSampling', normalEstimation: 'NormalEstimation', outlierRemoval: 'OutlierRemoval', remesh: 'Remesh', camera: 'Camera', turntable: 'Turntable'): + # Capture settings descriptor. + self.capture = capture + # Sampling settings descriptor. + self.sampling = sampling + # Edge detection settings descriptor. + self.edgeDetection = edgeDetection + # Phase filter settings descriptor. + self.phaseFilter = phaseFilter + # Adaptive sampling settings descriptor. + self.adaptiveSampling = adaptiveSampling + # Normal estimation settings descriptor. + self.normalEstimation = normalEstimation + # Outlier removal settings descriptor. + self.outlierRemoval = outlierRemoval + # Remesh settings descriptor. + self.remesh = remesh + # Camera settings descriptor. + self.camera = camera + # Turntable settings descriptor. + self.turntable = turntable + + diff --git a/three/MF/V3/Descriptors/Settings/Camera.py b/three/MF/V3/Descriptors/Settings/Camera.py new file mode 100644 index 0000000..df45cc0 --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Camera.py @@ -0,0 +1,71 @@ +from MF.V3.Settings.Rectangle import Rectangle as MF_V3_Settings_Rectangle_Rectangle +from typing import List + + +class Camera: + # Camera settings descriptor. + class AutoExposure: + # Auto exposure. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + class Exposure: + # Exposure. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class AnalogGain: + # Analog gain. + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class DigitalGain: + # Digital gain. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class Focus: + # Focus settings descriptor. + class Value: + # Focus value. + def __init__(self, min: int, max: int, value: List[int] = None, default: List[int] = None): + self.min = min + self.max = max + self.value = value + self.default = default + + class Box: + # Auto focus box. + def __init__(self, value: List[MF_V3_Settings_Rectangle_Rectangle] = None, default: List[MF_V3_Settings_Rectangle_Rectangle] = None): + self.value = value + self.default = default + + def __init__(self, value: 'Value', box: 'Box'): + # Focus value. + self.value = value + # Auto focus box. + self.box = box + + def __init__(self, autoExposure: 'AutoExposure', exposure: 'Exposure', analogGain: 'AnalogGain', digitalGain: 'DigitalGain', focus: 'Focus'): + # Auto exposure. + self.autoExposure = autoExposure + # Exposure. + self.exposure = exposure + # Analog gain. + self.analogGain = analogGain + # Digital gain. + self.digitalGain = digitalGain + # Focus settings descriptor. + self.focus = focus + + diff --git a/three/MF/V3/Descriptors/Settings/Capture.py b/three/MF/V3/Descriptors/Settings/Capture.py new file mode 100644 index 0000000..8143c07 --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Capture.py @@ -0,0 +1,46 @@ +from MF.V3.Settings.Quality import Quality as MF_V3_Settings_Quality_Quality + + +class Capture: + # Capture settings descriptor. + class Quality: + # Capture quality preset. + def __init__(self, value: MF_V3_Settings_Quality_Quality, default: MF_V3_Settings_Quality_Quality): + self.value = value + self.default = default + + class Texture: + # Capture texture. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + class BlendCount: + # Capture image blend count for noise reduction. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class BlendFrequency: + # The starting frequency for which multiple capture images are blended. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, quality: 'Quality', texture: 'Texture', blendCount: 'BlendCount', horizontalBlendFrequency: 'BlendFrequency', verticalBlendFrequency: 'BlendFrequency'): + # Capture quality preset. + self.quality = quality + # Capture texture. + self.texture = texture + # Capture blend count. + self.blendCount = blendCount + # Starting horizontal blend frequency. + self.horizontalBlendFrequency = horizontalBlendFrequency + # Starting vertical blend frequency. + self.verticalBlendFrequency = verticalBlendFrequency + + diff --git a/three/MF/V3/Descriptors/Settings/I18n.py b/three/MF/V3/Descriptors/Settings/I18n.py new file mode 100644 index 0000000..f6ece28 --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/I18n.py @@ -0,0 +1,16 @@ +from MF.V3.Settings.I18n import I18n as MF_V3_Settings_I18n_I18n + + +class I18n: + # I18n language settings descriptor. + class Language: + # Language settings descriptor. + def __init__(self, value: MF_V3_Settings_I18n_I18n.Language, default: MF_V3_Settings_I18n_I18n.Language): + self.value = value + self.default = default + + def __init__(self, language: 'Language'): + # Language settings descriptor. + self.language = language + + diff --git a/three/MF/V3/Descriptors/Settings/Projector.py b/three/MF/V3/Descriptors/Settings/Projector.py new file mode 100644 index 0000000..60310dc --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Projector.py @@ -0,0 +1,23 @@ +class Projector: + # Projector settings descriptor. + class Brightness: + # Projector brightness. + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + class On: + # Projector on/off. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, brightness: 'Brightness', on: 'On'): + # Projector brightness. + self.brightness = brightness + # Projector on/off. + self.on = on + + diff --git a/three/MF/V3/Descriptors/Settings/Scanner.py b/three/MF/V3/Descriptors/Settings/Scanner.py new file mode 100644 index 0000000..f0d2e6a --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Scanner.py @@ -0,0 +1,37 @@ +from MF.V3.Descriptors.Settings.Advanced import Advanced as MF_V3_Descriptors_Settings_Advanced_Advanced +from MF.V3.Descriptors.Settings.Camera import Camera as MF_V3_Descriptors_Settings_Camera_Camera +from MF.V3.Descriptors.Settings.Capture import Capture as MF_V3_Descriptors_Settings_Capture_Capture +from MF.V3.Descriptors.Settings.I18n import I18n as MF_V3_Descriptors_Settings_I18n_I18n +from MF.V3.Descriptors.Settings.Projector import Projector as MF_V3_Descriptors_Settings_Projector_Projector +from MF.V3.Descriptors.Settings.Software import Software as MF_V3_Descriptors_Settings_Software_Software +from MF.V3.Descriptors.Settings.Style import Style as MF_V3_Descriptors_Settings_Style_Style +from MF.V3.Descriptors.Settings.Turntable import Turntable as MF_V3_Descriptors_Settings_Turntable_Turntable +from MF.V3.Descriptors.Settings.Tutorials import Tutorials as MF_V3_Descriptors_Settings_Tutorials_Tutorials +from MF.V3.Descriptors.Settings.Viewer import Viewer as MF_V3_Descriptors_Settings_Viewer_Viewer + + +class Scanner: + # Scanner settings descriptor. + def __init__(self, advanced: MF_V3_Descriptors_Settings_Advanced_Advanced, camera: MF_V3_Descriptors_Settings_Camera_Camera, capture: MF_V3_Descriptors_Settings_Capture_Capture, projector: MF_V3_Descriptors_Settings_Projector_Projector, i18n: MF_V3_Descriptors_Settings_I18n_I18n, style: MF_V3_Descriptors_Settings_Style_Style, turntable: MF_V3_Descriptors_Settings_Turntable_Turntable, tutorials: MF_V3_Descriptors_Settings_Tutorials_Tutorials, viewer: MF_V3_Descriptors_Settings_Viewer_Viewer, software: MF_V3_Descriptors_Settings_Software_Software): + # Advanced settings descriptor. + self.advanced = advanced + # Camera settings descriptor. + self.camera = camera + # Capture settings descriptor. + self.capture = capture + # Projector settings descriptor. + self.projector = projector + # Internalization setting descriptor. + self.i18n = i18n + # Style settings descriptor. + self.style = style + # Turntable settings descriptor. + self.turntable = turntable + # Tutorials settings descriptor. + self.tutorials = tutorials + # Viewer settings descriptor. + self.viewer = viewer + # Software settings descriptor. + self.software = software + + diff --git a/three/MF/V3/Descriptors/Settings/Software.py b/three/MF/V3/Descriptors/Settings/Software.py new file mode 100644 index 0000000..2bcc7bb --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Software.py @@ -0,0 +1,13 @@ +class Software: + # Software settings descriptor. + class UpdateMajor: + # Enable major version updates which can have breaking API changes. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, updateMajor: 'UpdateMajor'): + # Enable major version updates which can have breaking API changes. + self.updateMajor = updateMajor + + diff --git a/three/MF/V3/Descriptors/Settings/Style.py b/three/MF/V3/Descriptors/Settings/Style.py new file mode 100644 index 0000000..274447f --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Style.py @@ -0,0 +1,16 @@ +from MF.V3.Settings.Style import Style as MF_V3_Settings_Style_Style + + +class Style: + # Style settings descriptor. + class Theme: + # Theme settings descriptor. + def __init__(self, value: MF_V3_Settings_Style_Style.Theme, default: MF_V3_Settings_Style_Style.Theme): + self.value = value + self.default = default + + def __init__(self, theme: 'Theme'): + # Theme settings descriptor. + self.theme = theme + + diff --git a/three/MF/V3/Descriptors/Settings/Turntable.py b/three/MF/V3/Descriptors/Settings/Turntable.py new file mode 100644 index 0000000..1655db7 --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Turntable.py @@ -0,0 +1,33 @@ +class Turntable: + # Turntable settings descriptor. + class Scans: + # The number of turntable scans. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class Sweep: + # Turntable angle sweep in degrees. + def __init__(self, value: int, default: int, min: int, max: int): + self.value = value + self.default = default + self.min = min + self.max = max + + class Use: + # Use the turntable. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + def __init__(self, scans: 'Scans', sweep: 'Sweep', use: 'Use'): + # The number of turntable scans. + self.scans = scans + # Turntable angle sweep in degrees. + self.sweep = sweep + # Use the turntable. + self.use = use + + diff --git a/three/MF/V3/Descriptors/Settings/Tutorials.py b/three/MF/V3/Descriptors/Settings/Tutorials.py new file mode 100644 index 0000000..ea01eea --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Tutorials.py @@ -0,0 +1,30 @@ +from typing import List + + +class Tutorials: + # Tutorials settings descriptor. + class Show: + # Tutorials to show. + def __init__(self, value: bool, default: bool): + self.value = value + self.default = default + + class Viewed: + # Viewed tutorials. + class Pages: + # Viewed tutorials pages. + def __init__(self, value: List[str] = None, default: List[str] = None): + self.value = value + self.default = default + + def __init__(self, pages: 'Pages'): + # Viewed tutorials pages. + self.pages = pages + + def __init__(self, show: 'Show', viewed: 'Viewed'): + # Show tutorials. + self.show = show + # Viewed tutorials. + self.viewed = viewed + + diff --git a/three/MF/V3/Descriptors/Settings/Viewer.py b/three/MF/V3/Descriptors/Settings/Viewer.py new file mode 100644 index 0000000..296fead --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/Viewer.py @@ -0,0 +1,15 @@ +class Viewer: + # 3D Viewer settings descriptor. + class TextureOpacity: + # Texture opacity. + def __init__(self, value: float, default: float, min: float, max: float): + self.value = value + self.default = default + self.min = min + self.max = max + + def __init__(self, textureOpacity: 'TextureOpacity'): + # Texture opacity. + self.textureOpacity = textureOpacity + + diff --git a/three/MF/V3/Descriptors/Settings/__init__.py b/three/MF/V3/Descriptors/Settings/__init__.py new file mode 100644 index 0000000..03ad69e --- /dev/null +++ b/three/MF/V3/Descriptors/Settings/__init__.py @@ -0,0 +1,11 @@ +from MF.V3.Descriptors.Settings.Advanced import * +from MF.V3.Descriptors.Settings.Camera import * +from MF.V3.Descriptors.Settings.Capture import * +from MF.V3.Descriptors.Settings.I18n import * +from MF.V3.Descriptors.Settings.Projector import * +from MF.V3.Descriptors.Settings.Scanner import * +from MF.V3.Descriptors.Settings.Software import * +from MF.V3.Descriptors.Settings.Style import * +from MF.V3.Descriptors.Settings.Turntable import * +from MF.V3.Descriptors.Settings.Tutorials import * +from MF.V3.Descriptors.Settings.Viewer import * diff --git a/three/MF/V3/Descriptors/Software.py b/three/MF/V3/Descriptors/Software.py new file mode 100644 index 0000000..4042713 --- /dev/null +++ b/three/MF/V3/Descriptors/Software.py @@ -0,0 +1,19 @@ +class Software: + # Software descriptor. + class Package: + # Software package descriptor. + def __init__(self, installed: str, update: str, changelog: str): + # The package installed version. + self.installed = installed + # The package update version. Empty if no update is available. + self.update = update + # The package changelog. Empty if no update is available or the update is a downgrade. + self.changelog = changelog + + def __init__(self, frontend: 'Package', server: 'Package'): + # Frontend software package. + self.frontend = frontend + # Server software package. + self.server = server + + diff --git a/three/MF/V3/Descriptors/System.py b/three/MF/V3/Descriptors/System.py new file mode 100644 index 0000000..3c8b3a4 --- /dev/null +++ b/three/MF/V3/Descriptors/System.py @@ -0,0 +1,19 @@ +class System: + # System descriptor. + class DiskSpace: + # Disk space descriptor. + def __init__(self, capacity: int, available: int): + # Disk space capacity in bytes. + self.capacity = capacity + # Available disk space in bytes. + self.available = available + + def __init__(self, serialNumber: str, diskSpace: 'DiskSpace', publicKey: str): + # Serial number; + self.serialNumber = serialNumber + # Used and available disk space. + self.diskSpace = diskSpace + # GPG public key. + self.publicKey = publicKey + + diff --git a/three/MF/V3/Descriptors/Transform.py b/three/MF/V3/Descriptors/Transform.py new file mode 100644 index 0000000..8638d74 --- /dev/null +++ b/three/MF/V3/Descriptors/Transform.py @@ -0,0 +1,16 @@ +from typing import List + + +class Transform: + # V3 transform descriptor. + def __init__(self, rotation: List[float] = None, translation: List[float] = None): + """ + Axis-angle rotation vector. + The direction of the vector is the rotation axis. + The magnitude of the vector is rotation angle in radians. + """ + self.rotation = rotation + # Translation vector. + self.translation = translation + + diff --git a/three/MF/V3/Descriptors/VideoFrame.py b/three/MF/V3/Descriptors/VideoFrame.py new file mode 100644 index 0000000..286925c --- /dev/null +++ b/three/MF/V3/Descriptors/VideoFrame.py @@ -0,0 +1,27 @@ +from MF.V3.Descriptors.Calibration import DetectedCard as MF_V3_Descriptors_Calibration_DetectedCard +from MF.V3.Settings.Video import Video as MF_V3_Settings_Video_Video + + +class VideoFrame: + # Video frame descriptor. + def __init__(self, codec: MF_V3_Settings_Video_Video.Codec, format: MF_V3_Settings_Video_Video.Format, width: int, height: int, step: int, number: int, timestamp: int, duration: int, calibrationCard: MF_V3_Descriptors_Calibration_DetectedCard): + # Video codec. + self.codec = codec + # Pixel format. + self.format = format + # Image width. + self.width = width + # Image height. + self.height = height + # Image row step in bytes. + self.step = step + # Frame number. + self.number = number + # Frame timestamp. + self.timestamp = timestamp + # Frame duration. + self.duration = duration + # Calibration card detection. + self.calibrationCard = calibrationCard + + diff --git a/three/MF/V3/Descriptors/Wifi.py b/three/MF/V3/Descriptors/Wifi.py new file mode 100644 index 0000000..b2fe4b8 --- /dev/null +++ b/three/MF/V3/Descriptors/Wifi.py @@ -0,0 +1,26 @@ +from typing import List + + +class Wifi: + # The wifi descriptor. + class Network: + # The wifi network descriptor. + def __init__(self, ssid: str, isPublic: bool, isActive: bool, password: str = None, quality: int = None): + # The service set identifier. + self.ssid = ssid + # Is the network public? + self.isPublic = isPublic + # Is the network active? + self.isActive = isActive + # The network password. + self.password = password + # Signal quality [0 ; 100]. + self.quality = quality + + def __init__(self, ssid: str, networks: List['Network'] = None): + # The wifi ssid. + self.ssid = ssid + # The list of wifi networks. + self.networks = networks + + diff --git a/three/MF/V3/Descriptors/__init__.py b/three/MF/V3/Descriptors/__init__.py new file mode 100644 index 0000000..0914107 --- /dev/null +++ b/three/MF/V3/Descriptors/__init__.py @@ -0,0 +1,13 @@ +from MF.V3.Descriptors.BoundingBox import * +from MF.V3.Descriptors.Export import * +from MF.V3.Descriptors.Image import * +from MF.V3.Descriptors.Merge import * +from MF.V3.Descriptors.Project import * +from MF.V3.Descriptors.ProjectActions import * +from MF.V3.Descriptors.RemoveVertices import * +from MF.V3.Descriptors.ScanData import * +from MF.V3.Descriptors.Software import * +from MF.V3.Descriptors.System import * +from MF.V3.Descriptors.Transform import * +from MF.V3.Descriptors.VideoFrame import * +from MF.V3.Descriptors.Wifi import * diff --git a/three/MF/V3/Settings/Advanced.py b/three/MF/V3/Settings/Advanced.py new file mode 100644 index 0000000..a54ba9b --- /dev/null +++ b/three/MF/V3/Settings/Advanced.py @@ -0,0 +1,185 @@ +from MF.V3.Settings.Quality import Quality as MF_V3_Settings_Quality_Quality +from MF.V3.Settings.Scan import Scan as MF_V3_Settings_Scan_Scan +from typing import List + + +class Advanced: + # Advanced settings. + class Capture: + # Capture settings. + def __init__(self, horizontalFrequencies: List[int] = None, verticalFrequencies: List[int] = None, use: bool = None): + # Projector sample rate. + self.horizontalFrequencies = horizontalFrequencies + # Image sample rate. + self.verticalFrequencies = verticalFrequencies + # Use the capture settings. + self.use = use + + class Sampling: + # Sampling settings. + def __init__(self, projectorSampleRate: float = None, imageSampleRate: float = None, use: bool = None): + # Projector sample rate. + self.projectorSampleRate = projectorSampleRate + # Image sample rate. + self.imageSampleRate = imageSampleRate + # Use the sampling settings. + self.use = use + + class EdgeDetection: + # Edge detection settings. + def __init__(self, threshold: float = None, laplacianKernelRadius: int = None, gaussianBlurRadius: int = None, gaussianBlurStdDev: float = None, maximumWidthForProcessing: int = None, use: bool = None): + # The edge detection threshold. + self.threshold = threshold + # The Laplacian kernel radius. This must be in the range [1..5]. + self.laplacianKernelRadius = laplacianKernelRadius + """ + Gaussian blur kernel radius. (Optional) To disable, set to 0. + + The phase images can optionally blurred before taking the Laplacian to reduce noise. + However as a result, the detected edges are wider. + """ + self.gaussianBlurRadius = gaussianBlurRadius + """ + Gaussian blur kernel standard deviation. This parameter is ignored if + \p gaussianBlurSize is zero. + """ + self.gaussianBlurStdDev = gaussianBlurStdDev + """ + The maximum image width for processing. (Optional) To disable, set to 0. + + If this value is greater than zero, the phase images are resized to the maximum + width prior to computing the Laplacian and the the detected edges are then upsampled to the + original size. + + This would be done to speed up processing or to detect edges on a larger scale. + """ + self.maximumWidthForProcessing = maximumWidthForProcessing + # Use the edge detection settings. + self.use = use + + class PhaseFilter: + # Phase filter settings. + def __init__(self, kernelRadius: int = None, spatialWeightStdDev: float = None, use: bool = None): + """ + The filter kernel radius. + + A neighboring value must be within this radius to be included in the filter. + If the kernel radius is set to zero, the phase filtering is disabled. + """ + self.kernelRadius = kernelRadius + """ + The standard deviation of the spatial weights. + + The weight of a neighboring value is \f$ exp(-(r/s)^2) \f$ where \f$ r \f$ + is the distance to the central value and \f$ s \f$ is the spatial weight + standard deviation. + + If the spatial weight standard deviation is set to zero, all the spatial + weights are uniformly set to 1. + """ + self.spatialWeightStdDev = spatialWeightStdDev + # Use the phase filter settings. + self.use = use + + class AdaptiveSampling: + """ + Adaptive sampling settings + + Adaptive sampling will downsample points in regions of low detail + and keep points in regions of high detail. + """ + def __init__(self, rate: float, type: MF_V3_Settings_Scan_Scan.Processing.AdaptiveSampling.Type = None, use: bool = None): + # The sample rate [0..1] for the regions of low detail. + self.rate = rate + # Sampling type. + self.type = type + # Use the adaptive sampling settings. + self.use = use + + class PointClipping: + # Point32 clipping settings. + def __init__(self, type: MF_V3_Settings_Scan_Scan.Processing.PointClipping.Type = None, transform: List[float] = None, use: bool = None): + # Point32 clipping type. + self.type = type + # 4x4 transform mapping 3D points to the canonical point32 clipping coordinates. + self.transform = transform + # Use the point32 clipping settings. + self.use = use + + class NormalEstimation: + # Normal estimation settings. + def __init__(self, method: MF_V3_Settings_Scan_Scan.Processing.NormalEstimation.Method = None, maximumNeighbourCount: int = None, maximumNeighbourRadius: float = None, useMaximumNeighbourCount: bool = None, useMaximumNeighbourRadius: bool = None, use: bool = None): + # Normal estimation method. + self.method = method + """ + Maximum number of nearest neighbors used to compute the normal. + This value is only used with the NORMAL_OPEN3D method. + """ + self.maximumNeighbourCount = maximumNeighbourCount + # Maximum radius for a point32 to be considered a neighbour. + self.maximumNeighbourRadius = maximumNeighbourRadius + self.useMaximumNeighbourCount = useMaximumNeighbourCount + self.useMaximumNeighbourRadius = useMaximumNeighbourRadius + # Use the normal estimation settings. + self.use = use + + class OutlierRemoval: + # Radius outlier removal settings. + def __init__(self, neighbourCount: int = None, neighbourRadius: float = None, use: bool = None): + # The minimum number of points within the radius for a point32 to be retained. + self.neighbourCount = neighbourCount + # The neighbour search radius. + self.neighbourRadius = neighbourRadius + # Use the outlier removal settings. + self.use = use + + class Remesh: + # Remesh settings. + def __init__(self, quality: MF_V3_Settings_Quality_Quality = None, voxelSize: float = None, depth: int = None, scale: float = None, linearInterpolation: bool = None, use: bool = None): + # Remesh quality preset. + self.quality = quality + # Voxel size. + self.voxelSize = voxelSize + # Depth. + self.depth = depth + # Scale. + self.scale = scale + # Linear Interpolation. + self.linearInterpolation = linearInterpolation + # Use the remesh settings. + self.use = use + + class Camera: + # Camera settings. + def __init__(self, useContinuousExposureValues: bool = None): + self.useContinuousExposureValues = useContinuousExposureValues + + class Turntable: + # Turntable settings. + def __init__(self, rampAngle: int = None): + # The angle in degrees to slow down the turntable at the end of a rotation. + self.rampAngle = rampAngle + + def __init__(self, capture: 'Capture' = None, sampling: 'Sampling' = None, edgeDetection: 'EdgeDetection' = None, phaseFilter: 'PhaseFilter' = None, adaptiveSampling: 'AdaptiveSampling' = None, normalEstimation: 'NormalEstimation' = None, outlierRemoval: 'OutlierRemoval' = None, remesh: 'Remesh' = None, camera: 'Camera' = None, turntable: 'Turntable' = None): + # Capture settings. + self.capture = capture + # Sampling settings. + self.sampling = sampling + # Edge detection settings. + self.edgeDetection = edgeDetection + # Phase filter settings. + self.phaseFilter = phaseFilter + # Adaptive sampling settings. + self.adaptiveSampling = adaptiveSampling + # Normal estimation settings. + self.normalEstimation = normalEstimation + # Radius outlier removal settings. + self.outlierRemoval = outlierRemoval + # Remesh settings. + self.remesh = remesh + # Camera settings. + self.camera = camera + # Turntable settings. + self.turntable = turntable + + diff --git a/three/MF/V3/Settings/Align.py b/three/MF/V3/Settings/Align.py new file mode 100644 index 0000000..c30616b --- /dev/null +++ b/three/MF/V3/Settings/Align.py @@ -0,0 +1,82 @@ +from enum import Enum +from typing import List + + +class Align: + # Alignment settings. + class Points: + # Point pair alignment settings. + def __init__(self, points: List[float] = None, absoluteError: float = None, relativeError: float = None, useAllPoints: bool = None): + # The set of corresponding point pairs. + self.points = points + # The maximum absolute error for a point pair to be an inlier to the model. + self.absoluteError = absoluteError + # The maximum error relative to the size of the aligned scans for a point pair to be an inlier to the model. + self.relativeError = relativeError + # Ignore alignment errors and use all selected points for alignment. + self.useAllPoints = useAllPoints + + class Ransac: + # Ransac alignment settings. + def __init__(self, downsampleVoxelSize: float = None, maximumFeatureRadius: float = None, maximumFeaturePointCount: int = None, maximumMatchDistance: float = None, minimumMatchSimilarity: float = None, mutualFilter: bool = None): + self.downsampleVoxelSize = downsampleVoxelSize + self.maximumFeatureRadius = maximumFeatureRadius + self.maximumFeaturePointCount = maximumFeaturePointCount + self.maximumMatchDistance = maximumMatchDistance + self.minimumMatchSimilarity = minimumMatchSimilarity + self.mutualFilter = mutualFilter + + class ICP: + # Iterative closest point alignment settings. + def __init__(self, matchDistance: float): + # The maximum distance for two points to be considered a match. + self.matchDistance = matchDistance + + class Rough: + # Rough alignment settings. + # Rough alignment methods. + class Method(Enum): + Empty = "None" # No rough alignment. + FastGlobal = "FastGlobal" # Fast global alignment. + Ransac = "Ransac" # Ransac alignment. + Points = "Points" # Point pair alignment. + + def __init__(self, method: 'Method', ransac: 'Align.Ransac' = None, points: 'Align.Points' = None): + # Rough alignment method. + self.method = method + # FastGlobal fastGlobal; + self.ransac = ransac + # Point pair alignment settings. + self.points = points + + class Fine: + # Fine alignment settings. + # Fine alignment methods. + class Method(Enum): + Empty = "None" # No fine alignment. + ICP = "ICP" # Iterative closest point alignment. + + class Transform: + def __init__(self, rotation: List[float] = None, translation: List[float] = None): + self.rotation = rotation + self.translation = translation + + def __init__(self, method: 'Method', icp: 'Align.ICP' = None, initialTransform: 'Transform' = None): + # Fine alignment method. + self.method = method + # Iterative closest point settings. + self.icp = icp + # The initial transform for fine alignment. + self.initialTransform = initialTransform + + def __init__(self, source: int, target: int, rough: 'Rough' = None, fine: 'Fine' = None): + # Index of the scan or group to align. + self.source = source + # Index of the scan or group to align to. + self.target = target + # Rough alignment settings. + self.rough = rough + # Fine alignment settings. + self.fine = fine + + diff --git a/three/MF/V3/Settings/AutoFocus.py b/three/MF/V3/Settings/AutoFocus.py new file mode 100644 index 0000000..8a5e494 --- /dev/null +++ b/three/MF/V3/Settings/AutoFocus.py @@ -0,0 +1,24 @@ +from MF.V3.Settings.Rectangle import Rectangle as MF_V3_Settings_Rectangle_Rectangle +from typing import List + + +class AutoFocus: + # Auto focus settings. + class Camera: + # Auto focus camera settings. + def __init__(self, index: int, box: MF_V3_Settings_Rectangle_Rectangle = None): + # The index of the camera on which to apply auto focus. + self.index = index + # The image rectangle in video image pixels on which to apply auto focus. + self.box = box + + def __init__(self, applyAll: bool, cameras: List['Camera'] = None): + """ + Apply the final focus value to both cameras. + This setting is ignored if more than one camera is selected. + """ + self.applyAll = applyAll + # The set of cameras on which to apply auto focus. + self.cameras = cameras + + diff --git a/three/MF/V3/Settings/BoundingBox.py b/three/MF/V3/Settings/BoundingBox.py new file mode 100644 index 0000000..3dd48ac --- /dev/null +++ b/three/MF/V3/Settings/BoundingBox.py @@ -0,0 +1,15 @@ +from MF.V3.Settings.ScanSelection import ScanSelection as MF_V3_Settings_ScanSelection_ScanSelection + + +class BoundingBox: + # Bounding box settings. + def __init__(self, selection: MF_V3_Settings_ScanSelection_ScanSelection, axisAligned: bool): + # The scan selection. + self.selection = selection + """ + If `true`, align the bounding box with the world axes. + Otherwise orient the bounding box with the scans. + """ + self.axisAligned = axisAligned + + diff --git a/three/MF/V3/Settings/Camera.py b/three/MF/V3/Settings/Camera.py new file mode 100644 index 0000000..3054771 --- /dev/null +++ b/three/MF/V3/Settings/Camera.py @@ -0,0 +1,20 @@ +from typing import List + + +class Camera: + # Camera settings. + def __init__(self, selection: List[int] = None, autoExposure: bool = None, exposure: int = None, analogGain: float = None, digitalGain: int = None, focus: int = None): + # Camera selection. Default is all cameras. + self.selection = selection + # Auto exposure. + self.autoExposure = autoExposure + # Exposure. + self.exposure = exposure + # Analog gain. + self.analogGain = analogGain + # Digital gain. + self.digitalGain = digitalGain + # Focus value. + self.focus = focus + + diff --git a/three/MF/V3/Settings/Capture.py b/three/MF/V3/Settings/Capture.py new file mode 100644 index 0000000..40834e6 --- /dev/null +++ b/three/MF/V3/Settings/Capture.py @@ -0,0 +1,25 @@ +from MF.V3.Settings.Quality import Quality as MF_V3_Settings_Quality_Quality +from typing import List + + +class Capture: + # Capture settings. + def __init__(self, quality: MF_V3_Settings_Quality_Quality = None, texture: bool = None, calibrationCard: bool = None, horizontalFrequencies: List[int] = None, verticalFrequencies: List[int] = None, blendCount: int = None, horizontalBlendFrequency: int = None, verticalBlendFrequency: int = None): + # Capture quality preset. + self.quality = quality + # Capture texture. + self.texture = texture + # Detect the calibration card. + self.calibrationCard = calibrationCard + # Horizontal pattern frequencies. + self.horizontalFrequencies = horizontalFrequencies + # Vertical pattern frequencies. + self.verticalFrequencies = verticalFrequencies + # The number of capture images blended together for noise reduction. + self.blendCount = blendCount + # The starting horizontal frequency for blending capture images for noise reduction. + self.horizontalBlendFrequency = horizontalBlendFrequency + # The starting vertical frequency for blending capture images for noise reduction. + self.verticalBlendFrequency = verticalBlendFrequency + + diff --git a/three/MF/V3/Settings/CopyGroup.py b/three/MF/V3/Settings/CopyGroup.py new file mode 100644 index 0000000..1e172c9 --- /dev/null +++ b/three/MF/V3/Settings/CopyGroup.py @@ -0,0 +1,25 @@ +from typing import List + + +class CopyGroup: + # Copy scan group settings. + def __init__(self, sourceIndexes: List[int] = None, targetIndex: int = None, childPosition: int = None, nameSuffix: str = None): + # The indexes of the groups to copy. + self.sourceIndexes = sourceIndexes + """ + The index of the group into which the source group are copied. + If unspecified the copied groups are added to the root of the group tree. + """ + self.targetIndex = targetIndex + """ + The position among the target group's children where the copied groups are inserted. + If unspecified the copied groups are appended to the end of the target group's children. + """ + self.childPosition = childPosition + """ + Optional name suffix to append to the copied group names. + If unspecified the names are unchanged. + """ + self.nameSuffix = nameSuffix + + diff --git a/three/MF/V3/Settings/CopyProject.py b/three/MF/V3/Settings/CopyProject.py new file mode 100644 index 0000000..7000d81 --- /dev/null +++ b/three/MF/V3/Settings/CopyProject.py @@ -0,0 +1,9 @@ +class CopyProject: + # Copy project settings. + def __init__(self, index: int, copyName: str = None): + # The index of the project to copy. + self.index = index + # The name of project copy. If unspecified a default copy name is generated. + self.copyName = copyName + + diff --git a/three/MF/V3/Settings/Export.py b/three/MF/V3/Settings/Export.py new file mode 100644 index 0000000..22a82ae --- /dev/null +++ b/three/MF/V3/Settings/Export.py @@ -0,0 +1,29 @@ +from MF.V3.Settings.ScanSelection import ScanSelection as MF_V3_Settings_ScanSelection_ScanSelection +from enum import Enum + + +class Export: + # Export settings. + # Scan export formats. + class Format(Enum): + ply = "ply" # Polygon format. + dae = "dae" # Digital asset exchange format. + fbx = "fbx" # Autodesk format. + glb = "glb" # GL transmission format. + obj = "obj" # Wavefront format. + stl = "stl" # Stereolithography format. + xyz = "xyz" # Chemical format. + + def __init__(self, selection: MF_V3_Settings_ScanSelection_ScanSelection = None, texture: bool = None, merge: bool = None, format: 'Format' = None, scale: float = None): + # The scan selection. + self.selection = selection + # Export textures. + self.texture = texture + # Merge the scans into a single file. + self.merge = merge + # The export format. + self.format = format + # Scale factor of the exported geometry. + self.scale = scale + + diff --git a/three/MF/V3/Settings/Group.py b/three/MF/V3/Settings/Group.py new file mode 100644 index 0000000..ab28619 --- /dev/null +++ b/three/MF/V3/Settings/Group.py @@ -0,0 +1,26 @@ +from typing import List + + +class Group: + # Scan group settings. + def __init__(self, index: int, name: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None): + # The unique group index that identifies the group within the group tree. + self.index = index + # Group name. + self.name = name + # Color in the renderer. + self.color = color + # Visibility in the renderer. + self.visible = visible + # Collapsed state in the group tree. + self.collapsed = collapsed + """ + Axis-angle rotation vector. + The direction of the vector is the rotation axis. + The magnitude of the vector is rotation angle in radians. + """ + self.rotation = rotation + # Translation vector. + self.translation = translation + + diff --git a/three/MF/V3/Settings/I18n.py b/three/MF/V3/Settings/I18n.py new file mode 100644 index 0000000..b6d2957 --- /dev/null +++ b/three/MF/V3/Settings/I18n.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class I18n: + # I18n language settings. + # Available languages. + class Language(Enum): + en = "en" + fr = "fr" + de = "de" + + def __init__(self, language: 'Language' = None): + # The language setting. Supported languages are ["en", "fr", "de"]. + self.language = language + + diff --git a/three/MF/V3/Settings/Merge.py b/three/MF/V3/Settings/Merge.py new file mode 100644 index 0000000..1772137 --- /dev/null +++ b/three/MF/V3/Settings/Merge.py @@ -0,0 +1,85 @@ +from MF.V3.Settings.Quality import Quality as MF_V3_Settings_Quality_Quality +from MF.V3.Settings.ScanSelection import ScanSelection as MF_V3_Settings_ScanSelection_ScanSelection +from enum import Enum + + +class Merge: + # Scan merge settings. + class Remesh: + # Remesh settings. + # Remesh method. + class Method(Enum): + FlowTriangles = "FlowTriangles" # Flow remesh as triangles. + FlowQuads = "FlowQuads" # Flow remesh as quads. + FlowQuadDominant = "FlowQuadDominant" # Flow remesh as quad-dominant mesh. + PoissonTriangles = "PoissonTriangles" # Poisson remesh as triangles. + + class Flow: + # Flow remesh settings + def __init__(self, scale: float = None, faceCount: int = None, vertexCount: int = None, creaseAngleThreshold: float = None, extrinsicSmoothness: bool = None, alignToBoundaries: bool = None, smoothIterations: int = None, knnPoints: int = None, deterministic: bool = None): + # Output resolution scale. Smaller means more faces. + self.scale = scale + # The approximate number of remeshed faces. + self.faceCount = faceCount + # The approximate number of remeshed vertices. + self.vertexCount = vertexCount + # The crease angle threshold. + self.creaseAngleThreshold = creaseAngleThreshold + # Use extrinsic smoothness. + self.extrinsicSmoothness = extrinsicSmoothness + # Align mesh to boundaries. + self.alignToBoundaries = alignToBoundaries + # The number of smoothing iterations. + self.smoothIterations = smoothIterations + # The number of KNN points (point cloud input only). + self.knnPoints = knnPoints + # Use deterministic (repeatable) remeshing. + self.deterministic = deterministic + + class Poisson: + def __init__(self, voxelSize: float = None, depth: int = None, scale: float = None, linearInterpolation: bool = None): + # Voxel size. + self.voxelSize = voxelSize + # Depth. + self.depth = depth + # Scale. + self.scale = scale + # Linear Interpolation. + self.linearInterpolation = linearInterpolation + + def __init__(self, method: 'Method' = None, quality: MF_V3_Settings_Quality_Quality = None, flow: 'Flow' = None, poisson: 'Poisson' = None, voxelSize: float = None, depth: int = None, scale: float = None, linearInterpolation: bool = None): + # Remesh method. + self.method = method + # Remesh quality. + self.quality = quality + # Flow remesh options (Ignored if method is 'Poison'). + self.flow = flow + # Poisson remesh options (Ignored if method is not 'Poisson'). + self.poisson = poisson + """ Temporary for backwards compatibility + Voxel size.""" + self.voxelSize = voxelSize + # Depth. + self.depth = depth + # Scale. + self.scale = scale + # Linear Interpolation. + self.linearInterpolation = linearInterpolation + + class Simplify: + # Simplify settings. + def __init__(self, triangleCount: int): + # Target triangle count. + self.triangleCount = triangleCount + + def __init__(self, selection: MF_V3_Settings_ScanSelection_ScanSelection = None, remesh: 'Remesh' = None, simplify: 'Simplify' = None, texturize: bool = None): + # The scan selection. + self.selection = selection + # Remesh settings. + self.remesh = remesh + # Simplify settings. + self.simplify = simplify + # Apply textures to the merged mesh. + self.texturize = texturize + + diff --git a/three/MF/V3/Settings/NewGroup.py b/three/MF/V3/Settings/NewGroup.py new file mode 100644 index 0000000..f466965 --- /dev/null +++ b/three/MF/V3/Settings/NewGroup.py @@ -0,0 +1,32 @@ +from typing import List + + +class NewGroup: + # Scan group settings. + def __init__(self, parentIndex: int = None, baseName: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None): + """ + The index of the parent group in which the new group is created. + If unspecified the new group is added to the root of the group tree. + """ + self.parentIndex = parentIndex + """ + Group base name. + The new group name will start with the base name followed by a unique index number chosen by the backend. + """ + self.baseName = baseName + # Group color. + self.color = color + # Group visibility. + self.visible = visible + # Collapsed state in the group tree. + self.collapsed = collapsed + """ + Group axis-angle rotation vector. + The direction of the vector is the rotation axis. + The magnitude of the vector is rotation angle in radians. + """ + self.rotation = rotation + # Group translation vector. + self.translation = translation + + diff --git a/three/MF/V3/Settings/Project.py b/three/MF/V3/Settings/Project.py new file mode 100644 index 0000000..9d5f56f --- /dev/null +++ b/three/MF/V3/Settings/Project.py @@ -0,0 +1,10 @@ +class Project: + # Project settings. + def __init__(self, index: int = None, name: str = None): + """ The index identifying which project the settings applies to. + If undefined the current open project is used.""" + self.index = index + # Project name. + self.name = name + + diff --git a/three/MF/V3/Settings/Projector.py b/three/MF/V3/Settings/Projector.py new file mode 100644 index 0000000..1740927 --- /dev/null +++ b/three/MF/V3/Settings/Projector.py @@ -0,0 +1,58 @@ +from MF.V3.Settings.Rectangle import Rectangle as MF_V3_Settings_Rectangle_Rectangle +from MF.V3.Settings.Video import Video as MF_V3_Settings_Video_Video +from enum import Enum +from typing import List + + +class Projector: + # Projector settings. + # Pattern orientation. + class Orientation(Enum): + Horizontal = "Horizontal" # Horizontal pattern. Image columns are identical. + Vertical = "Vertical" # Vertical pattern. Image rows are identical. + + class Pattern: + # Structured light pattern. + def __init__(self, orientation: 'Projector.Orientation', frequency: int, phase: int): + # Pattern orientation. + self.orientation = orientation + # Pattern frequency index. [0 - 8] + self.frequency = frequency + # Pattern phase. [0 - 2] + self.phase = phase + + class Image: + # Projector image settings + class Source: + # Image source. + def __init__(self, format: MF_V3_Settings_Video_Video.Format, width: int, height: int, step: int, fixAspectRatio: bool): + # Source image format + self.format = format + # Source image width. + self.width = width + # Source image height. + self.height = height + # Source image step in bytes. + self.step = step + # Fix the source aspect ratio to the target rectangle. + self.fixAspectRatio = fixAspectRatio + + def __init__(self, source: 'Source', target: MF_V3_Settings_Rectangle_Rectangle): + # Image source. + self.source = source + # Image target rectangle. + self.target = target + + def __init__(self, on: bool = None, brightness: float = None, pattern: 'Pattern' = None, image: 'Image' = None, color: List[float] = None): + # Projector on/off. + self.on = on + # Projector brightness. + self.brightness = brightness + # Structured light pattern. + self.pattern = pattern + # Image to project + self.image = image + # Solid color + self.color = color + + diff --git a/three/MF/V3/Settings/Quality.py b/three/MF/V3/Settings/Quality.py new file mode 100644 index 0000000..a1d5011 --- /dev/null +++ b/three/MF/V3/Settings/Quality.py @@ -0,0 +1,10 @@ +from enum import Enum + + +# Quality settings. +class Quality(Enum): + Low = "Low" # Low quality. + Medium = "Medium" # Medium quality. + High = "High" # High quality. + + diff --git a/three/MF/V3/Settings/Rectangle.py b/three/MF/V3/Settings/Rectangle.py new file mode 100644 index 0000000..8390e02 --- /dev/null +++ b/three/MF/V3/Settings/Rectangle.py @@ -0,0 +1,13 @@ +class Rectangle: + # Rectangle settings. + def __init__(self, x: int, y: int, width: int, height: int): + # Rectangle x offset. + self.x = x + # Rectangle y offset. + self.y = y + # Rectangle width. + self.width = width + # Rectangle height. + self.height = height + + diff --git a/three/MF/V3/Settings/Remesh.py b/three/MF/V3/Settings/Remesh.py new file mode 100644 index 0000000..832d760 --- /dev/null +++ b/three/MF/V3/Settings/Remesh.py @@ -0,0 +1,36 @@ +from enum import Enum + + +class Remesh: + # Field aligned remesh settings. + # Types of remesh output. + class Type(Enum): + triangle = "triangle" # Triangle mesh output. + quad = "quad" # Quad mesh output. + quadDominant = "quadDominant" # Quad-dominant mesh output. + + def __init__(self, scan: int, type: 'Type' = None, scale: float = None, faceCount: int = None, vertexCount: int = None, creaseAngleThreshold: float = None, extrinsicSmoothness: bool = None, alignToBoundaries: bool = None, smoothIterations: int = None, knnPoints: int = None, deterministic: bool = None): + # The scan index. + self.scan = scan + # The type of output mesh. + self.type = type + # Scale + self.scale = scale + # The approximate number of remeshed faces. + self.faceCount = faceCount + # The approximate number of remeshed vertices. + self.vertexCount = vertexCount + # The crease angle threshold. + self.creaseAngleThreshold = creaseAngleThreshold + # Use extrinsic smoothness. + self.extrinsicSmoothness = extrinsicSmoothness + # Align mesh to boundaries. + self.alignToBoundaries = alignToBoundaries + # The number of smoothing iterations. + self.smoothIterations = smoothIterations + # The number of KNN points (point cloud input only). + self.knnPoints = knnPoints + # Use deterministic (repeatable) remeshing. + self.deterministic = deterministic + + diff --git a/three/MF/V3/Settings/RemoveVertices.py b/three/MF/V3/Settings/RemoveVertices.py new file mode 100644 index 0000000..1e133de --- /dev/null +++ b/three/MF/V3/Settings/RemoveVertices.py @@ -0,0 +1,10 @@ +from typing import List + + +class RemoveVertices: + # Remove vertices. + def __init__(self, scans: List[int] = None): + # The scans indexes for which vertices are removed. + self.scans = scans + + diff --git a/three/MF/V3/Settings/Scan.py b/three/MF/V3/Settings/Scan.py new file mode 100644 index 0000000..7067b3b --- /dev/null +++ b/three/MF/V3/Settings/Scan.py @@ -0,0 +1,157 @@ +from MF.V3.Settings.Camera import Camera as MF_V3_Settings_Camera_Camera +from MF.V3.Settings.Capture import Capture as MF_V3_Settings_Capture_Capture +from MF.V3.Settings.Projector import Projector as MF_V3_Settings_Projector_Projector +from MF.V3.Settings.Turntable import Turntable as MF_V3_Settings_Turntable_Turntable +from enum import Enum +from typing import List + + +class Scan: + # Scan settings. + class Processing: + # Scan processing settings. + class PhaseEdgeDetection: + """ + Phase edge detection settings. + + Phase edge detection produces a binary mask indicating the edges of a horizontal/vertical pair of phase images. Since flat geometries give a constant phase image gradient, we use the second derivative (Laplacian) of the phase image to detect edges rather than the gradient. + + The edge mask generated by phase edge detection is an input to both phase filtering and adaptive sampling. If neither of these are enabled, the phase edge detection settings have no effect on the output point cloud. + """ + def __init__(self, threshold: float, laplacianKernelRadius: int, gaussianBlurRadius: int, gaussianBlurStdDev: float, maximumWidthForProcessing: int): + # The edge detection threshold. + self.threshold = threshold + # The Laplacian kernel radius. This must be in the range [1..5]. + self.laplacianKernelRadius = laplacianKernelRadius + """ + Gaussian blur kernel radius. (Optional) To disable, set to 0. + The phase images can optionally blurred before taking the Laplacian to reduce noise. + However as a result, the detected edges are wider. + """ + self.gaussianBlurRadius = gaussianBlurRadius + """ + Gaussian blur kernel standard deviation. + This parameter is ignored if `gaussianBlurSize` is zero. + """ + self.gaussianBlurStdDev = gaussianBlurStdDev + """ + The maximum image width for processing. (Optional) To disable, set to 0. + If this value is greater than zero, the phase images are resized to the maximum width prior + to computing the Laplacian and the the detected edges are then upsampled to the original size. + This would be done to speed up processing or to detect edges on a larger scale. + """ + self.maximumWidthForProcessing = maximumWidthForProcessing + + class PhaseFilter: + # Phase filter settings. + def __init__(self, kernelRadius: int, spatialWeightStdDev: float): + """ + The filter kernel radius. + A neighboring value must be within this radius to be included in the filter. + If the kernel radius is set to zero, the phase filtering is disabled. + """ + self.kernelRadius = kernelRadius + """ + The standard deviation of the spatial weights. + The weight of a neighboring value is $\exp(-(r/s)^2)$ where $r$ is the distance + to the central value and $s$ is the spatial weight standard deviation. + If the spatial weight standard deviation is set to zero, all the spatial + weights are uniformly set to 1. + """ + self.spatialWeightStdDev = spatialWeightStdDev + + class AdaptiveSampling: + """ + Adaptive sampling settings + + Adaptive sampling will downsample points in regions of low detail + and keep points in regions of high detail. + """ + class Type(Enum): + NONE = "NONE" # Do not use adaptive sampling. + REGULAR = "REGULAR" # Use a regular sampling mask in regions of low detail. + RANDOM = "RANDOM" # Use a random sampling mask in regions of low detail. + + def __init__(self, type: 'Type', rate: float): + # Sampling type. + self.type = type + # The sample rate [0..1] for the regions of low detail. + self.rate = rate + + class PointClipping: + # Point clipping settings. + # Point clipping type. + class Type(Enum): + OutsideCube = "OutsideCube" # Clip points outside a unit cube. + OutsideCylinder = "OutsideCylinder" # Clip points outside a unit cylinder. + OutsideSphere = "OutsideSphere" # Clip points outside a unit sphere. + InsideCube = "InsideCube" # Clip points inside a unit cube. + InsideCylinder = "InsideCylinder" # Clip points inside a unit cylinder. + InsideSphere = "InsideSphere" # Clip points inside a unit sphere. + + def __init__(self, type: 'Type', transform: List[float] = None): + # Point clipping type. + self.type = type + # 4x4 transform mapping 3D points to the canonical point clipping coordinates. + self.transform = transform + + class NormalEstimation: + # Normal estimation settings. + class Method(Enum): + NORMAL_LLS = "NORMAL_LLS" # Linear least squares method + NORMAL_OPEN3D = "NORMAL_OPEN3D" # Open3D method using KD tree search for nearest neighbors + + def __init__(self, method: 'Method', maximumNeighbourCount: int, maximumNeighbourRadius: float, useMaximumNeighbourCount: bool, useMaximumNeighbourRadius: bool): + # Normal estimation method. + self.method = method + """ + Maximum number of nearest neighbors used to compute the normal. + This value is only used with the NORMAL_OPEN3D method. + """ + self.maximumNeighbourCount = maximumNeighbourCount + # Maximum radius for a point to be considered a neighbour. + self.maximumNeighbourRadius = maximumNeighbourRadius + # Use the maximum neighbour count. + self.useMaximumNeighbourCount = useMaximumNeighbourCount + # Use the maximum neighbour radius. + self.useMaximumNeighbourRadius = useMaximumNeighbourRadius + + class OutlierRemoval: + # Outlier removal settings. + def __init__(self, neighbourCount: int, neighbourRadius: float): + # The minimum number of points within the radius for a point to be retained. + self.neighbourCount = neighbourCount + # The neighbour search radius. + self.neighbourRadius = neighbourRadius + + def __init__(self, projectorSampleRate: float = None, imageSampleRate: float = None, edgeDetection: 'PhaseEdgeDetection' = None, phaseFilter: 'PhaseFilter' = None, adaptiveSampling: 'AdaptiveSampling' = None, pointClipping: List['PointClipping'] = None, normalEstimation: 'NormalEstimation' = None, outlierRemoval: 'OutlierRemoval' = None): + # Projector sample rate. + self.projectorSampleRate = projectorSampleRate + # Image sample rate. + self.imageSampleRate = imageSampleRate + # Phase edge detection settings. + self.edgeDetection = edgeDetection + # Phase filter settings. + self.phaseFilter = phaseFilter + # Adaptive sampling settings. + self.adaptiveSampling = adaptiveSampling + # Point clipping settings. + self.pointClipping = pointClipping + # Normal estimation settings. + self.normalEstimation = normalEstimation + # Outlier removal settings. + self.outlierRemoval = outlierRemoval + + def __init__(self, camera: MF_V3_Settings_Camera_Camera = None, projector: MF_V3_Settings_Projector_Projector = None, turntable: MF_V3_Settings_Turntable_Turntable = None, capture: MF_V3_Settings_Capture_Capture = None, processing: 'Processing' = None): + # Camera settings. + self.camera = camera + # Projector settings. + self.projector = projector + # Turntable settings. + self.turntable = turntable + # Capture settings. + self.capture = capture + # Processing settings. + self.processing = processing + + diff --git a/three/MF/V3/Settings/ScanData.py b/three/MF/V3/Settings/ScanData.py new file mode 100644 index 0000000..5bc5314 --- /dev/null +++ b/three/MF/V3/Settings/ScanData.py @@ -0,0 +1,40 @@ +from enum import Enum +from typing import List + + +class ScanData: + # Scan data request. + # Scan buffer type. + class Buffer(Enum): + Position = "Position" # Vertex position. + Normal = "Normal" # Vertex normal. + Color = "Color" # Vertex color. + UV = "UV" # Vertex UVs + Triangle = "Triangle" # Triangle index. + Texture = "Texture" # Texture. + All = "All" # All buffer types. + + # Scan metadata type. + class Metadata(Enum): + Mean = "Mean" # The mean (centroid) of the vertex positions. + StdDev = "StdDev" # The standard deviation of the vertex positions. + AxisAlignedBoundingBox = "AxisAlignedBoundingBox" # The axis-aligned bounding box of the vertex positions. + + # The merge processing step. + class MergeStep(Enum): + Combined = "Combined" # The scan meshes are simply combined into a single mesh. + Remeshed = "Remeshed" # The combined mesh is remeshed to give a single geometric surface. + Simplified = "Simplified" # The combined or remeshed mesh is simplified to a reduced number of triangles. + Textured = "Textured" # The merged mesh has been textured. + + def __init__(self, index: int, mergeStep: 'MergeStep' = None, buffers: List['Buffer'] = None, metadata: List['Metadata'] = None): + # Requested index of the scan in the current open project. + self.index = index + # The merge process step if requesting merge data. + self.mergeStep = mergeStep + # Requested scan buffers. + self.buffers = buffers + # Requested scan metadata. + self.metadata = metadata + + diff --git a/three/MF/V3/Settings/ScanSelection.py b/three/MF/V3/Settings/ScanSelection.py new file mode 100644 index 0000000..8c5977d --- /dev/null +++ b/three/MF/V3/Settings/ScanSelection.py @@ -0,0 +1,22 @@ +from enum import Enum +from typing import List + + +class ScanSelection: + # Scan selection. + # Scan selection mode. + class Mode(Enum): + selected = "selected" # Select user-selected groups. + visible = "visible" # Select visible scans. + all = "all" # Select all scans. + + def __init__(self, mode: 'Mode', groups: List[int] = None): + # The scan selection mode. + self.mode = mode + """ + The set of user-selected groups. + These are only used if the selection mode is 'selected'. + """ + self.groups = groups + + diff --git a/three/MF/V3/Settings/Scanner.py b/three/MF/V3/Settings/Scanner.py new file mode 100644 index 0000000..402d3fe --- /dev/null +++ b/three/MF/V3/Settings/Scanner.py @@ -0,0 +1,37 @@ +from MF.V3.Settings.Advanced import Advanced as MF_V3_Settings_Advanced_Advanced +from MF.V3.Settings.Camera import Camera as MF_V3_Settings_Camera_Camera +from MF.V3.Settings.Capture import Capture as MF_V3_Settings_Capture_Capture +from MF.V3.Settings.I18n import I18n as MF_V3_Settings_I18n_I18n +from MF.V3.Settings.Projector import Projector as MF_V3_Settings_Projector_Projector +from MF.V3.Settings.Software import Software as MF_V3_Settings_Software_Software +from MF.V3.Settings.Style import Style as MF_V3_Settings_Style_Style +from MF.V3.Settings.Turntable import Turntable as MF_V3_Settings_Turntable_Turntable +from MF.V3.Settings.Tutorials import Tutorials as MF_V3_Settings_Tutorials_Tutorials +from MF.V3.Settings.Viewer import Viewer as MF_V3_Settings_Viewer_Viewer + + +class Scanner: + # Scanner settings. + def __init__(self, advanced: MF_V3_Settings_Advanced_Advanced = None, camera: MF_V3_Settings_Camera_Camera = None, capture: MF_V3_Settings_Capture_Capture = None, i18n: MF_V3_Settings_I18n_I18n = None, projector: MF_V3_Settings_Projector_Projector = None, style: MF_V3_Settings_Style_Style = None, turntable: MF_V3_Settings_Turntable_Turntable = None, tutorials: MF_V3_Settings_Tutorials_Tutorials = None, viewer: MF_V3_Settings_Viewer_Viewer = None, software: MF_V3_Settings_Software_Software = None): + # Advanced settings. + self.advanced = advanced + # Camera settings. + self.camera = camera + # Capture settings. + self.capture = capture + # I18n settings. + self.i18n = i18n + # Projector settings. + self.projector = projector + # Style settings. + self.style = style + # Turntable settings. + self.turntable = turntable + # Tutorials settings. + self.tutorials = tutorials + # Viewer settings. + self.viewer = viewer + # Software update settings. + self.software = software + + diff --git a/three/MF/V3/Settings/Software.py b/three/MF/V3/Settings/Software.py new file mode 100644 index 0000000..1ebb23c --- /dev/null +++ b/three/MF/V3/Settings/Software.py @@ -0,0 +1,9 @@ +class Software: + # Software settings. + def __init__(self, updateMajor: bool = None, updateNightly: bool = None): + # Enable major version updates which can have breaking API changes. + self.updateMajor = updateMajor + # Enable nightly release candidate updates. + self.updateNightly = updateNightly + + diff --git a/three/MF/V3/Settings/Style.py b/three/MF/V3/Settings/Style.py new file mode 100644 index 0000000..1cfdfc3 --- /dev/null +++ b/three/MF/V3/Settings/Style.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class Style: + # Style settings. + # Themes. + class Theme(Enum): + Light = "Light" # Light mode. + Dark = "Dark" # Dark mode. + + def __init__(self, theme: 'Theme' = None): + # Theme setting. + self.theme = theme + + diff --git a/three/MF/V3/Settings/Turntable.py b/three/MF/V3/Settings/Turntable.py new file mode 100644 index 0000000..b3b3063 --- /dev/null +++ b/three/MF/V3/Settings/Turntable.py @@ -0,0 +1,11 @@ +class Turntable: + # Turntable settings. + def __init__(self, scans: int, sweep: int, use: bool = None): + # The number of turntable scans. + self.scans = scans + # Turntable angle sweep in degrees. + self.sweep = sweep + # Use the turntable. + self.use = use + + diff --git a/three/MF/V3/Settings/Tutorials.py b/three/MF/V3/Settings/Tutorials.py new file mode 100644 index 0000000..569d51a --- /dev/null +++ b/three/MF/V3/Settings/Tutorials.py @@ -0,0 +1,18 @@ +from typing import List + + +class Tutorials: + # Tutorials settings. + class Viewed: + # Viewed tutorials. + def __init__(self, pages: List[str] = None): + # Viewed tutorials pages. + self.pages = pages + + def __init__(self, show: bool = None, viewed: 'Viewed' = None): + # Show tutorials. + self.show = show + # Viewed tutorials. + self.viewed = viewed + + diff --git a/three/MF/V3/Settings/Video.py b/three/MF/V3/Settings/Video.py new file mode 100644 index 0000000..8a68ce2 --- /dev/null +++ b/three/MF/V3/Settings/Video.py @@ -0,0 +1,31 @@ +from enum import Enum + + +class Video: + # Video settings. + # Video codecs. + class Codec(Enum): + NoCodec = "NoCodec" # No codec specified. + RAW = "RAW" # Raw encoding. + JPEG = "JPEG" # JPEG encoding. + H264 = "H264" # H264 encoding. + + # Pixel formats. + class Format(Enum): + NoFormat = "NoFormat" # No format specified. + RGB565 = "RGB565" # RGB565 16-bit + RGB888 = "RGB888" # RGB888 24-bit. + BGR888 = "BGR888" # BGR888 24-bit. + YUV420 = "YUV420" # YUV 420 planar. + + def __init__(self, codec: 'Codec', format: 'Format', width: int, height: int): + # Video codec. + self.codec = codec + # Pixel format. + self.format = format + # Image width. + self.width = width + # Image height. + self.height = height + + diff --git a/three/MF/V3/Settings/Viewer.py b/three/MF/V3/Settings/Viewer.py new file mode 100644 index 0000000..f797ad1 --- /dev/null +++ b/three/MF/V3/Settings/Viewer.py @@ -0,0 +1,7 @@ +class Viewer: + # 3D Viewer settings. + def __init__(self, textureOpacity: float = None): + # Texture opacity. + self.textureOpacity = textureOpacity + + diff --git a/three/MF/V3/Settings/Wifi.py b/three/MF/V3/Settings/Wifi.py new file mode 100644 index 0000000..bc7b529 --- /dev/null +++ b/three/MF/V3/Settings/Wifi.py @@ -0,0 +1,9 @@ +class Wifi: + # Wifi connection settings. + def __init__(self, ssid: str, password: str): + # The wifi ssid. + self.ssid = ssid + # The wifi password. + self.password = password + + diff --git a/three/MF/V3/Settings/__init__.py b/three/MF/V3/Settings/__init__.py new file mode 100644 index 0000000..9fadccd --- /dev/null +++ b/three/MF/V3/Settings/__init__.py @@ -0,0 +1,30 @@ +from MF.V3.Settings.Advanced import * +from MF.V3.Settings.Align import * +from MF.V3.Settings.AutoFocus import * +from MF.V3.Settings.BoundingBox import * +from MF.V3.Settings.Camera import * +from MF.V3.Settings.Capture import * +from MF.V3.Settings.CopyGroup import * +from MF.V3.Settings.CopyProject import * +from MF.V3.Settings.Export import * +from MF.V3.Settings.Group import * +from MF.V3.Settings.I18n import * +from MF.V3.Settings.Merge import * +from MF.V3.Settings.NewGroup import * +from MF.V3.Settings.Project import * +from MF.V3.Settings.Projector import * +from MF.V3.Settings.Quality import * +from MF.V3.Settings.Rectangle import * +from MF.V3.Settings.Remesh import * +from MF.V3.Settings.RemoveVertices import * +from MF.V3.Settings.Scan import * +from MF.V3.Settings.ScanData import * +from MF.V3.Settings.ScanSelection import * +from MF.V3.Settings.Scanner import * +from MF.V3.Settings.Software import * +from MF.V3.Settings.Style import * +from MF.V3.Settings.Turntable import * +from MF.V3.Settings.Tutorials import * +from MF.V3.Settings.Video import * +from MF.V3.Settings.Viewer import * +from MF.V3.Settings.Wifi import * diff --git a/three/MF/V3/Task.py b/three/MF/V3/Task.py new file mode 100644 index 0000000..b2dbb98 --- /dev/null +++ b/three/MF/V3/Task.py @@ -0,0 +1,89 @@ +from enum import Enum +from google.protobuf import any_pb2 as _any_pb2 + + +class TaskState(Enum): + Empty = "None" # The task state is not defined. + Sent = "Sent" # The task has been sent by the client. + Received = "Received" # The task has been received by the server. + Started = "Started" # The task started by the server. + Completed = "Completed" # The task is completed by the server. + Cancelled = "Cancelled" # The task has been cancelled by the client. + Failed = "Failed" # The task has failed. A string describing the error is returned with the task. + Dropped = "Dropped" # The task has not been received by the server, or task IDs were sent out of sequence. + Disconnected = "Disconnected" # The client has been disconnected from the server before the task could finish. + + +class Task: + """* + Generic task message for the Three Scanner. + + The task message is the generic message used for requesting a task of the Three Scanner and receiving updates and results. + + Example: Apply camera settings with the "SetCameras" task. + + > Example request: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetCameras" + "Input":{ + "analogGain":256, + "digitalGain":128, + "exposure":18000 + }, + } + } + ``` + + > Example response: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetCameras" + "Input":{ + "analogGain":256, + "digitalGain":512, + "exposure":18000 + }, + "Output":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":512}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "State":"Completed" + } + } + ``` + """ + class Progress: + # V3 Task Progress Descriptor + def __init__(self, current: int, step: str, total: int): + # The current step of the scan. + self.current = current + # The string description of the current step. + self.step = step + # The total steps in the progress. + self.total = total + + def __init__(self, Index: int, Type: str, Input: _any_pb2 = None, Output: _any_pb2 = None, State: 'TaskState' = None, Error: str = None, Progress: _any_pb2 = None): + # A unique identifier generated by the client. This identifier associates all incoming and outgoing task messages with a specific task requested by the client. + self.Index = Index + # The string identifying the task type. See task definitions for the list of valid task strings. + self.Type = Type + # Optional input message. See each task definition for details. + self.Input = Input + # Optional output message. See each task definition for details. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + # A Progress object for tasks that give updates on long running tasks + self.Progress = Progress + + diff --git a/three/MF/V3/Tasks/AddMergeToProject.py b/three/MF/V3/Tasks/AddMergeToProject.py new file mode 100644 index 0000000..45d53ff --- /dev/null +++ b/three/MF/V3/Tasks/AddMergeToProject.py @@ -0,0 +1,66 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class AddMergeToProject: + """* + Add a merged scan to the current project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"AddMergeToProject" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"AddMergeToProject", + "Output":{ + "groups":[ + { + "index":1, + "name":"Scan-1", + "scan": 1, + "color":[0.5, 0.8, 0.3] + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `AddMergeToProject` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "AddMergeToProject" + self.Type = Type + + class Response: + # Server response for the `AddMergeToProject` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "AddMergeToProject" + self.Type = Type + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/Align.py b/three/MF/V3/Tasks/Align.py new file mode 100644 index 0000000..ba8fe37 --- /dev/null +++ b/three/MF/V3/Tasks/Align.py @@ -0,0 +1,77 @@ +from MF.V3.Descriptors.Transform import Transform as MF_V3_Descriptors_Transform_Transform +from MF.V3.Settings.Align import Align as MF_V3_Settings_Align_Align +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class Align: + """* + Align two scan groups. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Align", + "Input":{ + "source":1, + "target":2, + "rough":{"method": "FastGlobal"}, + "fine":{"method": "ICP"} + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Align", + "Input":{ + "source":1, + "target":2, + "rough":{"method": "FastGlobal"}, + "fine":{"method": "ICP"} + }, + "Output":{ + "rotation":[0.2, 0.4, 0.6], + "translation":[11, -10, 24] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `Align` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Align_Align): + # A unique identifier generated by the client. + self.Index = Index + # "Align" + self.Type = Type + # The align settings. + self.Input = Input + + class Response: + # Server response for the `Align` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Align_Align, Output: MF_V3_Descriptors_Transform_Transform, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "Align" + self.Type = Type + # The requested align settings. + self.Input = Input + # The transform that aligns the source scan group to the target scan group. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/AutoFocus.py b/three/MF/V3/Tasks/AutoFocus.py new file mode 100644 index 0000000..d21722d --- /dev/null +++ b/three/MF/V3/Tasks/AutoFocus.py @@ -0,0 +1,94 @@ +from MF.V3.Descriptors.Settings.Camera import Camera as MF_V3_Descriptors_Settings_Camera_Camera +from MF.V3.Settings.AutoFocus import AutoFocus as MF_V3_Settings_AutoFocus_AutoFocus +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class AutoFocus: + """* + Auto focus one or both cameras. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"AutoFocus", + "Input":{ + "cameras":[{ + "index":1, + "box":{"x":196,"y":130,"width":64,"height":64} + }], + "applyAll":false + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"AutoFocus", + "Input":{ + "cameras":[{ + "index":1, + "box":{"x":196,"y":130,"width":64,"height":64} + }], + "applyAll":false + } + "Output":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "autoExposure":{"default":false,"value":true}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":320}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + "focus":{ + "box":{ + "default":[ + {"height":64,"width":64,"x":224,"y":158}, + {"height":64,"width":64,"x":224,"y":158} + ], + "value":[ + {"height":64,"width":64,"x":271,"y":134}, + {"height":64,"width":64,"x":196,"y":130} + ] + }, + "value":{"default":[350,350],"max":1024,"min":0,"value":[396,392]} + } + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `AutoFocus` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_AutoFocus_AutoFocus = None): + # A unique identifier generated by the client. + self.Index = Index + # "AutoFocus" + self.Type = Type + # AutoFocus settings. + self.Input = Input + + class Response: + # Server response for the `AutoFocus` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_AutoFocus_AutoFocus = None, Output: MF_V3_Descriptors_Settings_Camera_Camera = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "AutoFocus" + self.Type = Type + # Requested auto focus settings. + self.Input = Input + # Actual camera settings after auto focusing the camera(s). + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/BoundingBox.py b/three/MF/V3/Tasks/BoundingBox.py new file mode 100644 index 0000000..4a92fd2 --- /dev/null +++ b/three/MF/V3/Tasks/BoundingBox.py @@ -0,0 +1,82 @@ +from MF.V3.Descriptors.BoundingBox import BoundingBox as MF_V3_Descriptors_BoundingBox_BoundingBox +from MF.V3.Settings.BoundingBox import BoundingBox as MF_V3_Settings_BoundingBox_BoundingBox +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class BoundingBox: + """* + Get the bounding box of a set of scan groups. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"BoundingBox", + "Input":{ + "selection":{"mode":"visible"}, + "axisAligned":false + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"BoundingBox", + "Input":{ + "selection":{"mode":"visible"}, + "axisAligned":false + }, + "Output":{ + "center":[11.9,-10.1,94.5], + "rotation":[ + 0.7, -0.7, 0.0, + 0.7, 0.7, 0.0, + 0.0, 0.0, 1.0], + "size":[442.2,253.1,447.1], + "transform":[ + 221, 0.0, 0.0, 11.9, + 0.0, 126, 0.0, -10.1, + 0.0, 0.0, 223, 94.5, + 0.0, 0.0, 0.0, 1.0] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `BoundingBox` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_BoundingBox_BoundingBox): + # A unique identifier generated by the client. + self.Index = Index + # "BoundingBox" + self.Type = Type + # The bounding box settings. + self.Input = Input + + class Response: + # Server response for the `BoundingBox` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_BoundingBox_BoundingBox, Output: MF_V3_Descriptors_BoundingBox_BoundingBox, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "BoundingBox" + self.Type = Type + # The requested bounding box settings. + self.Input = Input + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/CalibrateCameras.py b/three/MF/V3/Tasks/CalibrateCameras.py new file mode 100644 index 0000000..b56dfa7 --- /dev/null +++ b/three/MF/V3/Tasks/CalibrateCameras.py @@ -0,0 +1,58 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class CalibrateCameras: + """* + Calibrate the cameras. + + This task starts the camera calibration capture sequence where the user is guided to place the calibration card with a card outline drawn on the video feed. Once each calibration card pose is captured, the cameras are calibrated and the calibration results are returned as a string. If the cameras cannot be calibrated the task finishes in a `Failed` state. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrateCameras" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrateCameras", + "Output":"Camera calibration results: ...", + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `CalibrateCameras` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "CalibrateCameras" + self.Type = Type + + class Response: + # Server response for the `CalibrateCameras` task. + def __init__(self, Index: int, Type: str, Output: str = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "CalibrateCameras" + self.Type = Type + # Camera calibration results. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/CalibrateTurntable.py b/three/MF/V3/Tasks/CalibrateTurntable.py new file mode 100644 index 0000000..546383c --- /dev/null +++ b/three/MF/V3/Tasks/CalibrateTurntable.py @@ -0,0 +1,61 @@ +from MF.V3.Descriptors.Calibration import Turntable as MF_V3_Descriptors_Calibration_Turntable +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class CalibrateTurntable: + """* + Calibrate the turntable. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrateTurntable" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrateTurntable", + "Output":{ + "date":[2024,4,27,16,57,35], + "quality":"Excellent", + "focus":[300,320] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `CalibrateTurntable` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "CalibrateTurntable" + self.Type = Type + + class Response: + # Server response for the `CalibrateTurntable` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Calibration_Turntable = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "CalibrateTurntable" + self.Type = Type + # The Turntable calibration descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/CalibrationCaptureTargets.py b/three/MF/V3/Tasks/CalibrationCaptureTargets.py new file mode 100644 index 0000000..6505b0f --- /dev/null +++ b/three/MF/V3/Tasks/CalibrationCaptureTargets.py @@ -0,0 +1,66 @@ +from MF.V3.Descriptors.Calibration import CaptureTarget as MF_V3_Descriptors_Calibration_CaptureTarget +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class CalibrationCaptureTargets: + """* + Get the camera calibration targets. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrationCaptureTargets" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CalibrationCaptureTargets", + "Output":{[ + { + "camera":0, + "quads":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] + }, + { + "camera":1, + "quads":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] + }, + ]}, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `CalibrationCaptureTargets` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "CalibrationCaptureTargets" + self.Type = Type + + class Response: + # Server response for the `CalibrationCaptureTargets` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Calibration_CaptureTarget = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "CalibrationCaptureTargets" + self.Type = Type + # The calibration capture target descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/CameraCalibration.py b/three/MF/V3/Tasks/CameraCalibration.py new file mode 100644 index 0000000..cabf59a --- /dev/null +++ b/three/MF/V3/Tasks/CameraCalibration.py @@ -0,0 +1,60 @@ +from MF.V3.Descriptors.Calibration import Camera as MF_V3_Descriptors_Calibration_Camera +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class CameraCalibration: + """* + Get the camera calibration descriptor. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CameraCalibration" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CameraCalibration", + "Output":{ + "date":[2024,4,27,16,57,35], + "quality":"Excellent" + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `CameraCalibration` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "CameraCalibration" + self.Type = Type + + class Response: + # Server response for the `CameraCalibration` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Calibration_Camera = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "CameraCalibration" + self.Type = Type + # The camera calibration descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/CloseProject.py b/three/MF/V3/Tasks/CloseProject.py new file mode 100644 index 0000000..9f17ced --- /dev/null +++ b/three/MF/V3/Tasks/CloseProject.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class CloseProject: + """* + Close the current open project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CloseProject", + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"CloseProject", + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `CloseProject` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "CloseProject" + self.Type = Type + + class Response: + # Server response for the `CloseProject` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "CloseProject" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ConnectWifi.py b/three/MF/V3/Tasks/ConnectWifi.py new file mode 100644 index 0000000..911ef46 --- /dev/null +++ b/three/MF/V3/Tasks/ConnectWifi.py @@ -0,0 +1,67 @@ +from MF.V3.Settings.Wifi import Wifi as MF_V3_Settings_Wifi_Wifi +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ConnectWifi: + """* + Connect to a wifi network. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ConnectWifi", + "Input":{ + "ssid":"Network1" + "password":"password" + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ConnectWifi", + "Input":{ + { + "ssid":"Network1" + "password":"password" + } + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ConnectWifi` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Wifi_Wifi): + # A unique identifier generated by the client. + self.Index = Index + # "ConnectWifi" + self.Type = Type + # Wifi settings. + self.Input = Input + + class Response: + # Server response for the `ConnectWifi` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Wifi_Wifi, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ConnectWifi" + self.Type = Type + # The requested wifi settings. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/DepthMap.py b/three/MF/V3/Tasks/DepthMap.py new file mode 100644 index 0000000..4dfcff3 --- /dev/null +++ b/three/MF/V3/Tasks/DepthMap.py @@ -0,0 +1,141 @@ +from MF.V3.Descriptors.Image import Image as MF_V3_Descriptors_Image_Image +from MF.V3.Settings.Scan import Scan as MF_V3_Settings_Scan_Scan +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task +from typing import List + + +class DepthMap: + """* + Capture a new scan. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"DepthMap" + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + }, + } + } + ``` + + > Depth map buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":13128960, + "Descriptor":{ + "cols":2104, + "rows":1560, + "step":8416, + "type":5 + }, + "Task":{ + "Index":1, + "Type":"DepthMap", + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + } + } + } + } + ``` + + > Depth map binary data transfer from server [13128960 bytes]. + + > Texture buffer message from server. + + ```json + { + "Buffer":{ + "Index":1, + "Size":9846720, + "Descriptor":{ + "cols":2104, + "rows":1560, + "step":6312, + "type":16 + }, + "Task":{ + "Index":1, + "Type":"DepthMap", + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + } + } + } + } + ``` + + > Texture binary data transfer from server [9846720 bytes]. + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"DepthMap" + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + }, + "Output":[2500,0,1052,0,2500,780,0,0,1], + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `DepthMap` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scan_Scan = None): + # A unique identifier generated by the client. + self.Index = Index + # "DepthMap" + self.Type = Type + # Scan settings. + self.Input = Input + + class Response: + # Server response for the `DepthMap` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scan_Scan = None, Output: List[float] = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "DepthMap" + self.Type = Type + # Requested scan settings. + self.Input = Input + # The 9 values of the camera matrix corresponding to the depth map (row-major). + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `DepthMap` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task, Descriptor: MF_V3_Descriptors_Image_Image): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested DepthMap task. + self.Task = Task + # The image descriptor. + self.Descriptor = Descriptor + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/DetectCalibrationCard.py b/three/MF/V3/Tasks/DetectCalibrationCard.py new file mode 100644 index 0000000..1127f4f --- /dev/null +++ b/three/MF/V3/Tasks/DetectCalibrationCard.py @@ -0,0 +1,65 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class DetectCalibrationCard: + """* + Detect the calibration card on one or both cameras. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"DetectCalibrationCard", + "Input":3 + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Input":3, + "Type":"DetectCalibrationCard", + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `DetectCalibrationCard` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "DetectCalibrationCard" + self.Type = Type + """ + Flag specifying on which camera(s) to detect the calibration card. + [0: neither camera (disable), 1: left camera, 2: right camera, 3: both cameras] + """ + self.Input = Input + + class Response: + # Server response for the `DetectCalibrationCard` task. + def __init__(self, Index: int, Type: str, Input: int, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "DetectCalibrationCard" + self.Type = Type + """ + Flag sent in the request specifying on which camera(s) to detect the calibration card. + [0: neither camera (disable), 1: left camera, 2: right camera, 3: both cameras] + """ + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/DownloadProject.py b/three/MF/V3/Tasks/DownloadProject.py new file mode 100644 index 0000000..eb34bd1 --- /dev/null +++ b/three/MF/V3/Tasks/DownloadProject.py @@ -0,0 +1,89 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class DownloadProject: + """* + Download a project from the scanner. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"DownloadProject", + "Input":5 + } + } + ``` + + > Buffer message from server. + + ```json + { + "Buffer":{ + "Descriptor":"Project-5.zip", + "Index":0, + "Size":15682096, + "Task":{ + "Index":1, + "Type":"DownloadProject", + "Input":5 + } + } + } + ``` + + > Binary data transfer from server: The project zip file [15682096 bytes]. + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"DownloadProject" + "Input":5, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `DownloadProject` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "DownloadProject" + self.Type = Type + # Index of the project to download. + self.Input = Input + + class Response: + # Server response for the `DownloadProject` task. + def __init__(self, Index: int, Type: str, Input: int, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "DownloadProject" + self.Type = Type + # Requested index of the project to download. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `DownloadProject` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task, Descriptor: str): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested DownloadProject task. + self.Task = Task + # The downloaded project filename. + self.Descriptor = Descriptor + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/Export.py b/three/MF/V3/Tasks/Export.py new file mode 100644 index 0000000..36209cc --- /dev/null +++ b/three/MF/V3/Tasks/Export.py @@ -0,0 +1,103 @@ +from MF.V3.Settings.Export import Export as MF_V3_Settings_Export_Export +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class Export: + """* + Export a group of scans. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Export", + "Input":{ + "selection":{"mode":"visible"}, + "format":"obj", + "texture":true, + "merge":false + } + } + } + ``` + + > Export file buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":8413737, + "Task":{ + "Index":1, + "Type":"Export", + "Input":{ + "selection":{"mode":"visible"}, + "format":"obj", + "texture":true, + "merge":false + } + } + } + } + ``` + + > Export file binary data transfer from server [8413737 bytes]. + + > Response from server: + + ```json + { + "Task":{ + "Index":1, + "Type":"Export" + "Input":{ + "selection":{"mode":"visible"}, + "format":"obj", + "texture":true, + "merge":false + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `Export` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Export_Export): + # A unique identifier generated by the client. + self.Index = Index + # "Export" + self.Type = Type + # Export settings. + self.Input = Input + + class Response: + # Server response for the `Export` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Export_Export, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "Export" + self.Type = Type + # Requested export settings. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `Export` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested Export task. + self.Task = Task + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ExportLogs.py b/three/MF/V3/Tasks/ExportLogs.py new file mode 100644 index 0000000..bb933f6 --- /dev/null +++ b/three/MF/V3/Tasks/ExportLogs.py @@ -0,0 +1,87 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class ExportLogs: + """* + Export scanner logs. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ExportLogs", + "Input":true + } + } + ``` + + > Export file buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":41337, + "Task":{ + "Index":1, + "Type":"ExportLogs", + "Input":true + } + } + } + ``` + + > Export file binary data transfer from server [41337 bytes]. + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ExportLogs" + "Input":true, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ExportLogs` task. + def __init__(self, Index: int, Type: str, Input: bool = None): + # A unique identifier generated by the client. + self.Index = Index + # "ExportLogs" + self.Type = Type + # Export log images. If unspecified, log images are not exported. + self.Input = Input + + class Response: + # Server response for the `ExportLogs` task. + def __init__(self, Index: int, Type: str, Input: bool = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ExportLogs" + self.Type = Type + # Requested export log images. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `ExportLogs` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested ExportLogs task. + self.Task = Task + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ExportMerge.py b/three/MF/V3/Tasks/ExportMerge.py new file mode 100644 index 0000000..78a1e45 --- /dev/null +++ b/three/MF/V3/Tasks/ExportMerge.py @@ -0,0 +1,97 @@ +from MF.V3.Settings.Export import Export as MF_V3_Settings_Export_Export +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class ExportMerge: + """* + Export a merged scan. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ExportMerge", + "Input":{ + "format":"obj", + "texture":true + } + } + } + ``` + + > Export file buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":8413737, + "Task":{ + "Index":1, + "Type":"ExportMerge", + "Input":{ + "format":"obj", + "texture":true + } + } + } + } + ``` + + > Export file binary data transfer from server [8413737 bytes]. + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ExportMerge" + "Input":{ + "format":"obj", + "texture":true + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ExportMerge` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Export_Export): + # A unique identifier generated by the client. + self.Index = Index + # "ExportMerge" + self.Type = Type + # Export settings. + self.Input = Input + + class Response: + # Server response for the `ExportMerge` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Export_Export, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ExportMerge" + self.Type = Type + # Requested export settings. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `ExportMerge` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested ExportMerge task. + self.Task = Task + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/FlattenGroup.py b/three/MF/V3/Tasks/FlattenGroup.py new file mode 100644 index 0000000..c772c6a --- /dev/null +++ b/three/MF/V3/Tasks/FlattenGroup.py @@ -0,0 +1,72 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class FlattenGroup: + """* + Flatten a scan group such that it only consists of single scans. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"FlattenGroup", + "Input":0 + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"FlattenGroup", + "Input":0, + "Output":{ + "groups":[{ + "index":2, + "name":"Group 2", + "groups":[{ + "index":1, + "name":"Group 1" + }] + }], + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `FlattenGroup` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "FlattenGroup" + self.Type = Type + # The index of the group to flatten. + self.Input = Input + + class Response: + # Server response for the `FlattenGroup` task. + def __init__(self, Index: int, Type: str, Input: int, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "FlattenGroup" + self.Type = Type + # The requested index of the group to flatten. + self.Input = Input + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ForgetWifi.py b/three/MF/V3/Tasks/ForgetWifi.py new file mode 100644 index 0000000..7b192ed --- /dev/null +++ b/three/MF/V3/Tasks/ForgetWifi.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ForgetWifi: + """* + Forget all wifi connections. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ForgetWifi" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ForgetWifi" + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ForgetWifi` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ForgetWifi" + self.Type = Type + + class Response: + # Server response for the `ForgetWifi` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ForgetWifi" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/HasCameras.py b/three/MF/V3/Tasks/HasCameras.py new file mode 100644 index 0000000..0bf9c8d --- /dev/null +++ b/three/MF/V3/Tasks/HasCameras.py @@ -0,0 +1,56 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class HasCameras: + """* + Check if the scanner has working cameras. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasCameras" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasCameras", + "Output":true, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `HasCameras` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "HasCameras" + self.Type = Type + + class Response: + # Server response for the `HasCameras` task. + def __init__(self, Index: int, Type: str, Output: bool = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "HasCameras" + self.Type = Type + # The working state of the cameras. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/HasProjector.py b/three/MF/V3/Tasks/HasProjector.py new file mode 100644 index 0000000..9b40f11 --- /dev/null +++ b/three/MF/V3/Tasks/HasProjector.py @@ -0,0 +1,56 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class HasProjector: + """* + Check if the scanner has a working projector. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasProjector" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasProjector", + "Output":true, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `HasProjector` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "HasProjector" + self.Type = Type + + class Response: + # Server response for the `HasProjector` task. + def __init__(self, Index: int, Type: str, Output: bool = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "HasProjector" + self.Type = Type + # The working state of the projector. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/HasTurntable.py b/three/MF/V3/Tasks/HasTurntable.py new file mode 100644 index 0000000..d56fc1a --- /dev/null +++ b/three/MF/V3/Tasks/HasTurntable.py @@ -0,0 +1,56 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class HasTurntable: + """* + Check if the scanner is connected to a working turntable. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasTurntable" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"HasTurntable", + "Output":true, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `HasTurntable` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "HasTurntable" + self.Type = Type + + class Response: + # Server response for the `HasTurntable` task. + def __init__(self, Index: int, Type: str, Output: bool = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "HasTurntable" + self.Type = Type + # The working start of the connected turntable. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListExportFormats.py b/three/MF/V3/Tasks/ListExportFormats.py new file mode 100644 index 0000000..326b812 --- /dev/null +++ b/three/MF/V3/Tasks/ListExportFormats.py @@ -0,0 +1,86 @@ +from MF.V3.Descriptors.Export import Export as MF_V3_Descriptors_Export_Export +from MF.V3.Task import TaskState as MF_V3_Task_TaskState +from typing import List + + +class ListExportFormats: + """* + List all export formats and the geometry elements they support. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListExportFormats" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListExportFormats", + "Output":{[ + { + "format": "ply", + "colors": true, + "description": "Polygon format", + "extension": ".ply", + "faces": ["Point","Triangle","Quad"], + "normals": true, + "textures": "Single" + }, + { + "format": "obj", + "colors": true, + "description": "Wavefront format", + "extension": ".obj", + "faces": ["Point","Triangle","Quad"], + "normals": true, + "textures": "Multiple" + }, + { + "format": "stl", + "colors": false, + "description": "Stereolithography format", + "extension": ".stl", + "faces": ["Point","Triangle"], + "normals": true, + "textures": "None" + } + ]}, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListExportFormats` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListExportFormats" + self.Type = Type + + class Response: + # Server response for the `ListExportFormats` task. + def __init__(self, Index: int, Type: str, Output: List[MF_V3_Descriptors_Export_Export] = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListExportFormats" + self.Type = Type + # The list of export format descriptors. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListGroups.py b/three/MF/V3/Tasks/ListGroups.py new file mode 100644 index 0000000..10ba504 --- /dev/null +++ b/three/MF/V3/Tasks/ListGroups.py @@ -0,0 +1,85 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ListGroups: + """* + List the scan groups in the current open project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListGroups" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListGroups", + "Output":{ + "groups":[ + { + "index":1, + "scan":1, + "name":"Scan-1", + "color":[0.75,0.5,0.2,1.0], + "rotation":[0.03,0.1,-0.01], + "translation":[-101,67,-561], + "visible":true + }, + { + "index":2, + "scan":2, + "name":"Scan-2", + "color":[0.7,0.9,0.6,1.0], + "rotation":[0.1,0.2,0.5], + "translation":[-90,64,-553], + "visible":true + }, + { + "index":3, + "scan":3, + "name":"Scan-3", + "color":[0.6,0.8,0.9,1.0], + "visible":true + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListGroups` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListGroups" + self.Type = Type + + class Response: + # Server response for the `ListGroups` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListGroups" + self.Type = Type + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListNetworkInterfaces.py b/three/MF/V3/Tasks/ListNetworkInterfaces.py new file mode 100644 index 0000000..763cb9d --- /dev/null +++ b/three/MF/V3/Tasks/ListNetworkInterfaces.py @@ -0,0 +1,61 @@ +from MF.V3.Descriptors.Network import Interface as MF_V3_Descriptors_Network_Interface +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ListNetworkInterfaces: + """* + List network interfaces. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListNetworkInterfaces" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListNetworkInterfaces", + "Output":[ + {"ip":"192.168.1.234","name":"eth0","ssid":""}, + {"ip":"127.0.0.1","name":"lo","ssid":""} + {"ip":"192.168.2.345","name":"wlan0","ssid":"Network1"} + ], + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListNetworkInterfaces` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListNetworkInterfaces" + self.Type = Type + + class Response: + # Server response for the `ListNetworkInterfaces` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Network_Interface = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListNetworkInterfaces" + self.Type = Type + # Network interface descriptors. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListProjects.py b/three/MF/V3/Tasks/ListProjects.py new file mode 100644 index 0000000..6216681 --- /dev/null +++ b/three/MF/V3/Tasks/ListProjects.py @@ -0,0 +1,61 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ListProjects: + """* + List all projects. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListProjects" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListProjects", + "Output":[ + {"index":1,"modified":[2024,5,12,11,23,17],"name":"Project 1","size":35409834}, + {"index":2,"modified":[2024,5,12,11,2,37],"name":"Project 2","size":175025367}, + {"index":3,"modified":[2024,5,6,17,15,53],"name":"Project 3","size":24314083} + ], + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListProjects` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListProjects" + self.Type = Type + + class Response: + # Server response for the `ListProjects` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Brief = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListProjects" + self.Type = Type + # Brief project descriptors. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListScans.py b/three/MF/V3/Tasks/ListScans.py new file mode 100644 index 0000000..8926274 --- /dev/null +++ b/three/MF/V3/Tasks/ListScans.py @@ -0,0 +1,77 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState +from typing import List + + +class ListScans: + """* + List the scans in the current open project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListScans" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListScans", + "Output":{[ + { + "color":[0.8,0.5,0.6,1.0], + "index":1, + "name":"Scan-1", + "scan":1, + "rotation":[0.2,0.8,-0.1], + "translation":[-275,-32,-134], + "visible":true + }, + { + "color":[0.5,0.7,0.2,1.0], + "index":2, + "name":"Scan-2", + "scan":2, + "rotation":[0.7,-0.5,0.3], + "translation":[75,-62,38], + "visible":true + }, + ]}, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListScans` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListScans" + self.Type = Type + + class Response: + # Server response for the `ListScans` task. + def __init__(self, Index: int, Type: str, Output: List[MF_V3_Descriptors_Project_Project.Group] = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListScans" + self.Type = Type + # The list of scans in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListSettings.py b/three/MF/V3/Tasks/ListSettings.py new file mode 100644 index 0000000..238935c --- /dev/null +++ b/three/MF/V3/Tasks/ListSettings.py @@ -0,0 +1,86 @@ +from MF.V3.Descriptors.Settings.Scanner import Scanner as MF_V3_Descriptors_Settings_Scanner_Scanner +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ListSettings: + """* + Get scanner settings. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListSettings" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListSettings", + "Output":{ + "camera":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "autoExposure":{"default":false,"value":false}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":320}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "projector":{ + "brightness":{"default":0.5,"max":1.0,"min":0.0,"value":0.800000011920929}, + "on":{"default":false,"value":true} + }, + "turntable":{ + "scans":{"default":8,"max":24,"min":1,"value":3}, + "sweep":{"default":360,"max":360,"min":5,"value":90}, + "use":{"default":true,"value":true} + }, + "capture":{ + "quality":{"default":"Medium","value":"Medium"}, + "texture":{"default":true,"value":true} + }, + "i18n":{ + "language":{"default":"en","value":"en"} + }, + "style":{ + "theme":{"default":"Dark","value":"Dark"} + }, + "viewer":{ + "textureOpacity":{"default":0.5,"max":1.0,"min":0.0,"value":1.0} + } + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListSettings` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListSettings" + self.Type = Type + + class Response: + # Server response for the `ListSettings` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Settings_Scanner_Scanner = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListSettings" + self.Type = Type + # The scanner settings descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ListWifi.py b/three/MF/V3/Tasks/ListWifi.py new file mode 100644 index 0000000..223010e --- /dev/null +++ b/three/MF/V3/Tasks/ListWifi.py @@ -0,0 +1,64 @@ +from MF.V3.Descriptors.Wifi import Wifi as MF_V3_Descriptors_Wifi_Wifi +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class ListWifi: + """* + List available wifi networks. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListWifi" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ListWifi", + "Output":{ + "networks":[ + {"ssid":"Network1","isActive":true,"isPublic":false,"quality":90}, + {"ssid":"Network2","isActive":true,"isPublic":true,"quality":50}, + {"ssid":"Network3","isActive":true,"isPublic":true,"quality":75} + ], + "ssid":"Network1" + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ListWifi` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "ListWifi" + self.Type = Type + + class Response: + # Server response for the `ListWifi` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Wifi_Wifi = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ListWifi" + self.Type = Type + # The wifi descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/Merge.py b/three/MF/V3/Tasks/Merge.py new file mode 100644 index 0000000..70ab574 --- /dev/null +++ b/three/MF/V3/Tasks/Merge.py @@ -0,0 +1,104 @@ +from MF.V3.Descriptors.Merge import Merge as MF_V3_Descriptors_Merge_Merge +from MF.V3.Settings.Merge import Merge as MF_V3_Settings_Merge_Merge +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class Merge: + """* + Merge two or more scan groups. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Merge", + "Input":{ + "selection":{"mode":"visible"}, + "remesh":{ + "method": "FlowTriangles", + "quality": "Medium" + }, + "simplify":{"triangleCount": 20000 } + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Merge", + "Input":{ + "selection":{"mode":"visible"}, + "remesh":{ + "method": "FlowTriangles", + "quality": "Medium" + }, + "simplify":{"triangleCount": 20000 } + }, + "Output":{ + "meshes":[ + { + "name":"Combined", + "positions":237757, + "normals":237757, + "triangles":459622, + "size":11221632 + }, + { + "name":"Remeshed", + "positions":34311, + "normals":0, + "triangles":29738, + "size":945540 + }, + { + "name":"Simplified", + "positions":32415, + "normals":0, + "triangles":20000, + "size":628980 + } + ], + "scans":3, + "textures":3 + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `Merge` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Merge_Merge): + # A unique identifier generated by the client. + self.Index = Index + # "Merge" + self.Type = Type + # The merge settings. + self.Input = Input + + class Response: + # Server response for the `Merge` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Merge_Merge, Output: MF_V3_Descriptors_Merge_Merge, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "Merge" + self.Type = Type + # The requested merge settings. + self.Input = Input + # The merge descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/MergeData.py b/three/MF/V3/Tasks/MergeData.py new file mode 100644 index 0000000..b9c0d68 --- /dev/null +++ b/three/MF/V3/Tasks/MergeData.py @@ -0,0 +1,239 @@ +from MF.V3.Descriptors.ScanData import ScanData as MF_V3_Descriptors_ScanData_ScanData +from MF.V3.Settings.ScanData import ScanData as MF_V3_Settings_ScanData_ScanData +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class MergeData: + """* + Download the raw scan data for the current merge process. + + > Request example: + + ``` + { + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":-1, + "buffers":["All"] + } + } + } + ``` + + > Vertex position buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":1558188, + "Descriptor":{ + "components":[{ + "type":"Position" + "size":3, + "offset":0, + "normalized":false, + }], + "stride":3 + }, + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex position binary data transfer from server [1558188 bytes]. + + > Vertex normal buffer message from server. + + ```json + { + "Buffer":{ + "Index":1, + "Size":1558188, + "Descriptor":{ + "components":[{ + "type":"Normal" + "size":3, + "offset":0, + "normalized":false, + }], + "stride":3 + }, + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex normal binary data transfer from server [1558188 bytes]. + + > Vertex texture coordinate buffer message from server. + + ```json + { + "Buffer":{ + "Index":2, + "Size":1038792, + "Descriptor":{ + "components":[{ + "type":"UV" + "size":2, + "offset":0, + "normalized":false, + }], + "stride":2 + }, + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex texture coordinate binary data transfer from server [1038792 bytes]. + + > Texture image buffer message from server. + + ```json + { + "Buffer":{ + "Index":3, + "Size":3504494, + "Descriptor":{ + "components":[{ + "type":"Texture" + "size":0, + "offset":0, + "normalized":false, + }], + "stride":0 + }, + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Texture binary data transfer from server [3504494 bytes]. + + > Triangle index buffer message from server. + + ```json + { + "Buffer":{ + "Index":4, + "Size":1996356, + "Descriptor":{ + "components":[{ + "type":"Triangle" + "size":1, + "offset":0, + "normalized":false, + }], + "stride":1 + }, + "Task":{ + "Index":1, + "Type":"MergeData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Triangle index binary data transfer from server [1996356 bytes]. + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"MergeData" + "Input":{"index":-1,"buffers":["All"]}, + "Output":{ + "buffers":[ + {"components":[{"normalized":false,"offset":0,"size":3,"type":"Position"}],"stride":3}, + {"components":[{"normalized":false,"offset":0,"size":3,"type":"Normal"}],"stride":3}, + {"components":[{"normalized":false,"offset":0,"size":2,"type":"UV"}],"stride":2}, + {"components":[{"normalized":false,"offset":0,"size":0,"type":"Texture"}],"stride":0}, + {"components":[{"normalized":false,"offset":0,"size":1,"type":"Triangle"}],"stride":1} + ], + "index":1, + "name":"Scan-1" + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `MergeData` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_ScanData_ScanData): + # A unique identifier generated by the client. + self.Index = Index + # "MergeData" + self.Type = Type + # Requested scan data. + self.Input = Input + + class Response: + # Server response for the `MergeData` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_ScanData_ScanData, Output: MF_V3_Descriptors_ScanData_ScanData, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "MergeData" + self.Type = Type + # The scan data requested by the client. + self.Input = Input + # The scan data sent from the server. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `MergeData` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task, Descriptor: MF_V3_Descriptors_ScanData_ScanData.Buffer): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested MergeData task. + self.Task = Task + # The scan data buffer descriptor. + self.Descriptor = Descriptor + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/MoveGroup.py b/three/MF/V3/Tasks/MoveGroup.py new file mode 100644 index 0000000..ba5d1aa --- /dev/null +++ b/three/MF/V3/Tasks/MoveGroup.py @@ -0,0 +1,79 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState +from typing import List + + +class MoveGroup: + """* + Move a scan group. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"MoveGroup", + "Input":[1,2,0] + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"MoveGroup", + "Input":[1,2,0], + "Output":{ + "groups":[{ + "index":2, + "name":"Group 2", + "groups":[{ + "index":1, + "name":"Group 1" + }] + }], + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `MoveGroup` task. + def __init__(self, Index: int, Type: str, Input: List[int] = None): + # A unique identifier generated by the client. + self.Index = Index + # "MoveGroup" + self.Type = Type + """ + The requested source and destination move indices. + An Array of group indexes where + 1. The first is the index of the _source group_: the group to be moved. + 2. The second is the index of the _parent group_: the group into which the source group is moved. + 3. (Optional) The third is the zero-based order in which the source group is placed the other children of the parent group. Use `0` to insert the source group at the beginning of the parent group's children. If omitted, the source group is inserted at the end of the parent group's children. + """ + self.Input = Input + + class Response: + # Server response for the `MoveGroup` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Group, Input: List[int] = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "MoveGroup" + self.Type = Type + # The root scan group in the current open project. + self.Output = Output + # The requested source and destination move indices. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/NewGroup.py b/three/MF/V3/Tasks/NewGroup.py new file mode 100644 index 0000000..93db660 --- /dev/null +++ b/three/MF/V3/Tasks/NewGroup.py @@ -0,0 +1,77 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.NewGroup import NewGroup as MF_V3_Settings_NewGroup_NewGroup +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class NewGroup: + """* + Create a new scan group. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewGroup", + "Input":{ + "parentIndex":0, + "baseName":"Group" + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewGroup", + "Input":{ + "parentIndex":0, + "baseName":"Group" + }, + "Output":{ + "groups":[ + { + "index":1, + "name":"Group 1" + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `NewGroup` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_NewGroup_NewGroup = None): + # A unique identifier generated by the client. + self.Index = Index + # "NewGroup" + self.Type = Type + # The requested new group settings. + self.Input = Input + + class Response: + # Server response for the `NewGroup` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Group, Input: MF_V3_Settings_NewGroup_NewGroup = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "NewGroup" + self.Type = Type + # The root scan group in the current open project. + self.Output = Output + # The requested new group settings. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/NewProject.py b/three/MF/V3/Tasks/NewProject.py new file mode 100644 index 0000000..28ebce7 --- /dev/null +++ b/three/MF/V3/Tasks/NewProject.py @@ -0,0 +1,69 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.Project import Project as MF_V3_Settings_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class NewProject: + """* + Create a new project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewProject", + "Input":"New Project Name" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewProject", + "Input":{ + "name":"New Project Name" + }, + "Output":{ + "index":5, + "name":"New Project Name" + } + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `NewProject` task. + def __init__(self, Index: int, Type: str, Input: str = None): + # A unique identifier generated by the client. + self.Index = Index + # "NewProject" + self.Type = Type + # Optional new project name. + self.Input = Input + + class Response: + # Server response for the `NewProject` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Project_Project = None, Output: MF_V3_Descriptors_Project_Project = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "NewProject" + self.Type = Type + # Requested new project name. + self.Input = Input + # The new project descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/NewScan.py b/three/MF/V3/Tasks/NewScan.py new file mode 100644 index 0000000..2107534 --- /dev/null +++ b/three/MF/V3/Tasks/NewScan.py @@ -0,0 +1,82 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.Scan import Scan as MF_V3_Settings_Scan_Scan +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class NewScan: + """* + Capture a new scan. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewScan" + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + }, + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"NewScan" + "Input":{ + "camera":{"exposure":18000,"analogGain":256,"digitalGain":256}, + "capture":{"quality":"Medium","texture":true}, + "projector":{"brightness":0.8} + }, + "Output":{ + "groups":[{ + "color":[0.8,0.5,0.6,1.0], + "index":1, + "name":"Scan-1", + "scan":1, + "rotation":[0.2,0.8,-0.1], + "translation":[-275,-32,-134], + "visible":true + }], + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `NewScan` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scan_Scan = None): + # A unique identifier generated by the client. + self.Index = Index + # "NewScan" + self.Type = Type + # Scan settings. + self.Input = Input + + class Response: + # Server response for the `NewScan` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scan_Scan = None, Output: MF_V3_Descriptors_Project_Project.Group = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "NewScan" + self.Type = Type + # Requested scan settings. + self.Input = Input + # Project group descriptor with the updated list of scans. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/OpenProject.py b/three/MF/V3/Tasks/OpenProject.py new file mode 100644 index 0000000..06c6fa2 --- /dev/null +++ b/three/MF/V3/Tasks/OpenProject.py @@ -0,0 +1,66 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class OpenProject: + """* + Create a new project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"OpenProject", + "Input":5 + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"OpenProject", + "Input":5, + "Output":{ + "index":5, + "name":"Project 5" + } + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `OpenProject` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "OpenProject" + self.Type = Type + # The index of the project to open. Project indices can be obtained from the `ListProjects` task. + self.Input = Input + + class Response: + # Server response for the `OpenProject` task. + def __init__(self, Index: int, Type: str, Input: int, Output: MF_V3_Descriptors_Project_Project = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "OpenProject" + self.Type = Type + # The index of the project requested to open. + self.Input = Input + # The open project descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/PopSettings.py b/three/MF/V3/Tasks/PopSettings.py new file mode 100644 index 0000000..80916e5 --- /dev/null +++ b/three/MF/V3/Tasks/PopSettings.py @@ -0,0 +1,92 @@ +from MF.V3.Descriptors.Settings.Scanner import Scanner as MF_V3_Descriptors_Settings_Scanner_Scanner +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class PopSettings: + """* + Pop and restore scanner settings from the stack and optionally apply the popped settings. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"PopSettings", + "Input":true + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"PopSettings", + "Input":true, + "Output":{ + "camera":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "autoExposure":{"default":false,"value":false}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":320}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "projector":{ + "brightness":{"default":0.5,"max":1.0,"min":0.0,"value":0.800000011920929}, + "on":{"default":false,"value":true} + }, + "turntable":{ + "scans":{"default":8,"max":24,"min":1,"value":3}, + "sweep":{"default":360,"max":360,"min":5,"value":90}, + "use":{"default":true,"value":true} + }, + "capture":{ + "quality":{"default":"Medium","value":"Medium"}, + "texture":{"default":true,"value":true} + }, + "i18n":{ + "language":{"default":"en","value":"en"} + }, + "style":{ + "theme":{"default":"Dark","value":"Dark"} + }, + "viewer":{ + "textureOpacity":{"default":0.5,"max":1.0,"min":0.0,"value":1.0} + } + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `PopSettings` task. + def __init__(self, Index: int, Type: str, Input: bool = None): + # A unique identifier generated by the client. + self.Index = Index + # "PopSettings" + self.Type = Type + # Apply the popped settings. If unspecified popped settings are not applied. + self.Input = Input + + class Response: + # Server response for the `PopSettings` task. + def __init__(self, Index: int, Type: str, Input: bool = None, Output: MF_V3_Descriptors_Settings_Scanner_Scanner = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "PopSettings" + self.Type = Type + # Request to apply the popped settings. + self.Input = Input + # The scanner settings descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/PushSettings.py b/three/MF/V3/Tasks/PushSettings.py new file mode 100644 index 0000000..feaf6b0 --- /dev/null +++ b/three/MF/V3/Tasks/PushSettings.py @@ -0,0 +1,86 @@ +from MF.V3.Descriptors.Settings.Scanner import Scanner as MF_V3_Descriptors_Settings_Scanner_Scanner +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class PushSettings: + """* + Push the current scanner settings to a stack so they can be restored with PopSettings. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"PushSettings" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"PushSettings", + "Output":{ + "camera":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "autoExposure":{"default":false,"value":false}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":320}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "projector":{ + "brightness":{"default":0.5,"max":1.0,"min":0.0,"value":0.800000011920929}, + "on":{"default":false,"value":true} + }, + "turntable":{ + "scans":{"default":8,"max":24,"min":1,"value":3}, + "sweep":{"default":360,"max":360,"min":5,"value":90}, + "use":{"default":true,"value":true} + }, + "capture":{ + "quality":{"default":"Medium","value":"Medium"}, + "texture":{"default":true,"value":true} + }, + "i18n":{ + "language":{"default":"en","value":"en"} + }, + "style":{ + "theme":{"default":"Dark","value":"Dark"} + }, + "viewer":{ + "textureOpacity":{"default":0.5,"max":1.0,"min":0.0,"value":1.0} + } + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `PushSettings` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "PushSettings" + self.Type = Type + + class Response: + # Server response for the `PushSettings` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Settings_Scanner_Scanner = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "PushSettings" + self.Type = Type + # The scanner settings descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/Reboot.py b/three/MF/V3/Tasks/Reboot.py new file mode 100644 index 0000000..c954807 --- /dev/null +++ b/three/MF/V3/Tasks/Reboot.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class Reboot: + """* + Reboot the scanner. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Reboot" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Reboot" + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `Reboot` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "Reboot" + self.Type = Type + + class Response: + # Server response for the `Reboot` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "Reboot" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/RemoveGroups.py b/three/MF/V3/Tasks/RemoveGroups.py new file mode 100644 index 0000000..cc0cbd3 --- /dev/null +++ b/three/MF/V3/Tasks/RemoveGroups.py @@ -0,0 +1,64 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState +from typing import List + + +class RemoveGroups: + """* + Remove selected scan groups. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RemoveGroups", + "Input":[1,2] + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RemoveGroups", + "Input":[1,2], + "Output":{"groups":[]}, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `RemoveGroups` task. + def __init__(self, Index: int, Type: str, Input: List[int] = None): + # A unique identifier generated by the client. + self.Index = Index + # "RemoveGroups" + self.Type = Type + # The list of indices of the scan groups to remove. + self.Input = Input + + class Response: + # Server response for the `RemoveGroups` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Project_Project.Group, Input: List[int] = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "RemoveGroups" + self.Type = Type + # The root scan group in the current open project. + self.Output = Output + # The requested of indices of the scan groups to remove. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/RemoveProjects.py b/three/MF/V3/Tasks/RemoveProjects.py new file mode 100644 index 0000000..aef7e61 --- /dev/null +++ b/three/MF/V3/Tasks/RemoveProjects.py @@ -0,0 +1,68 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState +from typing import List + + +class RemoveProjects: + """* + Remove selected projects. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RemoveProjects", + "Input":[1,3,6] + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RemoveProjects", + "Input":[1,3,6], + "Output":[ + {"index":2,"modified":[2024,5,12,11,23,17],"name":"Project 2","size":35409834}, + {"index":4,"modified":[2024,5,12,11,2,37],"name":"Project 4","size":175025367}, + {"index":5,"modified":[2024,5,6,17,15,53],"name":"Project 5","size":24314083} + ], + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `RemoveProjects` task. + def __init__(self, Index: int, Type: str, Input: List[int] = None): + # A unique identifier generated by the client. + self.Index = Index + # "RemoveProjects" + self.Type = Type + # The list of indices of the projects to remove. + self.Input = Input + + class Response: + # Server response for the `RemoveProjects` task. + def __init__(self, Index: int, Type: str, Input: List[int] = None, Output: MF_V3_Descriptors_Project_Project.Brief = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "RemoveProjects" + self.Type = Type + # The list of indices of the requested projects to remove. + self.Input = Input + # Brief descriptors of the remaining projects. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/RestoreFactoryCalibration.py b/three/MF/V3/Tasks/RestoreFactoryCalibration.py new file mode 100644 index 0000000..4caea88 --- /dev/null +++ b/three/MF/V3/Tasks/RestoreFactoryCalibration.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class RestoreFactoryCalibration: + """* + Restore factory calibration. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RestoreFactoryCalibration" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RestoreFactoryCalibration", + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `RestoreFactoryCalibration` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "RestoreFactoryCalibration" + self.Type = Type + + class Response: + # Server response for the `RestoreFactoryCalibration` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "RestoreFactoryCalibration" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/RotateTurntable.py b/three/MF/V3/Tasks/RotateTurntable.py new file mode 100644 index 0000000..219c752 --- /dev/null +++ b/three/MF/V3/Tasks/RotateTurntable.py @@ -0,0 +1,59 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class RotateTurntable: + """* + Rotate the turntable. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RotateTurntable", + "Input":15 + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"RotateTurntable", + "Input":15, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `RotateTurntable` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "RotateTurntable" + self.Type = Type + # The rotation angle in degrees. + self.Input = Input + + class Response: + # Server response for the `RotateTurntable` task. + def __init__(self, Index: int, Type: str, Input: int, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "RotateTurntable" + self.Type = Type + # The requested rotation angle in degrees. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/ScanData.py b/three/MF/V3/Tasks/ScanData.py new file mode 100644 index 0000000..c5ecab3 --- /dev/null +++ b/three/MF/V3/Tasks/ScanData.py @@ -0,0 +1,239 @@ +from MF.V3.Descriptors.ScanData import ScanData as MF_V3_Descriptors_ScanData_ScanData +from MF.V3.Settings.ScanData import ScanData as MF_V3_Settings_ScanData_ScanData +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class ScanData: + """* + Download the raw scan data for a scan in the current open project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + ``` + + > Vertex position buffer message from server. + + ```json + { + "Buffer":{ + "Index":0, + "Size":1558188, + "Descriptor":{ + "components":[{ + "type":"Position" + "size":3, + "offset":0, + "normalized":false, + }], + "stride":3 + }, + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex position binary data transfer from server [1558188 bytes]. + + > Vertex normal buffer message from server. + + ```json + { + "Buffer":{ + "Index":1, + "Size":1558188, + "Descriptor":{ + "components":[{ + "type":"Normal" + "size":3, + "offset":0, + "normalized":false, + }], + "stride":3 + }, + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex normal binary data transfer from server [1558188 bytes]. + + > Vertex texture coordinate buffer message from server. + + ```json + { + "Buffer":{ + "Index":2, + "Size":1038792, + "Descriptor":{ + "components":[{ + "type":"UV" + "size":2, + "offset":0, + "normalized":false, + }], + "stride":2 + }, + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Vertex texture coordinate binary data transfer from server [1038792 bytes]. + + > Texture image buffer message from server. + + ```json + { + "Buffer":{ + "Index":3, + "Size":3504494, + "Descriptor":{ + "components":[{ + "type":"Texture" + "size":0, + "offset":0, + "normalized":false, + }], + "stride":0 + }, + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Texture binary data transfer from server [3504494 bytes]. + + > Triangle index buffer message from server. + + ```json + { + "Buffer":{ + "Index":4, + "Size":1996356, + "Descriptor":{ + "components":[{ + "type":"Triangle" + "size":1, + "offset":0, + "normalized":false, + }], + "stride":1 + }, + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{ + "index":1, + "buffers":["All"] + } + } + } + } + ``` + + > Triangle index binary data transfer from server [1996356 bytes]. + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"ScanData", + "Input":{"buffers":["All"],"index":1}, + "Output":{ + "buffers":[ + {"components":[{"normalized":false,"offset":0,"size":3,"type":"Position"}],"stride":3}, + {"components":[{"normalized":false,"offset":0,"size":3,"type":"Normal"}],"stride":3}, + {"components":[{"normalized":false,"offset":0,"size":2,"type":"UV"}],"stride":2}, + {"components":[{"normalized":false,"offset":0,"size":0,"type":"Texture"}],"stride":0}, + {"components":[{"normalized":false,"offset":0,"size":1,"type":"Triangle"}],"stride":1} + ], + "index":1, + "name":"Scan-1" + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `ScanData` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_ScanData_ScanData): + # A unique identifier generated by the client. + self.Index = Index + # "ScanData" + self.Type = Type + # Requested scan data. + self.Input = Input + + class Response: + # Server response for the `ScanData` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_ScanData_ScanData, Output: MF_V3_Descriptors_ScanData_ScanData, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "ScanData" + self.Type = Type + # The scan data requested by the client. + self.Input = Input + # The scan data sent from the server. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Server buffer message for the `ScanData` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task, Descriptor: MF_V3_Descriptors_ScanData_ScanData.Buffer): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested ScanData task. + self.Task = Task + # The scan data buffer descriptor. + self.Descriptor = Descriptor + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SetCameras.py b/three/MF/V3/Tasks/SetCameras.py new file mode 100644 index 0000000..188e93c --- /dev/null +++ b/three/MF/V3/Tasks/SetCameras.py @@ -0,0 +1,76 @@ +from MF.V3.Descriptors.Settings.Camera import Camera as MF_V3_Descriptors_Settings_Camera_Camera +from MF.V3.Settings.Camera import Camera as MF_V3_Settings_Camera_Camera +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SetCameras: + """* + Apply camera settings to one or both cameras. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetCameras", + "Input":{ + "analogGain":256, + "digitalGain":128, + "exposure":18000 + }, + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetCameras" + "Input":{ + "analogGain":256, + "digitalGain":512, + "exposure":18000 + }, + "Output":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":512}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SetCameras` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Camera_Camera = None): + # A unique identifier generated by the client. + self.Index = Index + # "SetCameras" + self.Type = Type + # Camera settings. + self.Input = Input + + class Response: + # Server response for the `SetCameras` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Camera_Camera = None, Output: MF_V3_Descriptors_Settings_Camera_Camera = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SetCameras" + self.Type = Type + # Requested camera settings. + self.Input = Input + # Actual camera settings after applying the requested settings. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SetGroup.py b/three/MF/V3/Tasks/SetGroup.py new file mode 100644 index 0000000..ab4933c --- /dev/null +++ b/three/MF/V3/Tasks/SetGroup.py @@ -0,0 +1,104 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.Group import Group as MF_V3_Settings_Group_Group +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SetGroup: + """* + Set scan group properties. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetGroup", + "Input":{ + "index":2, + "name":"Amazing Scan" + "color":[1,0,0,1], + "rotation":[0,3.14,0], + "translation":[0,10,25] + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetGroup", + "Input":{ + "index":2, + "name":"Amazing Scan" + "color":[1,0,0,1], + "rotation":[0,3.14,0], + "translation":[0,10,25] + } + "Output":{ + "groups":[ + { + "index":1, + "scan":1, + "name":"Scan-1", + "color":[0.75,0.5,0.2,1.0], + "rotation":[0.03,0.1,-0.01], + "translation":[-101,67,-561], + "visible":true + }, + { + "index":2, + "scan":2, + "name":"Amazing Scan", + "color":[1,0,0,1], + "rotation":[0,3.14,0], + "translation":[0,10,25], + "visible":true + }, + { + "index":3, + "scan":3, + "name":"Scan-3", + "color":[0.6,0.8,0.9,1.0], + "visible":true + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SetGroup` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Group_Group): + # A unique identifier generated by the client. + self.Index = Index + # "SetGroup" + self.Type = Type + # The requested group settings. + self.Input = Input + + class Response: + # Server response for the `SetGroup` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Group_Group, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SetGroup" + self.Type = Type + # The requested group settings. + self.Input = Input + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SetProject.py b/three/MF/V3/Tasks/SetProject.py new file mode 100644 index 0000000..c2d785f --- /dev/null +++ b/three/MF/V3/Tasks/SetProject.py @@ -0,0 +1,80 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.Project import Project as MF_V3_Settings_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SetProject: + """* + Apply settings to the current open project. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetProject", + "Input":{ + "name":"My Project" + }, + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetProject", + "Input":{ + "name":"My Project" + }, + "Output":{ + "index":5, + "name":"My Project", + "groups":[{ + "color":[0.8,0.5,0.6,1.0], + "index":1, + "name":"Scan-1", + "scan":1, + "rotation":[0.2,0.8,-0.1], + "translation":[-275,-32,-134], + "visible":true + }], + } + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SetProject` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Project_Project = None): + # A unique identifier generated by the client. + self.Index = Index + # "SetProject" + self.Type = Type + # Project settings. + self.Input = Input + + class Response: + # Server response for the `SetProject` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Project_Project = None, Output: MF_V3_Descriptors_Project_Project = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SetProject" + self.Type = Type + # Requested project settings. + self.Input = Input + # Actual project settings after applying the requested settings. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SetProjector.py b/three/MF/V3/Tasks/SetProjector.py new file mode 100644 index 0000000..2fe887c --- /dev/null +++ b/three/MF/V3/Tasks/SetProjector.py @@ -0,0 +1,75 @@ +from MF.V3.Descriptors.Settings.Projector import Projector as MF_V3_Descriptors_Settings_Projector_Projector +from MF.V3.Settings.Projector import Projector as MF_V3_Settings_Projector_Projector +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SetProjector: + """* + Apply projector settings. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetProjector" + "Input":{ + "on":true, + "brightness":0.75, + "color":[1.0, 1.0, 1.0] + }, + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SetProjector" + "Input":{ + "on":true, + "brightness":0.75, + "color":[1.0, 1.0, 1.0] + }, + "Output":{ + "on":{"default":false,"value":true}, + "brightness":{"default":0.5,"max":1.0,"min":0.0,"value":0.75} + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SetProjector` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Projector_Projector = None): + # A unique identifier generated by the client. + self.Index = Index + # "SetProjector" + self.Type = Type + # Projector settings. + self.Input = Input + + class Response: + # Server response for the `SetProjector` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Projector_Projector = None, Output: MF_V3_Descriptors_Settings_Projector_Projector = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SetProjector" + self.Type = Type + # Requested projector settings. + self.Input = Input + # Actual projector settings after applying the requested settings. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/Shutdown.py b/three/MF/V3/Tasks/Shutdown.py new file mode 100644 index 0000000..c6928ef --- /dev/null +++ b/three/MF/V3/Tasks/Shutdown.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class Shutdown: + """* + Shutdown the scanner. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Shutdown" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"Shutdown" + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `Shutdown` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "Shutdown" + self.Type = Type + + class Response: + # Server response for the `Shutdown` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "Shutdown" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SplitGroup.py b/three/MF/V3/Tasks/SplitGroup.py new file mode 100644 index 0000000..7efbce8 --- /dev/null +++ b/three/MF/V3/Tasks/SplitGroup.py @@ -0,0 +1,74 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SplitGroup: + """* + Split a scan group (ie. move its subgroups to its parent group). + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SplitGroup", + "Input":0 + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"SplitGroup", + "Input":0, + "Output":{ + "groups":[ + { + "index":1, + "name":"Group 1", + }, + { + "index":2, + "name":"Group 2" + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SplitGroup` task. + def __init__(self, Index: int, Type: str, Input: int): + # A unique identifier generated by the client. + self.Index = Index + # "SplitGroup" + self.Type = Type + # The index of the group to split. + self.Input = Input + + class Response: + # Server response for the `SplitGroup` task. + def __init__(self, Index: int, Type: str, Input: int, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SplitGroup" + self.Type = Type + # The requested index of the group to split. + self.Input = Input + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/StartVideo.py b/three/MF/V3/Tasks/StartVideo.py new file mode 100644 index 0000000..da7f6b2 --- /dev/null +++ b/three/MF/V3/Tasks/StartVideo.py @@ -0,0 +1,64 @@ +from MF.V3.Settings.Video import Video as MF_V3_Settings_Video_Video +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class StartVideo: + """* + Start the video stream. + + The video frames are sent as task buffers associated with the reserved video task index -1. The left and right camera frames are sent in buffer 0 and 1, respectively. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"StartVideo" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"StartVideo", + "Output":{ + "codec":"JPEG", + "format":"YUV420", + "width":510, + "height":380 + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `StartVideo` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "StartVideo" + self.Type = Type + + class Response: + # Server response for the `StartVideo` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Settings_Video_Video = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "StartVideo" + self.Type = Type + # The video settings. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/StopVideo.py b/three/MF/V3/Tasks/StopVideo.py new file mode 100644 index 0000000..68662b0 --- /dev/null +++ b/three/MF/V3/Tasks/StopVideo.py @@ -0,0 +1,53 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class StopVideo: + """* + Stop the video stream. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"StopVideo" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"StopVideo", + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `StopVideo` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "StopVideo" + self.Type = Type + + class Response: + # Server response for the `StopVideo` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "StopVideo" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/SystemInfo.py b/three/MF/V3/Tasks/SystemInfo.py new file mode 100644 index 0000000..59d8d4e --- /dev/null +++ b/three/MF/V3/Tasks/SystemInfo.py @@ -0,0 +1,92 @@ +from MF.V3.Descriptors.System import System as MF_V3_Descriptors_System_System +from MF.V3.Settings.Software import Software as MF_V3_Settings_Software_Software +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class SystemInfo: + """* + Get system information including the serial number, disk space and installed and available software versions. + + Request example: + ``` + { + "Task":{ + "Index":1, + "Type":"SystemInfo", + "Input":{ + "installed":["server","frontend"], + "available":["server","frontend"] + } + } + } + ``` + Response example: + ``` + { + "Task":{ + "Index":1, + "Type":"SystemInfo" + "Input":{ + "installed":["server","frontend"], + "available":["server","frontend"] + } + "Output":{ + "serialNumber":"1000000012345678", + "diskSpace":{"available":8523210752,"capacity":15082610688}, + "software:{ + "installed":[ + { + "name":"server", + "version":{ + "major":2, + "minor":21, + "patch":119, + "string":"2.21.119" + } + }, + { + "name":"frontend", + "version":{ + "major":2, + "minor":14, + "patch":39, + "string":"2.14.39" + } + } + }, + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `SystemInfo` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Software_Software = None): + # A unique identifier generated by the client. + self.Index = Index + # "SystemInfo" + self.Type = Type + # Software settings. + self.Input = Input + + class Response: + # Server response for the `SystemInfo` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_System_System, Input: MF_V3_Settings_Software_Software = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "SystemInfo" + self.Type = Type + # The system descriptor. + self.Output = Output + # The requested software settings. + self.Input = Input + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/TransformGroup.py b/three/MF/V3/Tasks/TransformGroup.py new file mode 100644 index 0000000..f706e6b --- /dev/null +++ b/three/MF/V3/Tasks/TransformGroup.py @@ -0,0 +1,81 @@ +from MF.V3.Descriptors.Project import Project as MF_V3_Descriptors_Project_Project +from MF.V3.Settings.Group import Group as MF_V3_Settings_Group_Group +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class TransformGroup: + """* + Apply a rigid transformation to a group. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"TransformGroup", + "Input":{ + "index":1, + "rotation":[0.5, 1.0, 1.5], + "translation":[10, 20, 30] + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"TransformGroup", + "Input":{ + "index":1, + "rotation":[0.5, 1.0, 1.5], + "translation":[10, 20, 30] + }, + "Output":{ + "groups":[ + { + "index":1, + "name":"Group 1", + "rotation":[0.5, 1.0, 1.5], + "translation":[10, 20, 30] + } + ] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `TransformGroup` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Group_Group): + # A unique identifier generated by the client. + self.Index = Index + # "TransformGroup" + self.Type = Type + # The group settings containing the requested rotation and translation. + self.Input = Input + + class Response: + # Server response for the `TransformGroup` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Group_Group, Output: MF_V3_Descriptors_Project_Project.Group, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "TransformGroup" + self.Type = Type + # The group settings containing the requested rotation and translation. + self.Input = Input + # The root scan group in the current open project. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/TurntableCalibration.py b/three/MF/V3/Tasks/TurntableCalibration.py new file mode 100644 index 0000000..67308a8 --- /dev/null +++ b/three/MF/V3/Tasks/TurntableCalibration.py @@ -0,0 +1,61 @@ +from MF.V3.Descriptors.Calibration import Turntable as MF_V3_Descriptors_Calibration_Turntable +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class TurntableCalibration: + """* + Get the turntable calibration descriptor. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"TurntableCalibration" + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"TurntableCalibration", + "Output":{ + "date":[2024,4,27,16,57,35], + "quality":"Excellent", + "focus":[300,320] + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `TurntableCalibration` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "TurntableCalibration" + self.Type = Type + + class Response: + # Server response for the `TurntableCalibration` task. + def __init__(self, Index: int, Type: str, Output: MF_V3_Descriptors_Calibration_Turntable = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "TurntableCalibration" + self.Type = Type + # The turntable calibration descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/UpdateSettings.py b/three/MF/V3/Tasks/UpdateSettings.py new file mode 100644 index 0000000..26818f7 --- /dev/null +++ b/three/MF/V3/Tasks/UpdateSettings.py @@ -0,0 +1,105 @@ +from MF.V3.Descriptors.Settings.Scanner import Scanner as MF_V3_Descriptors_Settings_Scanner_Scanner +from MF.V3.Settings.Scanner import Scanner as MF_V3_Settings_Scanner_Scanner +from MF.V3.Task import TaskState as MF_V3_Task_TaskState + + +class UpdateSettings: + """* + Update scanner settings. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"UpdateSettings", + "Input":{ + "camera":{ + "analogGain":256, + "digitalGain":320, + "exposure":18000 + } + } + } + } + ``` + + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"UpdateSettings", + "Input":{ + "camera":{ + "analogGain":256, + "digitalGain":320, + "exposure":18000 + } + }, + "Output":{ + "camera":{ + "analogGain":{"default":512.0,"max":1024.0,"min":256.0,"value":256.0}, + "autoExposure":{"default":false,"value":false}, + "digitalGain":{"default":256,"max":65536,"min":256,"value":320}, + "exposure":{"default":27000,"max":90000,"min":9000,"value":18000}, + }, + "projector":{ + "brightness":{"default":0.5,"max":1.0,"min":0.0,"value":0.800000011920929}, + "on":{"default":false,"value":true} + }, + "turntable":{ + "scans":{"default":8,"max":24,"min":1,"value":3}, + "sweep":{"default":360,"max":360,"min":5,"value":90}, + "use":{"default":true,"value":true} + }, + "capture":{ + "quality":{"default":"Medium","value":"Medium"}, + "texture":{"default":true,"value":true} + }, + "i18n":{ + "language":{"default":"en","value":"en"} + }, + "style":{ + "theme":{"default":"Dark","value":"Dark"} + }, + "viewer":{ + "textureOpacity":{"default":0.5,"max":1.0,"min":0.0,"value":1.0} + } + }, + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `UpdateSettings` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scanner_Scanner): + # A unique identifier generated by the client. + self.Index = Index + # "UpdateSettings" + self.Type = Type + # Scanner settings. + self.Input = Input + + class Response: + # Server response for the `UpdateSettings` task. + def __init__(self, Index: int, Type: str, Input: MF_V3_Settings_Scanner_Scanner, Output: MF_V3_Descriptors_Settings_Scanner_Scanner = None, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "UpdateSettings" + self.Type = Type + # The requested scanner settings. + self.Input = Input + # The scanner settings descriptor. + self.Output = Output + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/UploadProject.py b/three/MF/V3/Tasks/UploadProject.py new file mode 100644 index 0000000..8314523 --- /dev/null +++ b/three/MF/V3/Tasks/UploadProject.py @@ -0,0 +1,79 @@ +from MF.V3.Task import TaskState as MF_V3_Task_TaskState, Task as MF_V3_Task_Task + + +class UploadProject: + """* + Upload a project to the scanner. The project must be archived in a ZIP file. + + > Request example: + + ```json + { + "Task":{ + "Index":1, + "Type":"UploadProject" + } + } + ``` + + > Buffer message from client. + + ```json + { + "Buffer":{ + "Index":0, + "Size":15682096, + "Task":{ + "Index":1, + "Type":"UploadProject" + } + } + } + ``` + + > Binary data transfer from client: The project zip file [15682096 bytes]. + > Response example: + + ```json + { + "Task":{ + "Index":1, + "Type":"UploadProject" + "State":"Completed" + } + } + ``` + """ + class Request: + # Client request for the `UploadProject` task. + def __init__(self, Index: int, Type: str): + # A unique identifier generated by the client. + self.Index = Index + # "UploadProject" + self.Type = Type + + class Response: + # Server response for the `UploadProject` task. + def __init__(self, Index: int, Type: str, State: MF_V3_Task_TaskState = None, Error: str = None): + # The unique identifier generated by the client. + self.Index = Index + # "UploadProject" + self.Type = Type + # The current state of the task. + self.State = State + # A string describing the error if the task has failed. + self.Error = Error + + class Buffer: + # Client buffer message for the `UploadProject` task. + def __init__(self, Index: int, Size: int, Task: MF_V3_Task_Task): + # The zero-based index identifying the data buffer. + self.Index = Index + # The size of the incoming data buffer in bytes. + self.Size = Size + # The requested UploadProject task. + self.Task = Task + + def __init__(self): + pass + diff --git a/three/MF/V3/Tasks/__init__.py b/three/MF/V3/Tasks/__init__.py new file mode 100644 index 0000000..d91979d --- /dev/null +++ b/three/MF/V3/Tasks/__init__.py @@ -0,0 +1,56 @@ +from MF.V3.Tasks.AddMergeToProject import * +from MF.V3.Tasks.Align import * +from MF.V3.Tasks.AutoFocus import * +from MF.V3.Tasks.BoundingBox import * +from MF.V3.Tasks.CalibrateCameras import * +from MF.V3.Tasks.CalibrateTurntable import * +from MF.V3.Tasks.CalibrationCaptureTargets import * +from MF.V3.Tasks.CameraCalibration import * +from MF.V3.Tasks.CloseProject import * +from MF.V3.Tasks.ConnectWifi import * +from MF.V3.Tasks.DepthMap import * +from MF.V3.Tasks.DetectCalibrationCard import * +from MF.V3.Tasks.DownloadProject import * +from MF.V3.Tasks.Export import * +from MF.V3.Tasks.ExportLogs import * +from MF.V3.Tasks.ExportMerge import * +from MF.V3.Tasks.FlattenGroup import * +from MF.V3.Tasks.ForgetWifi import * +from MF.V3.Tasks.HasCameras import * +from MF.V3.Tasks.HasProjector import * +from MF.V3.Tasks.HasTurntable import * +from MF.V3.Tasks.ListExportFormats import * +from MF.V3.Tasks.ListGroups import * +from MF.V3.Tasks.ListNetworkInterfaces import * +from MF.V3.Tasks.ListProjects import * +from MF.V3.Tasks.ListScans import * +from MF.V3.Tasks.ListSettings import * +from MF.V3.Tasks.ListWifi import * +from MF.V3.Tasks.Merge import * +from MF.V3.Tasks.MergeData import * +from MF.V3.Tasks.MoveGroup import * +from MF.V3.Tasks.NewGroup import * +from MF.V3.Tasks.NewProject import * +from MF.V3.Tasks.NewScan import * +from MF.V3.Tasks.OpenProject import * +from MF.V3.Tasks.PopSettings import * +from MF.V3.Tasks.PushSettings import * +from MF.V3.Tasks.Reboot import * +from MF.V3.Tasks.RemoveGroups import * +from MF.V3.Tasks.RemoveProjects import * +from MF.V3.Tasks.RestoreFactoryCalibration import * +from MF.V3.Tasks.RotateTurntable import * +from MF.V3.Tasks.ScanData import * +from MF.V3.Tasks.SetCameras import * +from MF.V3.Tasks.SetGroup import * +from MF.V3.Tasks.SetProject import * +from MF.V3.Tasks.SetProjector import * +from MF.V3.Tasks.Shutdown import * +from MF.V3.Tasks.SplitGroup import * +from MF.V3.Tasks.StartVideo import * +from MF.V3.Tasks.StopVideo import * +from MF.V3.Tasks.SystemInfo import * +from MF.V3.Tasks.TransformGroup import * +from MF.V3.Tasks.TurntableCalibration import * +from MF.V3.Tasks.UpdateSettings import * +from MF.V3.Tasks.UploadProject import * diff --git a/three/MF/V3/Three.py b/three/MF/V3/Three.py new file mode 100644 index 0000000..150a228 --- /dev/null +++ b/three/MF/V3/Three.py @@ -0,0 +1,1160 @@ +from MF.V3 import Task +from MF.V3.Settings.Advanced import Advanced as MF_V3_Settings_Advanced_Advanced +from MF.V3.Settings.Align import Align as MF_V3_Settings_Align_Align +from MF.V3.Settings.AutoFocus import AutoFocus as MF_V3_Settings_AutoFocus_AutoFocus +from MF.V3.Settings.BoundingBox import BoundingBox as MF_V3_Settings_BoundingBox_BoundingBox +from MF.V3.Settings.Camera import Camera as MF_V3_Settings_Camera_Camera +from MF.V3.Settings.Capture import Capture as MF_V3_Settings_Capture_Capture +from MF.V3.Settings.Export import Export as MF_V3_Settings_Export_Export +from MF.V3.Settings.Group import Group as MF_V3_Settings_Group_Group +from MF.V3.Settings.I18n import I18n as MF_V3_Settings_I18n_I18n +from MF.V3.Settings.Merge import Merge as MF_V3_Settings_Merge_Merge +from MF.V3.Settings.NewGroup import NewGroup as MF_V3_Settings_NewGroup_NewGroup +from MF.V3.Settings.Project import Project as MF_V3_Settings_Project_Project +from MF.V3.Settings.Projector import Projector as MF_V3_Settings_Projector_Projector +from MF.V3.Settings.Scan import Scan as MF_V3_Settings_Scan_Scan +from MF.V3.Settings.ScanData import ScanData as MF_V3_Settings_ScanData_ScanData +from MF.V3.Settings.ScanSelection import ScanSelection as MF_V3_Settings_ScanSelection_ScanSelection +from MF.V3.Settings.Scanner import Scanner as MF_V3_Settings_Scanner_Scanner +from MF.V3.Settings.Software import Software as MF_V3_Settings_Software_Software +from MF.V3.Settings.Style import Style as MF_V3_Settings_Style_Style +from MF.V3.Settings.Turntable import Turntable as MF_V3_Settings_Turntable_Turntable +from MF.V3.Settings.Tutorials import Tutorials as MF_V3_Settings_Tutorials_Tutorials +from MF.V3.Settings.Viewer import Viewer as MF_V3_Settings_Viewer_Viewer +from MF.V3.Settings.Wifi import Wifi as MF_V3_Settings_Wifi_Wifi +from MF.V3.Tasks.AddMergeToProject import AddMergeToProject as MF_V3_Tasks_AddMergeToProject +from MF.V3.Tasks.Align import Align as MF_V3_Tasks_Align +from MF.V3.Tasks.AutoFocus import AutoFocus as MF_V3_Tasks_AutoFocus +from MF.V3.Tasks.BoundingBox import BoundingBox as MF_V3_Tasks_BoundingBox +from MF.V3.Tasks.CalibrateCameras import CalibrateCameras as MF_V3_Tasks_CalibrateCameras +from MF.V3.Tasks.CalibrateTurntable import CalibrateTurntable as MF_V3_Tasks_CalibrateTurntable +from MF.V3.Tasks.CalibrationCaptureTargets import CalibrationCaptureTargets as MF_V3_Tasks_CalibrationCaptureTargets +from MF.V3.Tasks.CameraCalibration import CameraCalibration as MF_V3_Tasks_CameraCalibration +from MF.V3.Tasks.CloseProject import CloseProject as MF_V3_Tasks_CloseProject +from MF.V3.Tasks.ConnectWifi import ConnectWifi as MF_V3_Tasks_ConnectWifi +from MF.V3.Tasks.DepthMap import DepthMap as MF_V3_Tasks_DepthMap +from MF.V3.Tasks.DetectCalibrationCard import DetectCalibrationCard as MF_V3_Tasks_DetectCalibrationCard +from MF.V3.Tasks.DownloadProject import DownloadProject as MF_V3_Tasks_DownloadProject +from MF.V3.Tasks.Export import Export as MF_V3_Tasks_Export +from MF.V3.Tasks.ExportLogs import ExportLogs as MF_V3_Tasks_ExportLogs +from MF.V3.Tasks.ExportMerge import ExportMerge as MF_V3_Tasks_ExportMerge +from MF.V3.Tasks.FlattenGroup import FlattenGroup as MF_V3_Tasks_FlattenGroup +from MF.V3.Tasks.ForgetWifi import ForgetWifi as MF_V3_Tasks_ForgetWifi +from MF.V3.Tasks.HasCameras import HasCameras as MF_V3_Tasks_HasCameras +from MF.V3.Tasks.HasProjector import HasProjector as MF_V3_Tasks_HasProjector +from MF.V3.Tasks.HasTurntable import HasTurntable as MF_V3_Tasks_HasTurntable +from MF.V3.Tasks.ListExportFormats import ListExportFormats as MF_V3_Tasks_ListExportFormats +from MF.V3.Tasks.ListGroups import ListGroups as MF_V3_Tasks_ListGroups +from MF.V3.Tasks.ListNetworkInterfaces import ListNetworkInterfaces as MF_V3_Tasks_ListNetworkInterfaces +from MF.V3.Tasks.ListProjects import ListProjects as MF_V3_Tasks_ListProjects +from MF.V3.Tasks.ListScans import ListScans as MF_V3_Tasks_ListScans +from MF.V3.Tasks.ListSettings import ListSettings as MF_V3_Tasks_ListSettings +from MF.V3.Tasks.ListWifi import ListWifi as MF_V3_Tasks_ListWifi +from MF.V3.Tasks.Merge import Merge as MF_V3_Tasks_Merge +from MF.V3.Tasks.MergeData import MergeData as MF_V3_Tasks_MergeData +from MF.V3.Tasks.MoveGroup import MoveGroup as MF_V3_Tasks_MoveGroup +from MF.V3.Tasks.NewGroup import NewGroup as MF_V3_Tasks_NewGroup +from MF.V3.Tasks.NewProject import NewProject as MF_V3_Tasks_NewProject +from MF.V3.Tasks.NewScan import NewScan as MF_V3_Tasks_NewScan +from MF.V3.Tasks.OpenProject import OpenProject as MF_V3_Tasks_OpenProject +from MF.V3.Tasks.PopSettings import PopSettings as MF_V3_Tasks_PopSettings +from MF.V3.Tasks.PushSettings import PushSettings as MF_V3_Tasks_PushSettings +from MF.V3.Tasks.Reboot import Reboot as MF_V3_Tasks_Reboot +from MF.V3.Tasks.RemoveGroups import RemoveGroups as MF_V3_Tasks_RemoveGroups +from MF.V3.Tasks.RemoveProjects import RemoveProjects as MF_V3_Tasks_RemoveProjects +from MF.V3.Tasks.RestoreFactoryCalibration import RestoreFactoryCalibration as MF_V3_Tasks_RestoreFactoryCalibration +from MF.V3.Tasks.RotateTurntable import RotateTurntable as MF_V3_Tasks_RotateTurntable +from MF.V3.Tasks.ScanData import ScanData as MF_V3_Tasks_ScanData +from MF.V3.Tasks.SetCameras import SetCameras as MF_V3_Tasks_SetCameras +from MF.V3.Tasks.SetGroup import SetGroup as MF_V3_Tasks_SetGroup +from MF.V3.Tasks.SetProject import SetProject as MF_V3_Tasks_SetProject +from MF.V3.Tasks.SetProjector import SetProjector as MF_V3_Tasks_SetProjector +from MF.V3.Tasks.Shutdown import Shutdown as MF_V3_Tasks_Shutdown +from MF.V3.Tasks.SplitGroup import SplitGroup as MF_V3_Tasks_SplitGroup +from MF.V3.Tasks.StartVideo import StartVideo as MF_V3_Tasks_StartVideo +from MF.V3.Tasks.StopVideo import StopVideo as MF_V3_Tasks_StopVideo +from MF.V3.Tasks.SystemInfo import SystemInfo as MF_V3_Tasks_SystemInfo +from MF.V3.Tasks.TransformGroup import TransformGroup as MF_V3_Tasks_TransformGroup +from MF.V3.Tasks.TurntableCalibration import TurntableCalibration as MF_V3_Tasks_TurntableCalibration +from MF.V3.Tasks.UpdateSettings import UpdateSettings as MF_V3_Tasks_UpdateSettings +from MF.V3.Tasks.UploadProject import UploadProject as MF_V3_Tasks_UploadProject +from typing import List + + +def list_network_interfaces(self) -> Task: + # List available wifi networks. + list_network_interfaces_request = MF_V3_Tasks_ListNetworkInterfaces.Request( + Index=0, + Type="ListNetworkInterfaces" + ) + list_network_interfaces_response = MF_V3_Tasks_ListNetworkInterfaces.Response( + Index=0, + Type="ListNetworkInterfaces" + ) + task = Task(Index=0, Type="ListNetworkInterfaces", Input=list_network_interfaces_request, Output=list_network_interfaces_response) + self.SendTask(task) + return task + + +def list_wifi(self) -> Task: + # List available wifi networks. + list_wifi_request = MF_V3_Tasks_ListWifi.Request( + Index=0, + Type="ListWifi" + ) + list_wifi_response = MF_V3_Tasks_ListWifi.Response( + Index=0, + Type="ListWifi" + ) + task = Task(Index=0, Type="ListWifi", Input=list_wifi_request, Output=list_wifi_response) + self.SendTask(task) + return task + + +def connect_wifi(self, ssid: str, password: str) -> Task: + # Connect to a wifi network. + connect_wifi_request = MF_V3_Tasks_ConnectWifi.Request( + Index=0, + Type="ConnectWifi", + Input=MF_V3_Settings_Wifi_Wifi( + ssid=ssid, + password=password, + ) + ) + connect_wifi_response = MF_V3_Tasks_ConnectWifi.Response( + Index=0, + Type="ConnectWifi", + Input=MF_V3_Settings_Wifi_Wifi( + ssid=ssid, + password=password, + ) + ) + task = Task(Index=0, Type="ConnectWifi", Input=connect_wifi_request, Output=connect_wifi_response) + self.SendTask(task) + return task + + +def forget_wifi(self) -> Task: + # Forget all wifi connections. + forget_wifi_request = MF_V3_Tasks_ForgetWifi.Request( + Index=0, + Type="ForgetWifi" + ) + forget_wifi_response = MF_V3_Tasks_ForgetWifi.Response( + Index=0, + Type="ForgetWifi" + ) + task = Task(Index=0, Type="ForgetWifi", Input=forget_wifi_request, Output=forget_wifi_response) + self.SendTask(task) + return task + + +def list_settings(self) -> Task: + # Get scanner settings. + list_settings_request = MF_V3_Tasks_ListSettings.Request( + Index=0, + Type="ListSettings" + ) + list_settings_response = MF_V3_Tasks_ListSettings.Response( + Index=0, + Type="ListSettings" + ) + task = Task(Index=0, Type="ListSettings", Input=list_settings_request, Output=list_settings_response) + self.SendTask(task) + return task + + +def push_settings(self) -> Task: + # Push the current scanner settings to the settings stack. + push_settings_request = MF_V3_Tasks_PushSettings.Request( + Index=0, + Type="PushSettings" + ) + push_settings_response = MF_V3_Tasks_PushSettings.Response( + Index=0, + Type="PushSettings" + ) + task = Task(Index=0, Type="PushSettings", Input=push_settings_request, Output=push_settings_response) + self.SendTask(task) + return task + + +def pop_settings(self, Input: bool = None) -> Task: + # Pop and restore scanner settings from the settings stack. + pop_settings_request = MF_V3_Tasks_PopSettings.Request( + Index=0, + Type="PopSettings", + Input=Input + ) + pop_settings_response = MF_V3_Tasks_PopSettings.Response( + Index=0, + Type="PopSettings" + ) + task = Task(Index=0, Type="PopSettings", Input=pop_settings_request, Output=pop_settings_response) + self.SendTask(task) + return task + + +def update_settings(self, advanced: MF_V3_Settings_Advanced_Advanced = None, camera: MF_V3_Settings_Camera_Camera = None, capture: MF_V3_Settings_Capture_Capture = None, i18n: MF_V3_Settings_I18n_I18n = None, projector: MF_V3_Settings_Projector_Projector = None, style: MF_V3_Settings_Style_Style = None, turntable: MF_V3_Settings_Turntable_Turntable = None, tutorials: MF_V3_Settings_Tutorials_Tutorials = None, viewer: MF_V3_Settings_Viewer_Viewer = None, software: MF_V3_Settings_Software_Software = None) -> Task: + # Update scanner settings. + update_settings_request = MF_V3_Tasks_UpdateSettings.Request( + Index=0, + Type="UpdateSettings", + Input=MF_V3_Settings_Scanner_Scanner( + advanced=advanced, + camera=camera, + capture=capture, + i18n=i18n, + projector=projector, + style=style, + turntable=turntable, + tutorials=tutorials, + viewer=viewer, + software=software, + ) + ) + update_settings_response = MF_V3_Tasks_UpdateSettings.Response( + Index=0, + Type="UpdateSettings", + Input=MF_V3_Settings_Scanner_Scanner( + advanced=advanced, + camera=camera, + capture=capture, + i18n=i18n, + projector=projector, + style=style, + turntable=turntable, + tutorials=tutorials, + viewer=viewer, + software=software, + ) + ) + task = Task(Index=0, Type="UpdateSettings", Input=update_settings_request, Output=update_settings_response) + self.SendTask(task) + return task + + +def list_projects(self) -> Task: + # List all projects. + list_projects_request = MF_V3_Tasks_ListProjects.Request( + Index=0, + Type="ListProjects" + ) + list_projects_response = MF_V3_Tasks_ListProjects.Response( + Index=0, + Type="ListProjects" + ) + task = Task(Index=0, Type="ListProjects", Input=list_projects_request, Output=list_projects_response) + self.SendTask(task) + return task + + +def download_project(self, Input: int) -> Task: + # Download a project from the scanner. + download_project_request = MF_V3_Tasks_DownloadProject.Request( + Index=0, + Type="DownloadProject", + Input=Input + ) + download_project_response = MF_V3_Tasks_DownloadProject.Response( + Index=0, + Type="DownloadProject", + Input=Input + ) + task = Task(Index=0, Type="DownloadProject", Input=download_project_request, Output=download_project_response) + self.SendTask(task) + return task + + +def upload_project(self, buffer: bytes) -> Task: + # Upload a project to the scanner. + upload_project_request = MF_V3_Tasks_UploadProject.Request( + Index=0, + Type="UploadProject" + ) + upload_project_response = MF_V3_Tasks_UploadProject.Response( + Index=0, + Type="UploadProject" + ) + task = Task(Index=0, Type="UploadProject", Input=upload_project_request, Output=upload_project_response) + self.SendTask(task, buffer) + return task + + +def new_project(self, Input: str = None) -> Task: + # Create a new project. + new_project_request = MF_V3_Tasks_NewProject.Request( + Index=0, + Type="NewProject", + Input=Input + ) + new_project_response = MF_V3_Tasks_NewProject.Response( + Index=0, + Type="NewProject" + ) + task = Task(Index=0, Type="NewProject", Input=new_project_request, Output=new_project_response) + self.SendTask(task) + return task + + +def open_project(self, Input: int) -> Task: + # Open an existing project. + open_project_request = MF_V3_Tasks_OpenProject.Request( + Index=0, + Type="OpenProject", + Input=Input + ) + open_project_response = MF_V3_Tasks_OpenProject.Response( + Index=0, + Type="OpenProject", + Input=Input + ) + task = Task(Index=0, Type="OpenProject", Input=open_project_request, Output=open_project_response) + self.SendTask(task) + return task + + +def close_project(self) -> Task: + # Close the current open project. + close_project_request = MF_V3_Tasks_CloseProject.Request( + Index=0, + Type="CloseProject" + ) + close_project_response = MF_V3_Tasks_CloseProject.Response( + Index=0, + Type="CloseProject" + ) + task = Task(Index=0, Type="CloseProject", Input=close_project_request, Output=close_project_response) + self.SendTask(task) + return task + + +def remove_projects(self, Input: List[int] = None) -> Task: + # Remove selected projects. + remove_projects_request = MF_V3_Tasks_RemoveProjects.Request( + Index=0, + Type="RemoveProjects", + Input=Input + ) + remove_projects_response = MF_V3_Tasks_RemoveProjects.Response( + Index=0, + Type="RemoveProjects" + ) + task = Task(Index=0, Type="RemoveProjects", Input=remove_projects_request, Output=remove_projects_response) + self.SendTask(task) + return task + + +def list_groups(self) -> Task: + # List the scan groups in the current open project. + list_groups_request = MF_V3_Tasks_ListGroups.Request( + Index=0, + Type="ListGroups" + ) + list_groups_response = MF_V3_Tasks_ListGroups.Response( + Index=0, + Type="ListGroups", + Output=None + ) + task = Task(Index=0, Type="ListGroups", Input=list_groups_request, Output=list_groups_response) + self.SendTask(task) + return task + + +def list_scans(self) -> Task: + # List the scans in the current open project. + list_scans_request = MF_V3_Tasks_ListScans.Request( + Index=0, + Type="ListScans" + ) + list_scans_response = MF_V3_Tasks_ListScans.Response( + Index=0, + Type="ListScans" + ) + task = Task(Index=0, Type="ListScans", Input=list_scans_request, Output=list_scans_response) + self.SendTask(task) + return task + + +def scan_data(self, index: int, mergeStep: MF_V3_Settings_ScanData_ScanData.MergeStep = None, buffers: List[MF_V3_Settings_ScanData_ScanData.Buffer] = None, metadata: List[MF_V3_Settings_ScanData_ScanData.Metadata] = None) -> Task: + # Download the raw scan data for a scan in the current open project. + scan_data_request = MF_V3_Tasks_ScanData.Request( + Index=0, + Type="ScanData", + Input=MF_V3_Settings_ScanData_ScanData( + index=index, + mergeStep=mergeStep, + buffers=buffers, + metadata=metadata, + ) + ) + scan_data_response = MF_V3_Tasks_ScanData.Response( + Index=0, + Type="ScanData", + Input=MF_V3_Settings_ScanData_ScanData( + index=index, + mergeStep=mergeStep, + buffers=buffers, + metadata=metadata, + ), + Output=None + ) + task = Task(Index=0, Type="ScanData", Input=scan_data_request, Output=scan_data_response) + self.SendTask(task) + return task + + +def set_project(self, index: int = None, name: str = None) -> Task: + # Apply settings to the current open project. + set_project_request = MF_V3_Tasks_SetProject.Request( + Index=0, + Type="SetProject", + Input=MF_V3_Settings_Project_Project( + index=index, + name=name, + ) + ) + set_project_response = MF_V3_Tasks_SetProject.Response( + Index=0, + Type="SetProject" + ) + task = Task(Index=0, Type="SetProject", Input=set_project_request, Output=set_project_response) + self.SendTask(task) + return task + + +def set_group(self, index: int, name: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> Task: + # Set scan group properties. + set_group_request = MF_V3_Tasks_SetGroup.Request( + Index=0, + Type="SetGroup", + Input=MF_V3_Settings_Group_Group( + index=index, + name=name, + color=color, + visible=visible, + collapsed=collapsed, + rotation=rotation, + translation=translation, + ) + ) + set_group_response = MF_V3_Tasks_SetGroup.Response( + Index=0, + Type="SetGroup", + Input=MF_V3_Settings_Group_Group( + index=index, + name=name, + color=color, + visible=visible, + collapsed=collapsed, + rotation=rotation, + translation=translation, + ), + Output=None + ) + task = Task(Index=0, Type="SetGroup", Input=set_group_request, Output=set_group_response) + self.SendTask(task) + return task + + +def new_group(self, parentIndex: int = None, baseName: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> Task: + # Create a new scan group. + new_group_request = MF_V3_Tasks_NewGroup.Request( + Index=0, + Type="NewGroup", + Input=MF_V3_Settings_NewGroup_NewGroup( + parentIndex=parentIndex, + baseName=baseName, + color=color, + visible=visible, + collapsed=collapsed, + rotation=rotation, + translation=translation, + ) + ) + new_group_response = MF_V3_Tasks_NewGroup.Response( + Index=0, + Type="NewGroup", + Output=None + ) + task = Task(Index=0, Type="NewGroup", Input=new_group_request, Output=new_group_response) + self.SendTask(task) + return task + + +def move_group(self, Input: List[int] = None) -> Task: + # Move a scan group. + move_group_request = MF_V3_Tasks_MoveGroup.Request( + Index=0, + Type="MoveGroup", + Input=Input + ) + move_group_response = MF_V3_Tasks_MoveGroup.Response( + Index=0, + Type="MoveGroup", + Output=None + ) + task = Task(Index=0, Type="MoveGroup", Input=move_group_request, Output=move_group_response) + self.SendTask(task) + return task + + +def flatten_group(self, Input: int) -> Task: + # Flatten a scan group such that it only consists of single scans. + flatten_group_request = MF_V3_Tasks_FlattenGroup.Request( + Index=0, + Type="FlattenGroup", + Input=Input + ) + flatten_group_response = MF_V3_Tasks_FlattenGroup.Response( + Index=0, + Type="FlattenGroup", + Input=Input, + Output=None + ) + task = Task(Index=0, Type="FlattenGroup", Input=flatten_group_request, Output=flatten_group_response) + self.SendTask(task) + return task + + +def split_group(self, Input: int) -> Task: + # Split a scan group (ie. move its subgroups to its parent group). + split_group_request = MF_V3_Tasks_SplitGroup.Request( + Index=0, + Type="SplitGroup", + Input=Input + ) + split_group_response = MF_V3_Tasks_SplitGroup.Response( + Index=0, + Type="SplitGroup", + Input=Input, + Output=None + ) + task = Task(Index=0, Type="SplitGroup", Input=split_group_request, Output=split_group_response) + self.SendTask(task) + return task + + +def transform_group(self, index: int, name: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> Task: + # Apply a rigid transformation to a group. + transform_group_request = MF_V3_Tasks_TransformGroup.Request( + Index=0, + Type="TransformGroup", + Input=MF_V3_Settings_Group_Group( + index=index, + name=name, + color=color, + visible=visible, + collapsed=collapsed, + rotation=rotation, + translation=translation, + ) + ) + transform_group_response = MF_V3_Tasks_TransformGroup.Response( + Index=0, + Type="TransformGroup", + Input=MF_V3_Settings_Group_Group( + index=index, + name=name, + color=color, + visible=visible, + collapsed=collapsed, + rotation=rotation, + translation=translation, + ), + Output=None + ) + task = Task(Index=0, Type="TransformGroup", Input=transform_group_request, Output=transform_group_response) + self.SendTask(task) + return task + + +def remove_groups(self, Input: List[int] = None) -> Task: + # Remove selected scan groups. + remove_groups_request = MF_V3_Tasks_RemoveGroups.Request( + Index=0, + Type="RemoveGroups", + Input=Input + ) + remove_groups_response = MF_V3_Tasks_RemoveGroups.Response( + Index=0, + Type="RemoveGroups", + Output=None + ) + task = Task(Index=0, Type="RemoveGroups", Input=remove_groups_request, Output=remove_groups_response) + self.SendTask(task) + return task + + +def bounding_box(self, selection: MF_V3_Settings_ScanSelection_ScanSelection, axisAligned: bool) -> Task: + # Get the bounding box of a set of scan groups. + bounding_box_request = MF_V3_Tasks_BoundingBox.Request( + Index=0, + Type="BoundingBox", + Input=MF_V3_Settings_BoundingBox_BoundingBox( + selection=selection, + axisAligned=axisAligned, + ) + ) + bounding_box_response = MF_V3_Tasks_BoundingBox.Response( + Index=0, + Type="BoundingBox", + Input=MF_V3_Settings_BoundingBox_BoundingBox( + selection=selection, + axisAligned=axisAligned, + ), + Output=None + ) + task = Task(Index=0, Type="BoundingBox", Input=bounding_box_request, Output=bounding_box_response) + self.SendTask(task) + return task + + +def align(self, source: int, target: int, rough: MF_V3_Settings_Align_Align.Rough = None, fine: MF_V3_Settings_Align_Align.Fine = None) -> Task: + # Align two scan groups. + align_request = MF_V3_Tasks_Align.Request( + Index=0, + Type="Align", + Input=MF_V3_Settings_Align_Align( + source=source, + target=target, + rough=rough, + fine=fine, + ) + ) + align_response = MF_V3_Tasks_Align.Response( + Index=0, + Type="Align", + Input=MF_V3_Settings_Align_Align( + source=source, + target=target, + rough=rough, + fine=fine, + ), + Output=None + ) + task = Task(Index=0, Type="Align", Input=align_request, Output=align_response) + self.SendTask(task) + return task + + +def merge(self, selection: MF_V3_Settings_ScanSelection_ScanSelection = None, remesh: MF_V3_Settings_Merge_Merge.Remesh = None, simplify: MF_V3_Settings_Merge_Merge.Simplify = None, texturize: bool = None) -> Task: + # Merge two or more scan groups. + merge_request = MF_V3_Tasks_Merge.Request( + Index=0, + Type="Merge", + Input=MF_V3_Settings_Merge_Merge( + selection=selection, + remesh=remesh, + simplify=simplify, + texturize=texturize, + ) + ) + merge_response = MF_V3_Tasks_Merge.Response( + Index=0, + Type="Merge", + Input=MF_V3_Settings_Merge_Merge( + selection=selection, + remesh=remesh, + simplify=simplify, + texturize=texturize, + ), + Output=None + ) + task = Task(Index=0, Type="Merge", Input=merge_request, Output=merge_response) + self.SendTask(task) + return task + + +def merge_data(self, index: int, mergeStep: MF_V3_Settings_ScanData_ScanData.MergeStep = None, buffers: List[MF_V3_Settings_ScanData_ScanData.Buffer] = None, metadata: List[MF_V3_Settings_ScanData_ScanData.Metadata] = None) -> Task: + # Download the raw scan data for the current merge process. + merge_data_request = MF_V3_Tasks_MergeData.Request( + Index=0, + Type="MergeData", + Input=MF_V3_Settings_ScanData_ScanData( + index=index, + mergeStep=mergeStep, + buffers=buffers, + metadata=metadata, + ) + ) + merge_data_response = MF_V3_Tasks_MergeData.Response( + Index=0, + Type="MergeData", + Input=MF_V3_Settings_ScanData_ScanData( + index=index, + mergeStep=mergeStep, + buffers=buffers, + metadata=metadata, + ), + Output=None + ) + task = Task(Index=0, Type="MergeData", Input=merge_data_request, Output=merge_data_response) + self.SendTask(task) + return task + + +def add_merge_to_project(self) -> Task: + # Add a merged scan to the current project. + add_merge_to_project_request = MF_V3_Tasks_AddMergeToProject.Request( + Index=0, + Type="AddMergeToProject" + ) + add_merge_to_project_response = MF_V3_Tasks_AddMergeToProject.Response( + Index=0, + Type="AddMergeToProject", + Output=None + ) + task = Task(Index=0, Type="AddMergeToProject", Input=add_merge_to_project_request, Output=add_merge_to_project_response) + self.SendTask(task) + return task + + +def list_export_formats(self) -> Task: + # List all export formats. + list_export_formats_request = MF_V3_Tasks_ListExportFormats.Request( + Index=0, + Type="ListExportFormats" + ) + list_export_formats_response = MF_V3_Tasks_ListExportFormats.Response( + Index=0, + Type="ListExportFormats" + ) + task = Task(Index=0, Type="ListExportFormats", Input=list_export_formats_request, Output=list_export_formats_response) + self.SendTask(task) + return task + + +def export(self, selection: MF_V3_Settings_ScanSelection_ScanSelection = None, texture: bool = None, merge: bool = None, format: MF_V3_Settings_Export_Export.Format = None, scale: float = None) -> Task: + # Export a group of scans. + export_request = MF_V3_Tasks_Export.Request( + Index=0, + Type="Export", + Input=MF_V3_Settings_Export_Export( + selection=selection, + texture=texture, + merge=merge, + format=format, + scale=scale, + ) + ) + export_response = MF_V3_Tasks_Export.Response( + Index=0, + Type="Export", + Input=MF_V3_Settings_Export_Export( + selection=selection, + texture=texture, + merge=merge, + format=format, + scale=scale, + ) + ) + task = Task(Index=0, Type="Export", Input=export_request, Output=export_response) + self.SendTask(task) + return task + + +def export_merge(self, selection: MF_V3_Settings_ScanSelection_ScanSelection = None, texture: bool = None, merge: bool = None, format: MF_V3_Settings_Export_Export.Format = None, scale: float = None) -> Task: + # Export a merged scan. + export_merge_request = MF_V3_Tasks_ExportMerge.Request( + Index=0, + Type="ExportMerge", + Input=MF_V3_Settings_Export_Export( + selection=selection, + texture=texture, + merge=merge, + format=format, + scale=scale, + ) + ) + export_merge_response = MF_V3_Tasks_ExportMerge.Response( + Index=0, + Type="ExportMerge", + Input=MF_V3_Settings_Export_Export( + selection=selection, + texture=texture, + merge=merge, + format=format, + scale=scale, + ) + ) + task = Task(Index=0, Type="ExportMerge", Input=export_merge_request, Output=export_merge_response) + self.SendTask(task) + return task + + +def export_logs(self, Input: bool = None) -> Task: + # Export scanner logs. + export_logs_request = MF_V3_Tasks_ExportLogs.Request( + Index=0, + Type="ExportLogs", + Input=Input + ) + export_logs_response = MF_V3_Tasks_ExportLogs.Response( + Index=0, + Type="ExportLogs" + ) + task = Task(Index=0, Type="ExportLogs", Input=export_logs_request, Output=export_logs_response) + self.SendTask(task) + return task + + +def has_cameras(self) -> Task: + # Check if the scanner has working cameras. + has_cameras_request = MF_V3_Tasks_HasCameras.Request( + Index=0, + Type="HasCameras" + ) + has_cameras_response = MF_V3_Tasks_HasCameras.Response( + Index=0, + Type="HasCameras" + ) + task = Task(Index=0, Type="HasCameras", Input=has_cameras_request, Output=has_cameras_response) + self.SendTask(task) + return task + + +def has_projector(self) -> Task: + # Check if the scanner has a working projector. + has_projector_request = MF_V3_Tasks_HasProjector.Request( + Index=0, + Type="HasProjector" + ) + has_projector_response = MF_V3_Tasks_HasProjector.Response( + Index=0, + Type="HasProjector" + ) + task = Task(Index=0, Type="HasProjector", Input=has_projector_request, Output=has_projector_response) + self.SendTask(task) + return task + + +def has_turntable(self) -> Task: + # Check if the scanner is connected to a working turntable. + has_turntable_request = MF_V3_Tasks_HasTurntable.Request( + Index=0, + Type="HasTurntable" + ) + has_turntable_response = MF_V3_Tasks_HasTurntable.Response( + Index=0, + Type="HasTurntable" + ) + task = Task(Index=0, Type="HasTurntable", Input=has_turntable_request, Output=has_turntable_response) + self.SendTask(task) + return task + + +def system_info(self, updateMajor: bool = None, updateNightly: bool = None) -> Task: + # Get system information. + system_info_request = MF_V3_Tasks_SystemInfo.Request( + Index=0, + Type="SystemInfo", + Input=MF_V3_Settings_Software_Software( + updateMajor=updateMajor, + updateNightly=updateNightly, + ) + ) + system_info_response = MF_V3_Tasks_SystemInfo.Response( + Index=0, + Type="SystemInfo", + Output=None + ) + task = Task(Index=0, Type="SystemInfo", Input=system_info_request, Output=system_info_response) + self.SendTask(task) + return task + + +def camera_calibration(self) -> Task: + # Get the camera calibration descriptor. + camera_calibration_request = MF_V3_Tasks_CameraCalibration.Request( + Index=0, + Type="CameraCalibration" + ) + camera_calibration_response = MF_V3_Tasks_CameraCalibration.Response( + Index=0, + Type="CameraCalibration" + ) + task = Task(Index=0, Type="CameraCalibration", Input=camera_calibration_request, Output=camera_calibration_response) + self.SendTask(task) + return task + + +def turntable_calibration(self) -> Task: + # Get the turntable calibration descriptor. + turntable_calibration_request = MF_V3_Tasks_TurntableCalibration.Request( + Index=0, + Type="TurntableCalibration" + ) + turntable_calibration_response = MF_V3_Tasks_TurntableCalibration.Response( + Index=0, + Type="TurntableCalibration" + ) + task = Task(Index=0, Type="TurntableCalibration", Input=turntable_calibration_request, Output=turntable_calibration_response) + self.SendTask(task) + return task + + +def calibration_capture_targets(self) -> Task: + # Get the calibration capture target for each camera calibration capture. + calibration_capture_targets_request = MF_V3_Tasks_CalibrationCaptureTargets.Request( + Index=0, + Type="CalibrationCaptureTargets" + ) + calibration_capture_targets_response = MF_V3_Tasks_CalibrationCaptureTargets.Response( + Index=0, + Type="CalibrationCaptureTargets" + ) + task = Task(Index=0, Type="CalibrationCaptureTargets", Input=calibration_capture_targets_request, Output=calibration_capture_targets_response) + self.SendTask(task) + return task + + +def calibrate_cameras(self) -> Task: + # Calibrate the cameras. + calibrate_cameras_request = MF_V3_Tasks_CalibrateCameras.Request( + Index=0, + Type="CalibrateCameras" + ) + calibrate_cameras_response = MF_V3_Tasks_CalibrateCameras.Response( + Index=0, + Type="CalibrateCameras" + ) + task = Task(Index=0, Type="CalibrateCameras", Input=calibrate_cameras_request, Output=calibrate_cameras_response) + self.SendTask(task) + return task + + +def calibrate_turntable(self) -> Task: + # Calibrate the turntable. + calibrate_turntable_request = MF_V3_Tasks_CalibrateTurntable.Request( + Index=0, + Type="CalibrateTurntable" + ) + calibrate_turntable_response = MF_V3_Tasks_CalibrateTurntable.Response( + Index=0, + Type="CalibrateTurntable" + ) + task = Task(Index=0, Type="CalibrateTurntable", Input=calibrate_turntable_request, Output=calibrate_turntable_response) + self.SendTask(task) + return task + + +def detect_calibration_card(self, Input: int) -> Task: + # Detect the calibration card on one or both cameras. + detect_calibration_card_request = MF_V3_Tasks_DetectCalibrationCard.Request( + Index=0, + Type="DetectCalibrationCard", + Input=Input + ) + detect_calibration_card_response = MF_V3_Tasks_DetectCalibrationCard.Response( + Index=0, + Type="DetectCalibrationCard", + Input=Input + ) + task = Task(Index=0, Type="DetectCalibrationCard", Input=detect_calibration_card_request, Output=detect_calibration_card_response) + self.SendTask(task) + return task + + +def restore_factory_calibration(self) -> Task: + # Restore factory calibration. + restore_factory_calibration_request = MF_V3_Tasks_RestoreFactoryCalibration.Request( + Index=0, + Type="RestoreFactoryCalibration" + ) + restore_factory_calibration_response = MF_V3_Tasks_RestoreFactoryCalibration.Response( + Index=0, + Type="RestoreFactoryCalibration" + ) + task = Task(Index=0, Type="RestoreFactoryCalibration", Input=restore_factory_calibration_request, Output=restore_factory_calibration_response) + self.SendTask(task) + return task + + +def start_video(self) -> Task: + # Start the video stream. + start_video_request = MF_V3_Tasks_StartVideo.Request( + Index=0, + Type="StartVideo" + ) + start_video_response = MF_V3_Tasks_StartVideo.Response( + Index=0, + Type="StartVideo" + ) + task = Task(Index=0, Type="StartVideo", Input=start_video_request, Output=start_video_response) + self.SendTask(task) + return task + + +def stop_video(self) -> Task: + # Stop the video stream. + stop_video_request = MF_V3_Tasks_StopVideo.Request( + Index=0, + Type="StopVideo" + ) + stop_video_response = MF_V3_Tasks_StopVideo.Response( + Index=0, + Type="StopVideo" + ) + task = Task(Index=0, Type="StopVideo", Input=stop_video_request, Output=stop_video_response) + self.SendTask(task) + return task + + +def set_cameras(self, selection: List[int] = None, autoExposure: bool = None, exposure: int = None, analogGain: float = None, digitalGain: int = None, focus: int = None) -> Task: + # Apply camera settings to one or both cameras. + set_cameras_request = MF_V3_Tasks_SetCameras.Request( + Index=0, + Type="SetCameras", + Input=MF_V3_Settings_Camera_Camera( + selection=selection, + autoExposure=autoExposure, + exposure=exposure, + analogGain=analogGain, + digitalGain=digitalGain, + focus=focus, + ) + ) + set_cameras_response = MF_V3_Tasks_SetCameras.Response( + Index=0, + Type="SetCameras" + ) + task = Task(Index=0, Type="SetCameras", Input=set_cameras_request, Output=set_cameras_response) + self.SendTask(task) + return task + + +def set_projector(self, on: bool = None, brightness: float = None, pattern: MF_V3_Settings_Projector_Projector.Pattern = None, image: MF_V3_Settings_Projector_Projector.Image = None, color: List[float] = None, buffer: bytes = None) -> Task: + # Apply projector settings. + set_projector_request = MF_V3_Tasks_SetProjector.Request( + Index=0, + Type="SetProjector", + Input=MF_V3_Settings_Projector_Projector( + on=on, + brightness=brightness, + pattern=pattern, + image=image, + color=color, + ) + ) + set_projector_response = MF_V3_Tasks_SetProjector.Response( + Index=0, + Type="SetProjector" + ) + task = Task(Index=0, Type="SetProjector", Input=set_projector_request, Output=set_projector_response) + self.SendTask(task, buffer) + return task + + +def auto_focus(self, applyAll: bool, cameras: List[MF_V3_Settings_AutoFocus_AutoFocus.Camera] = None) -> Task: + # Auto focus one or both cameras. + auto_focus_request = MF_V3_Tasks_AutoFocus.Request( + Index=0, + Type="AutoFocus", + Input=MF_V3_Settings_AutoFocus_AutoFocus( + applyAll=applyAll, + cameras=cameras, + ) + ) + auto_focus_response = MF_V3_Tasks_AutoFocus.Response( + Index=0, + Type="AutoFocus" + ) + task = Task(Index=0, Type="AutoFocus", Input=auto_focus_request, Output=auto_focus_response) + self.SendTask(task) + return task + + +def rotate_turntable(self, Input: int) -> Task: + # Rotate the turntable. + rotate_turntable_request = MF_V3_Tasks_RotateTurntable.Request( + Index=0, + Type="RotateTurntable", + Input=Input + ) + rotate_turntable_response = MF_V3_Tasks_RotateTurntable.Response( + Index=0, + Type="RotateTurntable", + Input=Input + ) + task = Task(Index=0, Type="RotateTurntable", Input=rotate_turntable_request, Output=rotate_turntable_response) + self.SendTask(task) + return task + + +def new_scan(self, camera: MF_V3_Settings_Camera_Camera = None, projector: MF_V3_Settings_Projector_Projector = None, turntable: MF_V3_Settings_Turntable_Turntable = None, capture: MF_V3_Settings_Capture_Capture = None, processing: MF_V3_Settings_Scan_Scan.Processing = None) -> Task: + # Capture a new scan. + new_scan_request = MF_V3_Tasks_NewScan.Request( + Index=0, + Type="NewScan", + Input=MF_V3_Settings_Scan_Scan( + camera=camera, + projector=projector, + turntable=turntable, + capture=capture, + processing=processing, + ) + ) + new_scan_response = MF_V3_Tasks_NewScan.Response( + Index=0, + Type="NewScan" + ) + task = Task(Index=0, Type="NewScan", Input=new_scan_request, Output=new_scan_response) + self.SendTask(task) + return task + + +def depth_map(self, camera: MF_V3_Settings_Camera_Camera = None, projector: MF_V3_Settings_Projector_Projector = None, turntable: MF_V3_Settings_Turntable_Turntable = None, capture: MF_V3_Settings_Capture_Capture = None, processing: MF_V3_Settings_Scan_Scan.Processing = None) -> Task: + # Capture a depth map. + depth_map_request = MF_V3_Tasks_DepthMap.Request( + Index=0, + Type="DepthMap", + Input=MF_V3_Settings_Scan_Scan( + camera=camera, + projector=projector, + turntable=turntable, + capture=capture, + processing=processing, + ) + ) + depth_map_response = MF_V3_Tasks_DepthMap.Response( + Index=0, + Type="DepthMap" + ) + task = Task(Index=0, Type="DepthMap", Input=depth_map_request, Output=depth_map_response) + self.SendTask(task) + return task + + +def reboot(self) -> Task: + # Reboot the scanner. + reboot_request = MF_V3_Tasks_Reboot.Request( + Index=0, + Type="Reboot" + ) + reboot_response = MF_V3_Tasks_Reboot.Response( + Index=0, + Type="Reboot" + ) + task = Task(Index=0, Type="Reboot", Input=reboot_request, Output=reboot_response) + self.SendTask(task) + return task + + +def shutdown(self) -> Task: + # Shutdown the scanner. + shutdown_request = MF_V3_Tasks_Shutdown.Request( + Index=0, + Type="Shutdown" + ) + shutdown_response = MF_V3_Tasks_Shutdown.Response( + Index=0, + Type="Shutdown" + ) + task = Task(Index=0, Type="Shutdown", Input=shutdown_request, Output=shutdown_response) + self.SendTask(task) + return task + + + + diff --git a/three/MF/V3/__init__.py b/three/MF/V3/__init__.py new file mode 100644 index 0000000..2bcc874 --- /dev/null +++ b/three/MF/V3/__init__.py @@ -0,0 +1,3 @@ +from MF.V3.Buffer import * +from MF.V3.Task import * +from MF.V3.Three import * diff --git a/three/PYPI_README.md b/three/PYPI_README.md new file mode 100644 index 0000000..bdfdf30 --- /dev/null +++ b/three/PYPI_README.md @@ -0,0 +1,56 @@ +# Matter and Form THREE Library + +## Overview +The Matter and Form THREE library provides a comprehensive API for controlling and interacting with the Matter and Form THREE scanner. This library allows developers to build custom integrations, automate tasks, and create new front-end systems for 3D scanning. + +## Project Home +[Github Project HERE](https://github.com/Matter-and-Form/three-python-library). + +## Examples + +### Using Premade Examples +To connect to the scanner, you can use the provided examples. For instance, to run the connection example, execute: + +```sh +python examples/connect.py +``` + +### Simple Example +Here is an example of how to use the library to connect to the scanner and control the projector: + +```python +from matter_and_form_three import Scanner + +# Create and connect to the scanner +scanner = Scanner(OnTask=None, OnMessage=None, OnBuffer=None) +# Use the Zeroconf address or replace with the ip +scanner.Connect("ws://matterandform.local:8081") + +# Simple request to list all projects +projectTask = scanner.list_projects() + +# Check the output from the task for errors +if projectTask.Error: + print('Error:', projectTask.Error) + return +# Do something with the output +for project_obj in projectTask.Output: + project = Project.Brief(**project_obj) + print('Project index:', project.index, ' - Name:', project.name) +``` + +### More Examples +The library comes with several pre-made examples. You can find them in the [examples directory](https://github.com/Matter-and-Form/three-python-library/tree/develop/three/examples). + +To run a specific example, use: + +```sh +python three/examples/.py +``` + +## Documentation +For detailed documentation, visit the TODO -> [official documentation](https://github.com/Matter-and-Form/three-python-library/wiki). + + +## License +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/three/__init__.py b/three/__init__.py new file mode 100644 index 0000000..f9614df --- /dev/null +++ b/three/__init__.py @@ -0,0 +1 @@ +__version__ = '8.20.8' \ No newline at end of file diff --git a/maf_three/examples/__main__.py b/three/examples/__main__.py similarity index 80% rename from maf_three/examples/__main__.py rename to three/examples/__main__.py index 3301487..a3917d9 100644 --- a/maf_three/examples/__main__.py +++ b/three/examples/__main__.py @@ -2,13 +2,12 @@ import sys -from maf_three.examples import connection, projector, turntable, task, turntableCalibration, simpleScanner +from three.examples import connection, projector, task, turntableCalibration, simpleScanner # Available examples dictionary examples = { 'connection': connection, 'projector': projector, - 'turntable': turntable, 'task': task, 'turntableCalibration': turntableCalibration, 'simpleScanner': simpleScanner @@ -20,7 +19,7 @@ def PrintExampleList(): for ex in examples: print(' *',ex) print('Run via:') - print('python3 -m maf_three.examples {example_name}') + print('python3 -m three.examples {example_name}') # No argument provided ? if len(sys.argv) == 1: diff --git a/maf_three/examples/connection.py b/three/examples/connection.py similarity index 91% rename from maf_three/examples/connection.py rename to three/examples/connection.py index db47015..cf91d41 100644 --- a/maf_three/examples/connection.py +++ b/three/examples/connection.py @@ -1,6 +1,6 @@ # Connection -from maf_three.scanner import Scanner +from three.scanner import Scanner def main(): try: diff --git a/three/examples/projector.py b/three/examples/projector.py new file mode 100644 index 0000000..f49b7c1 --- /dev/null +++ b/three/examples/projector.py @@ -0,0 +1,82 @@ +# Projector + +import sys +import os +import time +import numpy as np + +# from three.scanner import Scanner +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from three.scanner import Scanner +import three.MF.V3.Three as Three +from three.MF.V3.Settings.Projector import Projector +from three.MF.V3.Settings.Video import Video +from three.MF.V3.Settings.Rectangle import Rectangle + +def main(): + + try: + scanner = Scanner(OnTask=None, OnMessage=None, OnBuffer=None) + scanner.Connect("ws://matterandform.local:8081") + + # Set white color + scanner.set_projector(color=[1,1,1], on=True, brightness=1.0) + # Sleep for 1 second + time.sleep(1) + + # Set red color + scanner.set_projector(color=[1,0,0]) + # Sleep for 1 second + time.sleep(1) + + # Set green color + scanner.set_projector(color=[0,1,0]) + # Sleep for 1 second + time.sleep(1) + + # Set blue color + scanner.set_projector(color=[0,0,1]) + # Sleep for 1 second + time.sleep(1) + + # Set brightness to 0.25. The other settings will persist + scanner.set_projector(brightness=0.25) + time.sleep(1) + + pattern = Projector.Pattern(Projector.Orientation.Vertical,4, 1) + scanner.set_projector(True, 1.0, pattern, None) + time.sleep(1) + + ### Project an image + print('Project Image') + width = 856 + height = 480 + img = np.zeros([height, width, 3], np.uint8) + for y in range(height): + for x in range(0, width): + img[y,x] = ( + 255 * y / height , # Blue + 255 * x / width , # Green + 255 - 255 * y / height # Red + ) + source = Projector.Image.Source(format = Video.Format.BGR888, width=width, height=height, step=3*width, fixAspectRatio=True) + scanner.set_projector(on=True, image=Projector.Image(source, Rectangle(0,0,width,height)), color=None, buffer=img.tobytes()) + + time.sleep(1) + + #### Turn OFF + scanner.set_projector(on=False) + + except Exception as error: + print('Error: ', error) + except: + print('Error') + + finally: + if scanner.IsConnected(): + scanner.Disconnect() + print('Finally') + +if __name__ == "__main__": + main() diff --git a/three/examples/simpleScanner.py b/three/examples/simpleScanner.py new file mode 100644 index 0000000..4dbce83 --- /dev/null +++ b/three/examples/simpleScanner.py @@ -0,0 +1,234 @@ +# Simple Scanner + +import numpy as np +import json +from typing import List + +# Three library +from three.scanner import Scanner +from three.MF.V3.Settings.Projector import Projector +from three.MF.V3.Settings.Capture import Capture +from three.MF.V3.Settings.Camera import Camera +from three.MF.V3.Settings.Turntable import Turntable +from three.MF.V3.Settings.ScanSelection import ScanSelection +from three.MF.V3.Settings.Export import Export +from three.MF.V3.Settings.Quality import Quality +from three.MF.V3.Descriptors.Project import Project +from three.MF.V3.Descriptors.Settings.Camera import Camera as CameraDescriptor +from three.MF.V3.Descriptors.Settings.Projector import Projector as ProjectorDescriptor +from three.MF.V3.Descriptors.Settings.Turntable import Turntable as TurntableDescriptor +from three.MF.V3.Descriptors.Settings.Capture import Capture as CaptureDescriptor + +from three.MF.V3 import Task, TaskState +# Two frames for the video stream +frame0 = np.zeros((0,0,3), np.uint8) +frame1 = np.zeros((0,0,3), np.uint8) + + +# Camera/Projector settings +camera = Camera(exposure=50000, digitalGain=256, analogGain=256.0) +projector = Projector(on=True, brightness=0.5) +turntable = Turntable(8,360,False) +capture = Capture(Quality.Low,False) + +scanTaskIndex = -1 + +def main(): + + # OpenCV + try: + import cv2 + + except ModuleNotFoundError as error: + print('###############################################') + print('This example required OpenCV for Python') + print('To install (apt or pip):') + print(' * sudo apt install python3-opencv') + print(' * pip3 install opencv-python') + print('###############################################') + exit(1) + + ControlsWindow = 'Controls' + Camera0Window = 'Camera0' + Camera1Window = 'Camera1' + + # Task update + def OnTask(task:Task): + # print(json.dumps(task, default=lambda o: o.__dict__, indent=4)) + if task.Progress: + print(f"{int((task.Progress.current/task.Progress.total)*100)} %") + else: + print(task.Type,task.Index,task.State) + + # Buffer received + def OnBuffer(descriptor, buffer:bytes): + global frame0, frame1 + + # Video task + if descriptor.Task['Type'] == 'Video': + if descriptor.Index == 0: + frame0 = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR) + else: + frame1 = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR) + + # DownloadFile + elif descriptor.Task['Type'] == "Export": + with open('scan.zip', 'wb') as binary_file: + binary_file.write(buffer) + print('Scan saved into scan.zip') + + + def OnTrackbarExposure(value): + # Exposure Min in 9000 + if (value < 9000): + value = 9000 + camera.exposure = value + scanner.set_cameras(exposure=value) + + def OnTrackbarAnalogGain(value): + camera.analogGain = value + scanner.set_cameras(analogGain=value) + + def OnTrackbarDigitalGain(value): + camera.digitalGain = value + scanner.set_cameras(digitalGain=value) + + def OnTrackbarProjectorBrightness(value): + projector.brightness = value/100 + scanner.set_projector(brightness=value/100) + + def OnTrackbarUseTurntable(value): + global turntable + turntable.use = value == 1 + + def OnTrackbarTurntableSweep(value): + global turntable + turntable.sweep = value + + def OnTrackbarTurntableSteps(value): + global turntable + turntable.scans = value + + def OnTrackbarFocus(value): + camera.focus = value + scanner.set_cameras(focus=value) + + def OnTrackbarQuality(value): + global capture + capture.quality = getQualityFromInt(value) + + def OnTrackbarTexture(value): + global capture + capture.texture = value == 1 + + def getIntFromQuality(quality:Quality) -> int: + if quality == Quality.Low.value: + return 0 + elif quality == Quality.Medium.value: + return 1 + elif quality == Quality.High.value: + return 2 + + def getQualityFromInt(quality:int) -> Quality: + if quality == 0: + return Quality.Low + elif quality == 1: + return Quality.Medium + elif quality == 2: + return Quality.High + + try: + global camera, projector, turntable + + # Connect to the scanner + scanner = Scanner(OnTask=OnTask, OnBuffer=OnBuffer, OnMessage=None) + scanner.Connect("ws://matterandform.local:8081") + + # Get the settings stored on the scanner and apply them to the UI + scanSettingsTask = scanner.list_settings() + cameraDescriptor = CameraDescriptor(**scanSettingsTask.Output["camera"]) + projectorDescriptor = ProjectorDescriptor(**scanSettingsTask.Output["projector"]) + turntableDescriptor = TurntableDescriptor(**scanSettingsTask.Output["turntable"]) + captureDescriptor = CaptureDescriptor(**scanSettingsTask.Output["capture"]) + + camera.exposure = cameraDescriptor.exposure["value"] + camera.analogGain = cameraDescriptor.analogGain["value"] + camera.digitalGain = cameraDescriptor.digitalGain["value"] + camera.focus = cameraDescriptor.focus["value"]["default"][0] + projector.brightness = projectorDescriptor.brightness["value"] + turntable.use = False + turntable.sweep = turntableDescriptor.sweep["value"] + turntable.scans = turntableDescriptor.scans["value"] + capture.quality = captureDescriptor.quality["value"] + capture.texture = captureDescriptor.texture["value"] + + # Create the UI + cv2.namedWindow(ControlsWindow) + cv2.namedWindow(Camera0Window) + cv2.namedWindow(Camera1Window) + cv2.moveWindow(ControlsWindow,0, 550) + cv2.moveWindow(Camera0Window,0,100) + cv2.moveWindow(Camera1Window,550,100) + cv2.createTrackbar('Exposure', ControlsWindow, int(camera.exposure), int(cameraDescriptor.exposure["max"]), OnTrackbarExposure) + cv2.createTrackbar('Camera Focus', ControlsWindow, int(camera.focus), int(cameraDescriptor.focus["value"]["max"]), OnTrackbarFocus) + cv2.createTrackbar('Analog Gain', ControlsWindow, int(camera.analogGain), int(cameraDescriptor.analogGain["max"]), OnTrackbarAnalogGain) + cv2.createTrackbar('Digital Gain', ControlsWindow, int(camera.digitalGain), int(cameraDescriptor.digitalGain["max"]), OnTrackbarDigitalGain) + cv2.createTrackbar('Projector Brightness', ControlsWindow, int(100 * projector.brightness), 100, OnTrackbarProjectorBrightness) + cv2.createTrackbar('Use Turntable', ControlsWindow, 1 if turntable.use else 0, 1, OnTrackbarUseTurntable) + cv2.createTrackbar('Turntable Sweep', ControlsWindow, int(turntable.sweep), int(turntableDescriptor.sweep["max"]), OnTrackbarTurntableSweep) + cv2.createTrackbar('Turntable Scans', ControlsWindow, int(turntable.scans), int(turntableDescriptor.scans["max"]), OnTrackbarTurntableSteps) + cv2.createTrackbar('Capture Quality', ControlsWindow, getIntFromQuality(capture.quality), 2, OnTrackbarQuality) + cv2.createTrackbar('Capture Texture', ControlsWindow, 1 if capture.texture else 0, 1, OnTrackbarTexture) + + new_project_return = scanner.new_project('SimpleScanner') + project:Project = Project(**new_project_return.Output) + scanner.open_project(project.index) + + # Turn on the projector and start the video + scanner.set_projector(on=True, brightness=projector.brightness, color=[1,1,1]) + scanner.start_video() + + + # User input loop + print('Press "Esc" to quit.') + while True: + + # If present => Show the frames + if frame0.size > 0: + cv2.imshow(Camera0Window,frame0) + if frame1.size > 0: + cv2.imshow(Camera1Window,frame1) + # User input + key = cv2.waitKey(1) + if(key != -1): + + if key == 27: # Esc => Break the loop + break + + elif key == 115: # 's' => Create a new Test Scan + scanner.new_scan(camera=camera, projector=projector, turntable=turntable if turntable.use else None, capture=capture) + scanner.export(selection=ScanSelection(ScanSelection.Mode.all) ,merge=True, texture=True, format=Export.Format.ply) + + elif key == 108: # 'l' => List all scans in the current project + list_project_return = scanner.list_projects() + project_list: List[Project.Brief] = [] + for proj in list_project_return.Output: + project_list.append(Project.Brief(**proj)) + for proj in project_list: + print(f"Project Index: {proj.index}, Project Name: {proj.name}") + + except Exception as error: + print('Error: ', error) + + finally: + if scanner.IsConnected(): + if project: + scanner.remove_projects([project.index]) + scanner.set_projector(on=False) + + + scanner.Disconnect() + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() diff --git a/maf_three/examples/task.py b/three/examples/task.py similarity index 65% rename from maf_three/examples/task.py rename to three/examples/task.py index c98aa76..512842d 100644 --- a/maf_three/examples/task.py +++ b/three/examples/task.py @@ -1,14 +1,11 @@ -# Task -import time +import json +from typing import List -from maf_three.scanner import Scanner -from maf_three.task import Task, TaskState -from maf_three.V3Task import V3Task - -from MF.V3.Settings.Camera import Camera -from MF.V3.Settings.Capture import Capture -from MF.V3.Settings.Projector import Projector -from MF.V3.Settings.Scan import Scan +# Three library +from three.scanner import Scanner +from three.MF.V3.Settings import Capture, Camera, Projector +from three.MF.V3.Descriptors import Project +from three.MF.V3 import Task, TaskState done = False @@ -16,9 +13,8 @@ def main(): global done + # Task update def OnTask(task:Task): - global done - # Inspect Task State match task.State: @@ -50,20 +46,16 @@ def OnTask(task:Task): scanner.Connect("ws://matterandform.local:8081") # Try to scan without input => Will trigger an error - scanner.SendTask(0, V3Task.NewTestScan) - - # Scan - scan = Scan( - camera=Camera(analogGain=256,digitalGain=256,exposure=50000), - capture=Capture(), - projector=Projector(brightness=0.5) - ) - scanner.SendTask(1, V3Task.NewTestScan, scan) - - # Wait for the tasks to finish - while not done: - time.sleep(0.1) - + scanner.new_scan() + + projectTask = scanner.list_projects() + if projectTask.Error: + print('Error:', projectTask.Error) + return + + for project_obj in projectTask.Output: + project = Project.Brief(**project_obj) + print('Project index:', project.index, ' - Name:', project.name) except Exception as error: print('Error: ', error) diff --git a/three/examples/turntableCalibration.py b/three/examples/turntableCalibration.py new file mode 100644 index 0000000..132e0b5 --- /dev/null +++ b/three/examples/turntableCalibration.py @@ -0,0 +1,112 @@ +from typing import List +import time + +# Three library +from three.scanner import Scanner +from three.MF.V3.Settings import Capture, Camera, Projector, Turntable, ScanSelection, Export, Quality +from three.MF.V3.Descriptors import Calibration +from three.MF.V3.Descriptors.Settings import Scanner as ScannerDescriptor, Camera as CameraDescriptor, Projector as ProjectorDescriptor, Turntable as TurntableDescriptor, Capture as CaptureDescriptor + +from three.MF.V3 import Task, TaskState + + +done = False +cornersDetected_0 = 0 # Amount of corners detected on camera 0 +cornersDetected_1 = 0 # Amount of corners detected on camera 1 +cornersTotal = 0 # Total number of corners to be detected + +def main(): + + def OnTask(task:Task): + global done + # print(json.dumps(task, default=lambda o: o.__dict__, indent=4)) + if task.Progress: + print(f"{int((task.Progress.current/task.Progress.total)*100)} %") + else: + print(task.Type,task.Index,task.State) + if task.Type == "CalibrateTurntable": + if task.State == "Completed": + print('Calibration Completed') + elif task.State == "Failed": + print('Calibration Failed:', task.Error) + done = True + elif task.Type == "DetectCalibrationCard": + if task.State == "Completed": + print('Calibration Card Detected') + elif task.State == "Failed": + print('Calibration Card Detection Failed:', task.Error) + done = True + + def OnBuffer(bufferObject, buffer:bytes): + global cornersDetected_0, cornersDetected_1, cornersTotal + + # Video task ? + if bufferObject.Task['Type'] == "Video": + + # Calibration card present in the descriptor + if "calibrationCard" in bufferObject.Descriptor: + calibrationCard = Calibration.DetectedCard(**bufferObject.Descriptor['calibrationCard']) + + # Total amount of corners + if cornersTotal == 0: + cardWidth = calibrationCard.size[0] + cardHeight = calibrationCard.size[1] + cornersTotal = (cardWidth - 1) * (cardHeight - 1) + + detectedCorners = int(len(calibrationCard.corners) / 2) + # Camera 0 + if bufferObject.Index == 0: + cornersDetected_0 = detectedCorners + # Camera 1 + else: + cornersDetected_1 = detectedCorners + + # No calibration card in the descriptor + else: + if bufferObject.Index == 0: + cornersDetected_0 = 0 + else: + cornersDetected_1 = 0 + print(f'Camera 0: {cornersDetected_0}/{cornersTotal} ; Camera 1: {cornersDetected_1}/{cornersTotal}') + + try: + # Connect + scanner = Scanner(OnTask=OnTask, OnMessage=None, OnBuffer=OnBuffer) + scanner.Connect("ws://matterandform.local:8081") + + # Start the video + scanner.start_video() + + # Detect the calibration card + print('******* Detecting the calibration card') + scanner.detect_calibration_card(3) # left camera only, 2 = Right camera only, 3 = Both cameras + + # Wait for the calibration card to be detected + while cornersTotal == 0: + time.sleep(0.1) + + # Detect the calibration card for 5sec + time.sleep(5) + + # Stop the video + scanner.detect_calibration_card(0) # Stop the detection + scanner.stop_video() + + # Calibration the turntable + print('\n******* Calibrating the turntable') + scanner.calibrate_turntable() + + + + except Exception as error: + print('Error: ', error) + except: + print('Error') + + finally: + if scanner.IsConnected(): + scanner.Disconnect() + + +if __name__ == "__main__": + main() diff --git a/three/requirements.txt b/three/requirements.txt new file mode 100644 index 0000000..8c99434 --- /dev/null +++ b/three/requirements.txt @@ -0,0 +1,3 @@ +numpy==2.1.3 +protobuf==5.28.3 +websocket-client==1.8.0 \ No newline at end of file diff --git a/maf_three/scanner.py b/three/scanner.py similarity index 63% rename from maf_three/scanner.py rename to three/scanner.py index aac37b4..6e1922b 100644 --- a/maf_three/scanner.py +++ b/three/scanner.py @@ -1,24 +1,26 @@ -## @package maf_three +## @package three # @file scanner.py # @brief Scanner class to wrap websocket connection # @date 2024-04-22 # @copyright © 2024 Matter and Form. All rights reserved. -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, List import websocket import json import threading import time +import importlib +import inspect +import types -from maf_three import __version__ -from maf_three.V3Task import V3Task -from maf_three.serialization import TO_JSON -from maf_three.task import Task, TaskState -from maf_three.buffer import Buffer +from MF.V3 import Task, TaskState, Buffer -from MF.V3.Descriptors.Software import Software +from three import __version__ +from three.serialization import TO_JSON +from three.MF.V3.Buffer import Buffer +import three.MF.V3.Three as Three class Scanner: """ @@ -31,12 +33,16 @@ class Scanner: """ __bufferDescriptor = None + __buffer = None __error = None + __taskIndex:int = 0 + __tasks:List[Task] = [] + def __init__(self, - OnTask: Optional[Callable[[Task], None]], - OnMessage: Optional[Callable[[str], None]], - OnBuffer: Optional[Callable[[Any, bytes], None]], + OnTask: Optional[Callable[[Task], None]] = None, + OnMessage: Optional[Callable[[str], None]] = None, + OnBuffer: Optional[Callable[[Any, bytes], None]] = None, ): """ Initializes the Scanner object. @@ -51,16 +57,28 @@ def __init__(self, self.OnTask = OnTask self.OnMessage = OnMessage self.OnBuffer = OnBuffer + + self.__task_return_event = threading.Event() + + # Dynamically add methods from Three to Scanner + self._add_three_methods() + + def _add_three_methods(self): + """ + Dynamically adds functions from the three_methods module to the Scanner class. + """ + for name, func in inspect.getmembers(Three, predicate=inspect.isfunction): + if not name.startswith('_'): + setattr(self, name, func.__get__(self, self.__class__)) - def Connect(self, URI:str, timeoutSec=5, checkVersionsCompatibility=True) -> bool: + def Connect(self, URI:str, timeoutSec=5) -> bool: """ Attempts to connect to the scanner using the specified URI and timeout. Args: * URI (str): The URI of the websocket server. * timeoutSec (int): Timeout in seconds, default is 5. - * checkVersionsCompatibility (bool): If True, right after the client connects to the server, check if the versions are compatible. Returns: bool: True if connection is successful, raises Exception otherwise. @@ -74,7 +92,6 @@ def Connect(self, URI:str, timeoutSec=5, checkVersionsCompatibility=True) -> boo self.__error = None self.__serverVersion__= None - self.__checkVersionsCompatibility__ = checkVersionsCompatibility self.websocket = websocket.WebSocketApp(self.__URI, on_open=self.__OnOpen, @@ -92,24 +109,7 @@ def Connect(self, URI:str, timeoutSec=5, checkVersionsCompatibility=True) -> boo while time.time() < start + timeoutSec: if self.__isConnected: # Not checking versions => return True - if not checkVersionsCompatibility: return True - else: - # Request the server version - self.SendTask(0, V3Task.SoftwareVersionInstalled, 'three-server') - # Wait for the reply - while self.__serverVersion__ == None: - time.sleep(0.1) - # Compare the versions - if str(self.__serverVersion__.major) != __version__.split('.')[0]: - raise Exception( - 'Major versions of Python library and Server mismatch.\n'+ - '* Server: '+ str(self.__serverVersion__.major)+ '.'+str(self.__serverVersion__.minor)+'.'+str(self.__serverVersion__.patch)+'\n' - '* maf_three: '+ __version__+'\n'+ - 'Please update your python library: pip3 install --upgrade --no-cache-dir maf_three') - # Major versions match - return True - elif self.__error: raise Exception(self.__error) time.sleep(0.1) @@ -139,7 +139,7 @@ def IsConnected(self)-> bool: def __callback(self, callback, *args) -> None: if callback: callback(self, *args) - + # Called when the connection is opened def __OnOpen(self, ws) -> None: """ @@ -200,8 +200,15 @@ def __OnMessage(self, ws, message) -> None: # Bytes ? if isinstance(message, bytes): if self.OnBuffer: - self.OnBuffer(self.__bufferDescriptor, message) - self.__bufferDescriptor = None + + if self.__buffer: + self.__buffer += message + else: + self.__buffer = message + if self.__bufferDescriptor.Size == len(self.__buffer): + self.OnBuffer(self.__bufferDescriptor, message) + self.__bufferDescriptor = None + self.__buffer = None else: obj = json.loads(message) @@ -209,26 +216,51 @@ def __OnMessage(self, ws, message) -> None: if 'Task' in obj: # Create the task from the message task = Task(**obj['Task']) - - # Are we checking versions compatibility ? - if self.__checkVersionsCompatibility__ and task.Type == V3Task.SoftwareVersionInstalled and task.State == TaskState.Completed: - self.__serverVersion__ = Software.Version(**task.Output) - self.__checkVersionsCompatibility__ = False - + + if (task.Progress): + # Extract the first (and only) item from the task.Progress dictionary + # TODO Duct tape fix due to schema weirdness for progress + key, process = next(iter(task.Progress.items())) + task.Progress = Task.Progress( + current=process["current"], + step=process["step"], + total=process["total"] + ) + + # Find the original task for reference + inputTask = self.__FindTaskWithIndex(task.Index) + if inputTask == None: + raise Exception('Task not found') + + if task.Error: + inputTask.Error = task.Error + self.__OnError(self.websocket, task.Error) + self.__task_return_event.set() + # If assigned => Call the handler - elif self.OnTask: + if self.OnTask: self.OnTask(task) + + + # If waiting for a response, set the response and notify + if (task.State == TaskState.Completed.value): + if task.Output: + inputTask.Output = task.Output + self.__task_return_event.set() + elif (task.State == TaskState.Failed.value): + inputTask.Error = task.Error + self.__task_return_event # Buffer elif 'Buffer' in obj: - self.__bufferDescriptor = obj['Buffer'] + self.__bufferDescriptor = Buffer(**obj['Buffer']) + self.__buffer = None # Message elif 'Message' in obj: if self.OnMessage: self.OnMessage(obj) - # Send a task to the scanner - def SendTask(self, index:int, type:V3Task, input = None) -> Task: + def SendTask(self, task, buffer:bytes = None) -> Any: """ Sends a task to the scanner. Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image) @@ -236,56 +268,60 @@ def SendTask(self, index:int, type:V3Task, input = None) -> Task: Creates a task, serializes it, and sends it via the websocket. Args: - * index (int): The index of the task. - * type (V3Task): The type of the task. - * input: Additional input for the task, default is None. + * task (Task): The task to send. + * buffer (bytes): The buffer data to send, default is None. Returns: - Task: The task object that was sent. + Any: The task object that was sent. Raises: AssertionError: If the connection is not established. """ assert self.__isConnected + + # Update the index + task.Index = self.__taskIndex + task.Input.Index = self.__taskIndex + self.__taskIndex += 1 + + # Send the task + self.__task_return_event.clear() - # Create the task - task = Task(index, type, input) + # Append the task + self.__tasks.append(task) + + if buffer == None: + self.__SendTask(task) + else: + self.__SendTaskWithBuffer(task, buffer) + + if task.Output: + # Wait for response + self.__task_return_event.wait() + + self.__tasks.remove(task) + + return task + + # Send a task to the scanner + def __SendTask(self, task): + assert self.__isConnected # Serialize the task - message = TO_JSON(task) + message = TO_JSON(task.Input) # Build and send the message message = '{"Task":' + message + '}' - #print('Message: ', message) - self.websocket.send(message) + print('Message: ', message) - return task + self.websocket.send(message) # Send a task with its buffer to the scanner - def SendTaskWithBuffer(self, index:int, type:V3Task, buffer:bytes, input = None) -> Task: - """ - Sends a task along with its associated buffer to the scanner. - This call is used to send data to the scanner, like an image to be projected by the projector. - An appropriate task must be sent with the buffer, or the buffer will be ignored. - - The task is serialized, and sent to the scanner, followed by the buffer. - - Args: - * index (int): The index of the task. - * type (V3Task): The type of the task. - * buffer (bytes): The buffer data to send. - * input: Additional input for the task, default is None. - - Returns: - Task: The task object that was sent. - - Raises: - AssertionError: If the connection is not established. - """ + def __SendTaskWithBuffer(self, task:Task, buffer:bytes): assert self.__isConnected # Send the task - task = self.SendTask(index, type, input) + self.__SendTask(task) # Build the buffer descriptor bufferSize = len(buffer) @@ -310,6 +346,11 @@ def SendTaskWithBuffer(self, index:int, type:V3Task, buffer:bytes, input = None) # Send the remaining data. if sentSize < bufferSize: self.websocket.send(buffer[sentSize:bufferSize], websocket.ABNF.OPCODE_BINARY) - - return task - + + def __FindTaskWithIndex(self, index:int) -> Task: + # Find the task in the list + for i, t in enumerate(self.__tasks): + if t.Index == index: + return t + break + return None \ No newline at end of file diff --git a/three/scanner.pyi b/three/scanner.pyi new file mode 100644 index 0000000..e1dcbbd --- /dev/null +++ b/three/scanner.pyi @@ -0,0 +1,162 @@ +import MF +import MF.V3 +import MF.V3.Settings +import MF.V3.Settings.Advanced +import MF.V3.Settings.Align +import MF.V3.Settings.AutoFocus +import MF.V3.Settings.BoundingBox +import MF.V3.Settings.Camera +import MF.V3.Settings.Capture +import MF.V3.Settings.Export +import MF.V3.Settings.Group +import MF.V3.Settings.I18n +import MF.V3.Settings.Merge +import MF.V3.Settings.NewGroup +import MF.V3.Settings.Project +import MF.V3.Settings.Projector +import MF.V3.Settings.Scan +import MF.V3.Settings.ScanData +import MF.V3.Settings.ScanSelection +import MF.V3.Settings.Scanner +import MF.V3.Settings.Software +import MF.V3.Settings.Style +import MF.V3.Settings.Turntable +import MF.V3.Settings.Tutorials +import MF.V3.Settings.Viewer +import MF.V3.Settings.Wifi +import MF.V3.Tasks +import MF.V3.Tasks.AddMergeToProject +import MF.V3.Tasks.Align +import MF.V3.Tasks.AutoFocus +import MF.V3.Tasks.BoundingBox +import MF.V3.Tasks.CalibrateCameras +import MF.V3.Tasks.CalibrateTurntable +import MF.V3.Tasks.CalibrationCaptureTargets +import MF.V3.Tasks.CameraCalibration +import MF.V3.Tasks.CloseProject +import MF.V3.Tasks.ConnectWifi +import MF.V3.Tasks.DepthMap +import MF.V3.Tasks.DetectCalibrationCard +import MF.V3.Tasks.DownloadProject +import MF.V3.Tasks.Export +import MF.V3.Tasks.ExportLogs +import MF.V3.Tasks.ExportMerge +import MF.V3.Tasks.FlattenGroup +import MF.V3.Tasks.ForgetWifi +import MF.V3.Tasks.HasCameras +import MF.V3.Tasks.HasProjector +import MF.V3.Tasks.HasTurntable +import MF.V3.Tasks.ListExportFormats +import MF.V3.Tasks.ListGroups +import MF.V3.Tasks.ListNetworkInterfaces +import MF.V3.Tasks.ListProjects +import MF.V3.Tasks.ListScans +import MF.V3.Tasks.ListSettings +import MF.V3.Tasks.ListWifi +import MF.V3.Tasks.Merge +import MF.V3.Tasks.MergeData +import MF.V3.Tasks.MoveGroup +import MF.V3.Tasks.NewGroup +import MF.V3.Tasks.NewProject +import MF.V3.Tasks.NewScan +import MF.V3.Tasks.OpenProject +import MF.V3.Tasks.PopSettings +import MF.V3.Tasks.PushSettings +import MF.V3.Tasks.Reboot +import MF.V3.Tasks.RemoveGroups +import MF.V3.Tasks.RemoveProjects +import MF.V3.Tasks.RestoreFactoryCalibration +import MF.V3.Tasks.RotateTurntable +import MF.V3.Tasks.ScanData +import MF.V3.Tasks.SetCameras +import MF.V3.Tasks.SetGroup +import MF.V3.Tasks.SetProject +import MF.V3.Tasks.SetProjector +import MF.V3.Tasks.Shutdown +import MF.V3.Tasks.SplitGroup +import MF.V3.Tasks.StartVideo +import MF.V3.Tasks.StopVideo +import MF.V3.Tasks.SystemInfo +import MF.V3.Tasks.TransformGroup +import MF.V3.Tasks.TurntableCalibration +import MF.V3.Tasks.UpdateSettings +import MF.V3.Tasks.UploadProject +import importlib +import inspect +import json +import threading +import three +import three.MF +import three.MF.V3 +import three.MF.V3.Buffer +import three.MF.V3.Three +import three.serialization +import time +import types +import typing +import websocket +from typing import Optional, Callable, Any, Union, List + + +class Scanner: + def __init__(self, OnTask: Optional[Callable[[MF.V3.Task.Task], None]] = None, OnMessage: Optional[Callable[[str], None]] = None, OnBuffer: Optional[Callable[[Any, bytes], None]] = None): ... + def Connect(self, URI: str, timeoutSec=5) -> bool: ... + def Disconnect(self) -> None: ... + def IsConnected(self) -> bool: ... + def SendTask(self, task, buffer: bytes = None) -> Any: ... + def add_merge_to_project(self) -> MF.V3.Task.Task: ... + def align(self, source: int, target: int, rough: MF.V3.Settings.Align.Align.Rough = None, fine: MF.V3.Settings.Align.Align.Fine = None) -> MF.V3.Task.Task: ... + def auto_focus(self, applyAll: bool, cameras: List[MF.V3.Settings.AutoFocus.AutoFocus.Camera] = None) -> MF.V3.Task.Task: ... + def bounding_box(self, selection: MF.V3.Settings.ScanSelection.ScanSelection, axisAligned: bool) -> MF.V3.Task.Task: ... + def calibrate_cameras(self) -> MF.V3.Task.Task: ... + def calibrate_turntable(self) -> MF.V3.Task.Task: ... + def calibration_capture_targets(self) -> MF.V3.Task.Task: ... + def camera_calibration(self) -> MF.V3.Task.Task: ... + def close_project(self) -> MF.V3.Task.Task: ... + def connect_wifi(self, ssid: str, password: str) -> MF.V3.Task.Task: ... + def depth_map(self, camera: MF.V3.Settings.Camera.Camera = None, projector: MF.V3.Settings.Projector.Projector = None, turntable: MF.V3.Settings.Turntable.Turntable = None, capture: MF.V3.Settings.Capture.Capture = None, processing: MF.V3.Settings.Scan.Scan.Processing = None) -> MF.V3.Task.Task: ... + def detect_calibration_card(self, Input: int) -> MF.V3.Task.Task: ... + def download_project(self, Input: int) -> MF.V3.Task.Task: ... + def export(self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, texture: bool = None, merge: bool = None, format: MF.V3.Settings.Export.Export.Format = None, scale: float = None) -> MF.V3.Task.Task: ... + def export_logs(self, Input: bool = None) -> MF.V3.Task.Task: ... + def export_merge(self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, texture: bool = None, merge: bool = None, format: MF.V3.Settings.Export.Export.Format = None, scale: float = None) -> MF.V3.Task.Task: ... + def flatten_group(self, Input: int) -> MF.V3.Task.Task: ... + def forget_wifi(self) -> MF.V3.Task.Task: ... + def has_cameras(self) -> MF.V3.Task.Task: ... + def has_projector(self) -> MF.V3.Task.Task: ... + def has_turntable(self) -> MF.V3.Task.Task: ... + def list_export_formats(self) -> MF.V3.Task.Task: ... + def list_groups(self) -> MF.V3.Task.Task: ... + def list_network_interfaces(self) -> MF.V3.Task.Task: ... + def list_projects(self) -> MF.V3.Task.Task: ... + def list_scans(self) -> MF.V3.Task.Task: ... + def list_settings(self) -> MF.V3.Task.Task: ... + def list_wifi(self) -> MF.V3.Task.Task: ... + def merge(self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, remesh: MF.V3.Settings.Merge.Merge.Remesh = None, simplify: MF.V3.Settings.Merge.Merge.Simplify = None, texturize: bool = None) -> MF.V3.Task.Task: ... + def merge_data(self, index: int, mergeStep: MF.V3.Settings.ScanData.ScanData.MergeStep = None, buffers: List[MF.V3.Settings.ScanData.ScanData.Buffer] = None, metadata: List[MF.V3.Settings.ScanData.ScanData.Metadata] = None) -> MF.V3.Task.Task: ... + def move_group(self, Input: List[int] = None) -> MF.V3.Task.Task: ... + def new_group(self, parentIndex: int = None, baseName: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> MF.V3.Task.Task: ... + def new_project(self, Input: str = None) -> MF.V3.Task.Task: ... + def new_scan(self, camera: MF.V3.Settings.Camera.Camera = None, projector: MF.V3.Settings.Projector.Projector = None, turntable: MF.V3.Settings.Turntable.Turntable = None, capture: MF.V3.Settings.Capture.Capture = None, processing: MF.V3.Settings.Scan.Scan.Processing = None) -> MF.V3.Task.Task: ... + def open_project(self, Input: int) -> MF.V3.Task.Task: ... + def pop_settings(self, Input: bool = None) -> MF.V3.Task.Task: ... + def push_settings(self) -> MF.V3.Task.Task: ... + def reboot(self) -> MF.V3.Task.Task: ... + def remove_groups(self, Input: List[int] = None) -> MF.V3.Task.Task: ... + def remove_projects(self, Input: List[int] = None) -> MF.V3.Task.Task: ... + def restore_factory_calibration(self) -> MF.V3.Task.Task: ... + def rotate_turntable(self, Input: int) -> MF.V3.Task.Task: ... + def scan_data(self, index: int, mergeStep: MF.V3.Settings.ScanData.ScanData.MergeStep = None, buffers: List[MF.V3.Settings.ScanData.ScanData.Buffer] = None, metadata: List[MF.V3.Settings.ScanData.ScanData.Metadata] = None) -> MF.V3.Task.Task: ... + def set_cameras(self, selection: List[int] = None, autoExposure: bool = None, exposure: int = None, analogGain: float = None, digitalGain: int = None, focus: int = None) -> MF.V3.Task.Task: ... + def set_group(self, index: int, name: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> MF.V3.Task.Task: ... + def set_project(self, index: int = None, name: str = None) -> MF.V3.Task.Task: ... + def set_projector(self, on: bool = None, brightness: float = None, pattern: MF.V3.Settings.Projector.Projector.Pattern = None, image: MF.V3.Settings.Projector.Projector.Image = None, color: List[float] = None, buffer: bytes = None) -> MF.V3.Task.Task: ... + def shutdown(self) -> MF.V3.Task.Task: ... + def split_group(self, Input: int) -> MF.V3.Task.Task: ... + def start_video(self) -> MF.V3.Task.Task: ... + def stop_video(self) -> MF.V3.Task.Task: ... + def system_info(self, updateMajor: bool = None, updateNightly: bool = None) -> MF.V3.Task.Task: ... + def transform_group(self, index: int, name: str = None, color: List[float] = None, visible: bool = None, collapsed: bool = None, rotation: List[float] = None, translation: List[float] = None) -> MF.V3.Task.Task: ... + def turntable_calibration(self) -> MF.V3.Task.Task: ... + def update_settings(self, advanced: MF.V3.Settings.Advanced.Advanced = None, camera: MF.V3.Settings.Camera.Camera = None, capture: MF.V3.Settings.Capture.Capture = None, i18n: MF.V3.Settings.I18n.I18n = None, projector: MF.V3.Settings.Projector.Projector = None, style: MF.V3.Settings.Style.Style = None, turntable: MF.V3.Settings.Turntable.Turntable = None, tutorials: MF.V3.Settings.Tutorials.Tutorials = None, viewer: MF.V3.Settings.Viewer.Viewer = None, software: MF.V3.Settings.Software.Software = None) -> MF.V3.Task.Task: ... + def upload_project(self, buffer: bytes) -> MF.V3.Task.Task: ... \ No newline at end of file diff --git a/three/serialization.py b/three/serialization.py new file mode 100644 index 0000000..ec322ae --- /dev/null +++ b/three/serialization.py @@ -0,0 +1,65 @@ +# import logging +from array import array +import json +from google.protobuf.json_format import MessageToDict +from enum import Enum + +# Configure logging +# logging.basicConfig(level=logging.DEBUG) +# logger = logging.getLogger(__name__) + +def Serializer(object, visited=None): + if visited is None: + visited = set() + + # logger.debug(f"Serializing object: {object} (id: {id(object)})") + + # Only track objects for circular references + if isinstance(object, (dict, list, set, tuple, Enum)) or hasattr(object, '__dict__'): + if id(object) in visited: + return None + visited.add(id(object)) + + # Handle enums + if isinstance(object, Enum): + return object.name + + # Array is not JSON serializable => Convert to list + if isinstance(object, array): + return object.tolist() + + # Protobuf object + if hasattr(object, "DESCRIPTOR"): + dic = MessageToDict( + object, + preserving_proto_field_name=True, + including_default_value_fields=True + ) + return dict(filter(lambda tup: tup[1] is not None, dic.items())) + + # Our objects + if hasattr(object, '__dict__'): + dic = dict(filter(lambda tup: tup[1] is not None, object.__dict__.items())) + for key, value in dic.items(): + dic[key] = Serializer(value, visited) + return dic + + # Handle other types if necessary + return object + +def TO_JSON(object) -> str: + """ + Serialize an object into a json string. + + Args: + object: the object to serialize. + + Returns: + The string representing the object. + + """ + return json.dumps( + object, + default=Serializer, + allow_nan=False + ) \ No newline at end of file diff --git a/maf_three/tests/test_proto_camera.py b/three/tests/test_proto_camera.py similarity index 100% rename from maf_three/tests/test_proto_camera.py rename to three/tests/test_proto_camera.py diff --git a/maf_three/tests/test_proto_turntable.py b/three/tests/test_proto_turntable.py similarity index 100% rename from maf_three/tests/test_proto_turntable.py rename to three/tests/test_proto_turntable.py