Compare commits

...

52 Commits

Author SHA1 Message Date
Alper S. Soylu 4c381bd809 fix send stream object cases (#207)
* fix send stream object cases

* clean logs

* revert workaround

* revert unnecessary logic

* revert unnecessary logic

* rename set_user

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-08-05 13:36:06 +01:00
Alper S. Soylu 8c3885ece8 Lazy loading for stream branches & commits (#200)
* only fetch branches on reload

* initial load

* load on stream selection

* refactor get_item_by_index

* fix resetting commit selection on default branch

* refactor load_stream_branches

* clean logs

* explicit return

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2024-07-29 12:15:33 +01:00
Alper S. Soylu 15bd3f5070 fix resetting commit selection on default branch (#206)
Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-28 23:22:07 +01:00
Jedd Morgan 6fd4571d34 update macos images (#203) 2024-07-16 19:42:34 +02:00
Jedd Morgan 5081177653 Small tweaks to installer (#202)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

* small tweaks

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:23:36 +01:00
Alper S. Soylu 0d0ca2c811 Skip installing dependencies after initial install (#199)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:18:40 +01:00
Jedd Morgan 230e27a162 Merge pull request #196 from overengineer/fix/resetting_selected_branch_workaround
Recover branch selection
2024-06-05 14:54:21 +01:00
Jedd Morgan 669fd19c2e Type hint polish 2024-06-05 14:52:32 +01:00
Soylu 139f8ccb33 type hints 2024-06-03 19:10:54 +03:00
Soylu 7f625bd468 check None item 2024-05-30 18:11:29 +03:00
Soylu 4d07ba7637 use object ids 2024-05-30 16:53:53 +03:00
Jedd Morgan 7431b57e0e Merge pull request #198 from specklesystems/jrm/bump-deps
Updated deps
2024-05-27 16:34:39 +01:00
Jedd Morgan 57c19ba3c5 Updated deps 2024-05-27 16:34:07 +01:00
Jedd Morgan 8e3c2ece2f Merge pull request #197 from overengineer/fix/slow_startup
Speedup install dependencies
2024-05-24 16:48:29 +01:00
Soylu cfc58d9456 speedup install dependencies 2024-05-21 19:54:09 +03:00
Soylu e74a6cebb1 boundary check 2024-05-19 23:24:13 +03:00
Soylu 5e01d5a976 Recover branch selection 2024-05-16 18:19:36 +03:00
Jedd Morgan a2f7ab422f Merge pull request #194 from specklesystems/dev
2.19 changes
2024-05-14 17:41:21 +01:00
Jedd Morgan 8c58d9d14c Dev (#192)
* feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)

* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls

* Updated naming of revit elements to include family type (#193)
2024-05-14 17:38:50 +01:00
Jedd Morgan 90e61b6dc1 Updated naming of revit elements to include family type (#193) 2024-05-01 21:14:52 +01:00
Jedd Morgan 5c479e4c0e Merge branch 'main' into dev 2024-04-11 13:32:06 +01:00
Jedd Morgan 97d20ad7b1 Ci tweaks (#191) 2024-03-14 11:09:48 +00:00
Jedd Morgan 2800b84747 Merge pull request #190 from specklesystems/main
BACK MERGE MAIN -> DEV
2024-03-11 16:18:25 +00:00
Jedd Morgan 511d69314e feat(ci): [CNX-9125] Update to digicert-keylocker (#189)
* feat(ci): Update to digicert-keylocker

* removed pem
2024-03-11 17:14:36 +01:00
Jedd Morgan 24e7f02213 feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls
2024-02-28 18:53:11 +00:00
Jedd Morgan c1d7947085 Merge pull request #188 from specklesystems/main
deprecated delete stream (#187)
2024-02-28 16:46:37 +00:00
Jedd Morgan 21281e5d77 deprecated delete stream (#187) 2024-02-28 16:46:08 +00:00
Jedd Morgan 29bbdc69a2 chore(ci): CNX-9051 Update ci signing (#185)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Update ci signing

* Update config.yml

* Bump Deps

* powershell

* More powershell
2024-02-27 11:29:33 +00:00
Jedd Morgan efe6e6a4a0 2.18 Update (#183)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports
2024-02-26 17:32:38 +00:00
Jedd Morgan f036109020 Increased default branch get to 100 limit, and added as mesh conversi… (#181)
* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock
2024-02-07 18:05:00 +00:00
Jedd Morgan 86bc2dc590 Merge pull request #180 from specklesystems/jrm/update/2.17
chore(deps): lock deps
2023-11-29 11:57:35 +00:00
Jedd Morgan a34b6ad0c2 lock + readme 2023-11-29 11:56:50 +00:00
Jedd Morgan e436949ef9 Merge pull request #179 from specklesystems/jrm/4.0/material-support
feat(4.X): Added support for Blender 4.X material nodes
2023-11-11 20:18:37 +00:00
Jedd Morgan 6d8f4a4a80 Added support for Blender 4.X BSDF materials 2023-11-11 20:16:49 +00:00
Jedd Morgan dabb65427a Added defaults to converter settings so converter can be used without connector 2023-10-18 15:48:31 +01:00
Jedd Morgan 57ece17e8b Merge pull request #177 from specklesystems/jrm/chore/comments
chore: fixed some mistakes in code comments
2023-10-16 11:43:58 +01:00
Jedd Morgan 4362f737d0 chore: fixed some mistakes in code comments 2023-10-16 11:43:36 +01:00
Jedd Morgan b55df58313 Merge pull request #176 from specklesystems/jrm/blender/automate
Extracted some functions for automate
2023-10-13 13:02:28 +01:00
Jedd Morgan afa6722253 Extracted some functions for automate 2023-10-13 12:56:30 +01:00
Jedd Morgan a3d4881578 Merge pull request #175 from specklesystems/jrm/properties/depth
fix(custom_properties): Set max depth of properties to 64 to align with newtonsoft limit
2023-10-08 18:45:50 +01:00
JR-Morgan 1af158a5e0 poetry lock 2023-10-08 18:44:18 +01:00
JR-Morgan 47857a9db0 Aligned json depth cap with newtonsofts reader at 64 levels of depth 2023-10-08 18:40:43 +01:00
Jedd Morgan 3b026e6027 Merge pull request #173 from specklesystems/jrm/fix-py-indent-error
Update users.py
2023-09-22 13:03:32 +01:00
Jedd Morgan d572609f75 Update users.py 2023-09-22 13:03:06 +01:00
Jedd Morgan 37032cc7aa Merge pull request #172 from specklesystems/jrm/deps/specklepy216
better error message when user has no valid accounts
2023-09-12 12:35:03 +01:00
Jedd Morgan 6027325878 better error message when user has no valid accounts 2023-09-12 11:59:39 +01:00
Jedd Morgan 5ddb2aa052 Merge pull request #171 from specklesystems/jrm/deps/specklepy216
fix(receive)!: Fixed issue with collection name conflicts
2023-09-12 10:50:15 +01:00
Jedd Morgan 67a18821cc fix(receive)!: Collections will no longer update 2023-09-12 10:36:22 +01:00
Jedd Morgan 2688a69286 Merge pull request #169 from specklesystems/jrm/deps/specklepy216
Jrm/deps/specklepy216
2023-09-08 16:04:08 +01:00
Jedd Morgan 56216a6137 bump specklepy 2023-09-08 15:39:25 +01:00
Jedd Morgan 319cbf8960 Specklepy 2.16 2023-09-08 13:41:14 +01:00
Jedd Morgan d7ac6c0b95 Updated to new core metrics 2023-09-04 19:51:39 +01:00
25 changed files with 1962 additions and 1487 deletions
+39 -15
View File
@@ -71,24 +71,44 @@ jobs:
build-installer-win:
executor:
name: win/default
shell: cmd.exe
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
shell: powershell.exe
command: python patch_installer.py (Get-Content -Raw SEMVER)
- run:
name: Create Innosetup signing cert
shell: powershell.exe
command: |
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
- run:
name: Installer
shell: cmd.exe #does not work in powershell
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
- unless: # Build installers unsigned on non-tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
shell: cmd.exe #does not work in powershell
- when: # Setup certificates and build installers signed for tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: "Digicert Signing Manager Setup"
command: |
cd C:\
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
- run:
name: Create Auth & OV Signing Cert
command: |
cd C:\
echo $env:SM_CLIENT_CERT_FILE_B64 > certificate.txt
certutil -decode certificate.txt certificate.p12
- run:
name: Sync Certs
command: |
& $env:SSM\smksp_cert_sync.exe
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p /DSIGN_INSTALLER /DCODE_SIGNING_CERT_FINGERPRINT=%SM_CODE_SIGNING_CERT_SHA1_HASH%
shell: cmd.exe #does not work in powershell
- persist_to_workspace:
root: ./
paths:
@@ -96,7 +116,8 @@ jobs:
build-installer-mac:
macos:
xcode: 12.5.1
xcode: 13.4.1
resource_class: macos.m1.medium.gen1
parameters:
runtime:
type: string
@@ -109,6 +130,9 @@ jobs:
- checkout
- attach_workspace:
at: ./
- run:
name: Exit if External PR
command: if [ "$CIRCLE_PR_REPONAME" ]; then circleci-agent step halt; fi
- run:
name: Install mono
command: |
@@ -214,7 +238,7 @@ workflows:
filters: *build_filters
- build-installer-win:
context: innosetup
context: digicert-keylocker
name: Windows Installer Build
requires:
- package-connector
@@ -304,4 +328,4 @@ workflows:
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
filters: *deploy_filters
+35 -20
View File
@@ -41,42 +41,57 @@ Give Speckle a try in no time by:
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
# Repo structure
# Blender Connector
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
## Disclaimer
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
## Installation
1. Place `bpy_speckle` folder in your `addons` folder. On Windows this is typically `%APPDATA%/Blender Foundation/Blender/2.80/scripts/addons`.
2. Go to `Edit->Preferences` (Ctrl + Alt + U)
3. Go to the `Add-ons` tab
4. Find and enable `SpeckleBlender 2.0` in the `Scene` category. <!-- **If enabling for the first time, expect the UI to freeze for bit while it silently installs all the dependencies.** -->
5. The Speckle UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
We officially support Blender 3.3 and newer, on Windows and Mac.
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
- Select a branch and commit from the dropdown menus.
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
- Click on `Open Stream in Web` to view the stream in your web browser.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Projects` list with available projects for the selected user account.
- Select a model and version from the dropdown menus.
- Click on `Receive` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection, named `<PROJECT_NAME> [ <MODEL_NAME> @ <VERSION_ID> ]`.
- Click on `Open Model in Web` to view the model in your web browser.
## Caveats
## Supported Elements
- Mesh objects are supported. Breps are imported as meshes using their `displayValue` data.
- Curves have limited support: `Polylines` are supported; `NurbsCurves` are supported, though they are not guaranteed to look the same; `Lines` are supported; `Arcs` are not supported, though they are very roughly approximated; `PolyCurves` are supported for linear / polyline segments and very approximate arc segments. These conversions are a point of focus for further development.
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
## Custom properties
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
## Additional Features
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Receiving vertex colors is supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows and `~/.config/Speckle/connector_installations` on Mac.
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
As such, an internet connection is required for first launch of the connector.
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
If you suspect you're seeing a conflict, Please uninstall other third party addons one at a time to identify which addon is conflicting.
If you find an addon that conflicts, please try using a different version of that addon (newer or older).
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
## Contributing
+14 -14
View File
@@ -3,29 +3,29 @@ import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
from bpy_speckle.specklepy_extras.commit_object_builder import CommitObjectBuilder, ROOT
from specklepy.objects.graph_traversal.commit_object_builder import CommitObjectBuilder, ROOT
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from attrs import define
ELEMENTS = "elements"
def _id(natvive_object: ID) -> str:
def _id(native_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(natvive_object).__name__}:{natvive_object.name_full}"
return f"{type(native_object).__name__}:{native_object.name_full}"
def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(natvive_object) if natvive_object else None
def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(native_object) if native_object else None
def convert_collection_to_speckle(col: Collection) -> SCollection:
convered_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
convered_collection.applicationId = _id(col)
converted_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
converted_collection.applicationId = _id(col)
color_tag = col.color_tag
if color_tag and color_tag != "NONE":
convered_collection["colorTag"] = col.color_tag
converted_collection["colorTag"] = col.color_tag
return convered_collection
return converted_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
@@ -41,7 +41,7 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
parent_collections = native_object.users_collection
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
app_id = _id(native_object)
@@ -67,11 +67,11 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
convered_collection = convert_collection_to_speckle(col)
self.converted[id] = convered_collection
self._collections[id] = convered_collection
converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection
self._collections[id] = converted_collection
return convered_collection
return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.api.client import SpeckleClient
from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
+2 -1
View File
@@ -18,4 +18,5 @@ ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SEPERATOR = " -- "
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
+88 -58
View File
@@ -1,6 +1,6 @@
import math
from typing import Any, Dict, Iterable, List, Optional, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
from bpy_speckle.convert.util import ConversionSkippedException
from mathutils import (
@@ -20,7 +20,7 @@ from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycur
from bpy.types import Object, Collection as BCollection
from .util import (
add_to_heirarchy,
add_to_hierarchy,
get_render_material,
get_vertex_color_material,
render_material_to_native,
@@ -40,7 +40,7 @@ CAN_CONVERT_TO_NATIVE = (
)
def _has_native_convesion(speckle_object: Base) -> bool:
def _has_native_conversion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack
def _has_fallback_conversion(speckle_object: Base) -> bool:
@@ -48,28 +48,11 @@ def _has_fallback_conversion(speckle_object: Base) -> bool:
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
return False
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
convert_instances_as: str = "" #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
@@ -87,7 +70,7 @@ def convert_to_native(speckle_object: Base) -> Object:
children: list[Object] = []
# convert elements/breps
if not _has_native_convesion(speckle_object):
if not _has_native_conversion(speckle_object):
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
if not converted and not children:
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
@@ -150,8 +133,8 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
@@ -163,11 +146,11 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
separate(x)
return False
did_halt = seperate(display)
did_halt = separate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
@@ -198,12 +181,15 @@ def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam = bpy.data.cameras.new(name=name)
native_cam.lens = 18 # 90° horizontal fov
if not hasattr(speckle_view, "origin"):
raise ConversionSkippedException("2D views not supported")
cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale)
tx = (speckle_view.origin.x * scale_factor)
ty = (speckle_view.origin.y * scale_factor)
tz = (speckle_view.origin.z * scale_factor) #TODO: do these need to be scaled?
tz = (speckle_view.origin.z * scale_factor)
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
@@ -532,7 +518,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the ployline displayValue
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the polyline displayValue
icurve_to_native_spline(speckle_curve, blender_curve, scale)
@@ -540,7 +526,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
"""
Transforms and Intances
Transforms and Instances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
@@ -554,7 +540,7 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
)
# scale the translation
for i in range(3):
mat[i][3] *= scale # type: ignore
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
@@ -583,7 +569,7 @@ def _get_instance_name(instance: Instance) -> str:
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Object:
@@ -602,12 +588,12 @@ def instance_to_native_object(instance: Instance, scale: float) -> Object:
traversal_root: Base = definition
if not can_convert_to_native(definition):
# Non-convertable (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# so we explicitly convert them as empties.
native_instance = create_new_object(None, name)
native_instance.empty_display_size = 0
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertable
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition
@@ -649,7 +635,7 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->
instance_transform = transform_to_native(instance.transform, scale)
native_instance = bpy.data.objects.new(name, None)
native_instance = create_new_object(None, name)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
@@ -669,11 +655,11 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
if native_def:
return native_def
native_def = bpy.data.collections.new(name)
native_def = create_new_collection(name)
native_def["applicationId"] = definition.applicationId
converted_objects = {}
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertable
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
dummyRoot = Base(elements=definition, id="__ROOT")
_deep_conversion(dummyRoot, converted_objects, True)
@@ -706,7 +692,7 @@ def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCol
converted_objects[current.id] = converted
add_to_heirarchy(converted, item, converted_objects, preserve_transform)
add_to_hierarchy(converted, item, converted_objects, preserve_transform)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
@@ -725,43 +711,87 @@ def collection_to_native(collection: SCollection) -> BCollection:
return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
existing = cast(BCollection, bpy.data.collections.get(name))
if existing:
if clear_collection:
for obj in existing.objects:
existing.objects.unlink(obj)
return existing
else:
new_collection = bpy.data.collections.new(name)
#Disabled for now, since update mode needs rescoping.
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing:
# if clear_collection:
# for obj in existing.objects:
# existing.objects.unlink(obj)
# return existing
# else:
new_collection = create_new_collection(name)
#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
return new_collection
return new_collection
"""
Object Naming
Object Naming and Creation
"""
def create_new_collection( desired_name: str) -> bpy.types.Collection:
"""
Creates a new blender collection with a unique name
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.collections.keys())
blender_collection = bpy.data.collections.new(name)
return blender_collection
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.objects.keys())
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
"""
Using Blenders default naming (append numeral in .xxx format) to avoid name conflicts with taken names
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names:
#Name already taken, increment counter and try again!
return _make_unique_name(desired_name, taken_names, counter + 1)
return name
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
or _get_revit_family_name(speckle_object)
)
def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
family = getattr(speckle_object, "family", None)
family_type = getattr(speckle_object, "type", None)
if family and family_type:
return f"{family_type}-{family}"
else:
return None
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
return name[:MAX_NAME_LENGTH]
@@ -777,7 +807,7 @@ def _generate_object_name(speckle_object: Base) -> str:
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
+35 -26
View File
@@ -20,7 +20,7 @@ from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
@@ -34,7 +34,7 @@ from bpy_speckle.functions import _report
Units: str = "m" # The desired final units to send
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA")
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA", "FONT", "SURFACE", "META")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
@@ -69,6 +69,8 @@ def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: st
converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA":
converted = camera_to_speckle_view(blender_object, cast(NCamera, blender_object.data))
elif blender_type == "FONT" or "SURFACE" or "META":
converted = anything_to_speckle_mesh(blender_object)
if not converted:
raise Exception("Conversion returned None")
@@ -99,7 +101,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices] # type: ignore
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
@@ -107,7 +109,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indicies to their new index in m_verts
#Loop through each polygon, and map indices to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
@@ -176,8 +178,8 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
num_points = len(points)
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
@@ -192,7 +194,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
rational=True,
@@ -219,15 +221,15 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
@@ -237,7 +239,7 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=weights,
knots=knots,
rational=is_rational,
@@ -305,27 +307,27 @@ def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SEPERATOR)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SEPERATOR in blender_object.name
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SEPERATOR, 1)[0]
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattend_points),
value=list(flattened_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -370,6 +372,12 @@ def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) ->
return (meshes, curves)
def anything_to_speckle_mesh(blender_object: Object) -> Base:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh())
blender_object.to_mesh_clear()
return mesh
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
@@ -410,8 +418,10 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value) # type: ignore
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value) # type: ignore
speckle_mat.emissive = to_argb_int(inputs[emission_color].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
@@ -435,8 +445,8 @@ def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = matrix.col[1].xyz # type: ignore
forwards = -matrix.col[2].xyz # type: ignore
up = cast(MVector, matrix.col[1].xyz)
forwards = cast(MVector, -matrix.col[2].xyz)
translation = matrix.translation
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
@@ -466,7 +476,7 @@ def vector_to_speckle(xyz: MVector) -> Vector:
)
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are itterable, even if type hinting says they are not
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
# scale the translation
for i in (3, 7, 11):
@@ -515,14 +525,13 @@ def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
# Empty -> Block
return block_instance_to_speckle(blender_object)
else:
#raise ConversionSkippedException("Sending non-collection instance empties are not currently supported")
# Empty -> Point
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
return wrapper
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likly other apps) don't support a pont with "elements"
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
+17 -13
View File
@@ -8,9 +8,9 @@ from specklepy.objects.geometry import Mesh
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from bpy_speckle.functions import _report
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
from bpy_speckle.specklepy_extras.traversal import TraversalContext
from specklepy.objects.graph_traversal.traversal import TraversalContext
class ConversionSkippedException(Exception):
pass
@@ -88,11 +88,14 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive) # type: ignore
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
# Blender >=4.0 use "Emission Color"
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
inputs[emission_color].default_value = to_rgba(speckle_mat.emissive) # type: ignore
if speckle_mat.opacity < 1.0:
blender_mat.blend_method = "BLEND"
@@ -165,7 +168,7 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materia
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]] # type: ignore
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
)
f.material_index = materialIndex
f.smooth = smooth
@@ -195,10 +198,10 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
)
# Make vertex colors
if len(scolors) == len(blender_mesh.verts): # type: ignore
if len(scolors) == len(blender_mesh.verts):
color_layer = blender_mesh.loops.layers.color.new("Col")
for face in blender_mesh.faces: # type: ignore
for face in blender_mesh.faces:
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
@@ -217,21 +220,21 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
try:
uv = []
if len(s_uvs) // 2 == len(blender_mesh.verts): # type: ignore
if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}" # type: ignore
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
)
return
# Make UVs
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces: # type: ignore
for f in blender_mesh.faces:
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
@@ -254,8 +257,9 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 200):
if max_depth < 0:
def get_blender_custom_properties(obj, max_depth: int = 63):
"""Recursively grabs custom properties on blender objects. Max depth is determined by the max allowed by Newtonsoft.NET, don't exceed unless you know what you're doing"""
if max_depth <= 0:
return obj
if hasattr(obj, "keys"):
@@ -446,10 +450,10 @@ def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: #type: ignore
col.objects.link(obj)
for child in obj.children: #type: ignore
for child in obj.children:
link_object_to_collection_nested(child, col)
def add_to_heirarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
+8 -39
View File
@@ -2,35 +2,11 @@ from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
from bpy_speckle.specklepy_extras.traversal import GraphTraversal, TraversalRule
"""
Speckle functions
"""
UNIT_SCALE = {
"meters": 1.0,
"centimeters": 0.01,
"millimeters": 0.001,
"inches": 0.0254,
"feet": 0.3048,
"kilometers": 1000.0,
"mm": 0.001,
"cm": 0.01,
"m": 1.0,
"km": 1000.0,
"in": 0.0254,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.340,
}
"""
Utility functions
"""
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
def _report(msg):
def _report(msg: object) -> None:
"""
Function for printing messages to the console
"""
@@ -38,15 +14,8 @@ def _report(msg):
def get_scale_length(units: str) -> float:
if units.lower() in UNIT_SCALE.keys():
return UNIT_SCALE[units]
_report("Units <{}> are not supported.".format(units))
return 1.0
"""
Client, user, and stream functions
"""
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
@@ -56,13 +25,13 @@ def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) ->
ignore_rule = TraversalRule(
[
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is nessasary to avoid double conversion...
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits
],
lambda _: [],
)
convertable_rule = TraversalRule(
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
@@ -73,4 +42,4 @@ def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) ->
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return GraphTraversal([ignore_rule, convertable_rule, default_rule])
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
+25 -4
View File
@@ -138,7 +138,6 @@ def ensure_pip() -> None:
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
return path
@@ -147,29 +146,51 @@ def install_requirements(host_application: str) -> None:
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
def debugger_is_active() -> bool:
"""Return if the debugger is currently active"""
return hasattr(sys, 'gettrace') and sys.gettrace() is not None
requirements_path = get_requirements_path()
is_debug = debugger_is_active()
if not is_debug and not requirements_path.exists():
print("Skipped installing dependencies")
return
print(f"Installing Speckle dependencies to {path}")
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"-q",
"--disable-pip-version-check",
"install",
"--prefer-binary",
"--ignore-installed",
"--no-compile",
"-t",
str(path),
"-r",
str(get_requirements_path()),
str(requirements_path),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
print("Successfully installed dependencies")
if not is_debug:
requirements_path.unlink()
def install_dependencies(host_application: str) -> None:
+2
View File
@@ -20,6 +20,7 @@ from .streams import (
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
@@ -33,6 +34,7 @@ operator_classes = [
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
]
operator_classes.extend([DeleteCommit])
+24 -17
View File
@@ -4,24 +4,26 @@ Commit operators
import bpy
from bpy.props import BoolProperty
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator):
"""
Deletes the selected commit from the selected stream.
Permanently deletes the selected version from the selected model.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
bl_label = "Delete commit"
bl_label = "Delete Version"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete active commit permanently"
bl_description = "Permanently Deletes the selected version from the selected model"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
)
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -37,29 +39,34 @@ class DeleteCommit(bpy.types.Operator):
return {"CANCELLED"}
def execute(self, context):
try:
self.delete_commit(context)
return {"FINISHED"}
except Exception as ex:
print(f"{self.bl_idname}: failed: {ex}")
return {"CANCELLED"}
def delete_commit(self, context: bpy.types.Context) -> None:
if not self.are_you_sure:
raise Exception("Cancelled by user")
_report("Cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_commit(context)
return {"FINISHED"}
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
(_, stream, _, commit) = speckle.validate_commit_selection()
(_, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_commit"
},
)
if not deleted:
raise Exception("Delete operation failed")
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
print(f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})")
+39 -9
View File
@@ -1,35 +1,65 @@
import bpy
import webbrowser
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Guide"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Browse the documentation on the Speckle Guide"
_guide_url = "https://speckle.guide/user/blender.html"
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Docs"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
def execute(self, context):
webbrowser.open("https://speckle.guide/user/blender.html")
webbrowser.open(self._guide_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
)
return {"FINISHED"}
class OpenSpeckleTutorials(bpy.types.Operator):
_tutorials_url = "https://speckle.systems/tutorials/"
bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Visit our tutorials portal for learning resources"
bl_description = f"Visit our tutorials portal for learning resources ({_tutorials_url})"
def execute(self, context):
webbrowser.open("https://speckle.systems/tutorials/")
webbrowser.open(self._tutorials_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
)
return {"FINISHED"}
class OpenSpeckleForum(bpy.types.Operator):
_forum_url = "https://speckle.community/"
bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Ask questions and join the discussion on our community forum"
bl_description = f"Ask questions and join the discussion on our community forum ({_forum_url})"
def execute(self, context):
webbrowser.open("https://speckle.community/")
webbrowser.open(self._forum_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
)
return {"FINISHED"}
+59 -11
View File
@@ -11,8 +11,9 @@ from bpy_speckle.convert.to_speckle import (
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
from specklepy.logging import metrics
@deprecated
class UpdateObject(bpy.types.Operator):
"""
Update local (receive) or remote (send) object depending on
@@ -21,7 +22,7 @@ class UpdateObject(bpy.types.Operator):
"""
bl_idname = "speckle.update_object"
bl_label = "Update Object"
bl_label = "Update Object (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
client = None
@@ -56,19 +57,26 @@ class UpdateObject(bpy.types.Operator):
_report("Updating object {}".format(sm["_id"]))
client.objects.update(active.speckle.object_id, sm)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UpdateObject"
},
)
return {"FINISHED"}
return {"CANCELLED"}
return {"CANCELLED"}
@deprecated
class ResetObject(bpy.types.Operator):
"""
Reset Speckle object settings
"""
bl_idname = "speckle.reset_object"
bl_label = "Reset Object"
bl_label = "Reset Object (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -79,16 +87,24 @@ class ResetObject(bpy.types.Operator):
context.object.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetObject"
},
)
return {"FINISHED"}
@deprecated
class DeleteObject(bpy.types.Operator):
"""
Delete object from the server and update relevant stream
"""
bl_idname = "speckle.delete_object"
bl_label = "Delete Object"
bl_label = "Delete Object (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -125,6 +141,14 @@ class DeleteObject(bpy.types.Operator):
active.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "DeleteObject"
},
)
return {"FINISHED"}
@deprecated
@@ -135,7 +159,7 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
"""
bl_idname = "speckle.upload_ngons_as_polylines"
bl_label = "Upload Ngons As Polylines"
bl_label = "Upload Ngons As Polylines (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
clear_stream: BoolProperty(
@@ -197,6 +221,13 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
context.view_layer.update()
_report("Done.")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UploadNgonsAsPolylines"
},
)
return {"FINISHED"}
def invoke(self, context, event):
@@ -217,7 +248,7 @@ def get_custom_speckle_props(self, context):
return [(x, "{}".format(x), "") for x in active.keys()]
@deprecated
class SelectIfSameCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
@@ -225,7 +256,7 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
"""
bl_idname = "speckle.select_if_same_custom_props"
bl_label = "Select Identical Custom Props"
bl_label = "Select Identical Custom Props (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
@@ -267,9 +298,17 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
)
return {"FINISHED"}
@deprecated
class SelectIfHasCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
@@ -277,7 +316,7 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
"""
bl_idname = "speckle.select_if_has_custom_props"
bl_label = "Select Same Custom Prop"
bl_label = "Select Same Custom Prop (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
@@ -315,4 +354,13 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
)
return {"FINISHED"}
+347 -193
View File
@@ -2,7 +2,7 @@
Stream operators
"""
from math import radians
from typing import Callable, Dict, Optional, Union, cast
from typing import Callable, Dict, Optional, Tuple, Union, cast
import webbrowser
import bpy
from bpy.props import (
@@ -15,6 +15,7 @@ from bpy.types import (
Object,
Collection
)
from deprecated import deprecated
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.to_native import (
can_convert_to_native,
@@ -31,13 +32,13 @@ from bpy_speckle.functions import (
get_scale_length,
)
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleUserObject, get_speckle
from bpy_speckle.convert.util import ConversionSkippedException, add_to_heirarchy
from specklepy.api.models import Commit
from specklepy.api import operations, host_applications
from specklepy.api.wrapper import StreamWrapper
from specklepy.api.resources.stream import Stream
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, selection_state
from bpy_speckle.convert.util import ConversionSkippedException, add_to_hierarchy
from specklepy.core.api.models import Commit
from specklepy.core.api import operations, host_applications
from specklepy.core.api.wrapper import StreamWrapper
from specklepy.core.api.resources.stream import Stream
from specklepy.transports.server import ServerTransport
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
@@ -74,25 +75,24 @@ def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, Re
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
Receive objects from selected model version
"""
bl_idname = "speckle.receive_stream_objects"
bl_label = "Download Stream Objects"
bl_label = "Receive"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from active stream"
bl_description = "Receive objects from selected model version"
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
clean_meshes: BoolProperty(name="Clean Meshes", default=False) # type: ignore
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the receive operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances") # type: ignore
def draw(self, context):
@@ -130,26 +130,31 @@ class ReceiveStreamObjects(bpy.types.Operator):
# Reset state to previous (not quite sure if this is 100% necessary)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = None # type: ignore
def execute(self, context):
try:
self.receive(context)
return {"FINISHED"}
except Exception as ex:
_report(f"Failed to receive objects: {type(ex)} {ex}")
return {"CANCELLED"}
self.receive(context)
return {"FINISHED"}
def receive(self, context: Context) -> None:
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = None # type: ignore
speckle = get_speckle(context)
(user, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
# Fetch commit data
commit_object = operations.receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="Received model version from Speckle Blender",
)
metrics.track(
metrics.RECEIVE,
@@ -158,17 +163,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
#"connector_version": "unknown", #TODO
},
)
# Fetch commit data
commit_object = operations._untracked_receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
# Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
@@ -181,7 +179,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
# older commits will have a non-collection root object
# for the sake of consistant behaviour, we will wrap any non-collection commit objects in a collection
# for the sake of consistent behaviour, we will wrap any non-collection commit objects in a collection
if not isinstance(commit_object, SCollection):
dummy_commit_object = SCollection()
dummy_commit_object.elements = [commit_object]
@@ -191,7 +189,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
# ensure commit object has a name if not already
if not commit_object.name:
commit_object.name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
commit_object.name = f"{stream.name} [ {branch.name} @ {commit.id} ]" # Matches Rhino "Create" naming
for item in traversalFunc.traverse(commit_object):
@@ -199,7 +197,8 @@ class ReceiveStreamObjects(bpy.types.Operator):
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id: raise Exception(f"{current} was an invalid speckle object")
if not current or not current.id:
raise Exception(f"{current} was an invalid Speckle object")
#Convert the object!
converted_data_type: str
@@ -221,7 +220,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
converted_objects[current.id] = converted
add_to_heirarchy(converted, item, converted_objects, True)
add_to_hierarchy(converted, item, converted_objects, True)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
@@ -246,19 +245,19 @@ class ReceiveStreamObjects(bpy.types.Operator):
class SendStreamObjects(bpy.types.Operator):
"""
Send stream objects
Send selected objects to selected model
"""
bl_idname = "speckle.send_stream_objects"
bl_label = "Send stream objects"
bl_label = "Send"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to active stream"
bl_description = "Send selected objects to selected model"
apply_modifiers: BoolProperty(name="Apply modifiers", default=True)
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
commit_message: StringProperty(
name="Message",
default="Pushed elements from Blender.",
)
default="Sent elements from Blender.",
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -268,23 +267,22 @@ class SendStreamObjects(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
if len(context.scene.speckle.users) <= 0: return {"CANCELLED"}
speckle = get_speckle(context)
if len(speckle.users) <= 0:
_report("No user accounts")
return {"CANCELLED"}
N = len(context.selected_objects)
if N == 1:
self.commit_message = f"Pushed {N} element from Blender."
self.commit_message = f"Sent {N} element from Blender."
else:
self.commit_message = f"Pushed {N} elements from Blender."
self.commit_message = f"Sent {N} elements from Blender."
return wm.invoke_props_dialog(self)
def execute(self, context):
try:
self.send(context)
return {"FINISHED"}
except Exception as ex:
_report(f"Send failed: {ex}")
return {"CANCELLED"}
self.send(context)
return {"FINISHED"}
def send(self, context: Context) -> None:
@@ -350,6 +348,16 @@ class SendStreamObjects(bpy.types.Operator):
commit_object = commit_builder.ensure_collection(context.scene.collection)
commit_builder.build_commit_object(commit_object)
metrics.track(
metrics.SEND,
client.account,
custom_props={
"branches": len(stream.branches),
#"collaborators": 0, #TODO:
"isMain": branch.name == "main",
},
)
_report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client)
OBJECT_ID = operations.send(
@@ -364,7 +372,18 @@ class SendStreamObjects(bpy.types.Operator):
message=self.commit_message,
source_application="blender",
)
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}")
if client.account.serverInfo.frontend2:
sent_url = f"{user.server_url}/projects/{stream.id}/models/{branch.id}@{COMMIT_ID}"
else:
sent_url = f"{user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}"
_report(f"Commit Created {sent_url}")
selection_state.selected_commit_id = COMMIT_ID
selection_state.selected_branch_id = branch.id
selection_state.selected_stream_id = stream.id
selection_state.selected_user_id = user.id
bpy.ops.speckle.load_user_streams() # refresh loaded commits
context.view_layer.update()
@@ -376,39 +395,67 @@ class SendStreamObjects(bpy.types.Operator):
class ViewStreamDataApi(bpy.types.Operator):
bl_idname = "speckle.view_stream_data_api"
bl_label = "Open Stream in Web"
bl_label = "Open Model in Web"
bl_options = {"REGISTER", "UNDO"}
bl_description = "View the stream in the web browser"
bl_description = "View the selected model in the web browser"
def execute(self, context):
try:
self.view_stream_data_api(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.view_stream_data_api(context)
return {"FINISHED"}
def view_stream_data_api(self, context: Context) -> None:
speckle = get_speckle(context)
(user, stream) = speckle.validate_stream_selection()
url = self._get_url_from_selection(speckle)
_report(f"Opening {url} in web browser")
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
raise Exception("Failed to open stream in browser")
if not webbrowser.open(url, new=2):
raise Exception(f"Failed to open model in browser ({url})")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "view_stream_data_api"
},
)
@staticmethod
def _get_url_from_selection(speckleScene : SpeckleSceneSettings) -> str:
client = speckle_clients[int(speckleScene.active_user)]
(user, stream) = speckleScene.validate_stream_selection()
branch = stream.get_active_branch()
commit = branch.get_active_commit() if branch else None
if client.account.serverInfo.frontend2:
server_url = f"{user.server_url}/projects/{stream.id}/"
if branch:
server_url += f"models/{branch.id}"
if commit:
server_url += f"@{commit.id}"
else:
server_url = f"{user.server_url}/streams/{stream.id}/"
if commit:
server_url += f"commits/{commit.id}"
elif branch:
server_url += f"branches/{branch.name}"
return server_url
class AddStreamFromURL(bpy.types.Operator):
"""
Add / select a stream using its url
Add / select an existing project by providing its URL
"""
bl_idname = "speckle.add_stream_from_url"
bl_label = "Add stream from URL"
bl_label = "Add Project From URL"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an existing stream by providing its URL"
bl_description = "Add / select an existing project by providing its URL"
stream_url: StringProperty(
name="Stream URL", default="https://speckle.xyz/streams/3073b96e86"
)
name="Project URL", default=""
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -424,13 +471,26 @@ class AddStreamFromURL(bpy.types.Operator):
return {"CANCELLED"}
def execute(self, context):
try:
self.add_stream_from_url(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.add_stream_from_url(context)
return {"FINISHED"}
@staticmethod
def _get_or_add_stream(user : SpeckleUserObject, stream : Stream) -> Tuple[int, SpeckleStreamObject]:
index, b_stream = next(
((i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id),
(None, None),
)
if index is not None:
assert(b_stream)
return (index, b_stream)
add_user_stream(user, stream)
return next(
(i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id
)
def add_stream_from_url(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -440,28 +500,20 @@ class AddStreamFromURL(bpy.types.Operator):
None,
)
if user_index is None:
raise Exception("Unable to find user stream server")
raise Exception(f"No user account credentials for {wrapper.host}, have you added your account in Manager?")
speckle.active_user = str(user_index)
user = cast(SpeckleUserObject, speckle.users[user_index])
client = speckle_clients[user_index]
stream = client.stream.get(wrapper.stream_id, branch_limit=20)
stream = client.stream.get(wrapper.stream_id, branch_limit=LoadUserStreams.branch_limit, commit_limit=LoadUserStreams.commits_limit)
if not isinstance(stream, Stream):
raise SpeckleException("Could not get the requested stream")
raise SpeckleException(f"Could not get the requested project {wrapper.stream_id}")
index, b_stream = next(
((i, s) for i, s in enumerate(user.streams) if s.id == stream.id),
(None, None),
)
(index, b_stream) = self._get_or_add_stream(user, stream)
user.active_stream = index
if index is None:
add_user_stream(user, stream)
user.active_stream, b_stream = next(
(i, s) for i, s in enumerate(user.streams) if s.id == stream.id
)
else:
user.active_stream = index
_report(f"Selecting project at index {index} ({b_stream.id} - {b_stream.name})")
if wrapper.branch_name:
b_index = b_stream.branches.find(wrapper.branch_name)
@@ -481,22 +533,30 @@ class AddStreamFromURL(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "add_stream_from_url"
},
)
class CreateStream(bpy.types.Operator):
"""
Create new stream
Create a new Speckle project using the selected user account
"""
bl_idname = "speckle.create_stream"
bl_label = "Create stream"
bl_label = "Create Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create new stream"
bl_description = "Create a new Speckle project using the selected user account"
stream_name: StringProperty(name="Stream name")
stream_name: StringProperty(name="Project name") # type: ignore
stream_description: StringProperty(
name="Stream description", default="This is a Blender stream."
)
name="Project description", default="My new project"
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -513,12 +573,8 @@ class CreateStream(bpy.types.Operator):
return {"CANCELLED"}
def execute(self, context):
try:
self.create_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.create_stream(context)
return {"FINISHED"}
def create_stream(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -541,24 +597,34 @@ class CreateStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "create_stream"
},
)
@deprecated
class DeleteStream(bpy.types.Operator):
"""
Delete stream
Permanently delete the selected project
"""
bl_idname = "speckle.delete_stream"
bl_label = "Delete stream"
bl_label = "Delete Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete selected stream permanently"
bl_description = "Permanently delete the selected project"
are_you_sure: BoolProperty(
name="Confirm",
description="⚠ This action will delete your entire stream permanently ⚠",
default=False,
)
) # type: ignore
delete_collection: BoolProperty(name="Delete collection", default=False)
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
def draw(self, context):
layout = self.layout
@@ -575,19 +641,16 @@ class DeleteStream(bpy.types.Operator):
return {"CANCELLED"}
def execute(self, context):
try:
self.delete_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def delete_stream(self, context: Context) -> None:
if not self.are_you_sure:
raise Exception("Cancled by user")
_report(f"Cancelled by user - are_you_sure was {self.are_you_sure}")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_stream(context, self.delete_collection)
return {"FINISHED"}
@staticmethod
def delete_stream(context: Context, delete_collection: bool) -> None:
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
@@ -595,7 +658,8 @@ class DeleteStream(bpy.types.Operator):
client.stream.delete(id=stream.id)
if self.delete_collection:
if delete_collection:
# This may not work anymore since we changed the collection naming...
col_name = "SpeckleStream_{}_{}".format(stream.name, stream.id)
if col_name in bpy.data.collections:
collection = bpy.data.collections[col_name]
@@ -607,7 +671,165 @@ class DeleteStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_stream"
},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
Select Speckle objects that don't belong to any stream
"""
bl_idname = "speckle.select_orphans"
bl_label = "Select Orphaned Objects (DEPRECATED)"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select Speckle objects that don't belong to any stream"
def draw(self, context):
layout = self.layout
def execute(self, context):
for o in context.scene.objects:
if (
o.speckle.stream_id
and o.speckle.stream_id not in context.scene["speckle_streams"]
):
o.select = True
else:
o.select = False
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
)
return {"FINISHED"}
class CopyStreamId(bpy.types.Operator):
"""
Copy the selected project id to clipboard
"""
bl_idname = "speckle.stream_copy_id"
bl_label = "Copy Project Id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy the selected project id to clipboard"
def execute(self, context):
self.copy_stream_id(context)
return {"FINISHED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
bpy.context.window_manager.clipboard = stream.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_stream_id"
},
)
class CopyCommitId(bpy.types.Operator):
"""
Copy the selected version id to clipboard
"""
bl_idname = "speckle.commit_copy_id"
bl_label = "Copy Version Id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy the selected version id to clipboard"
def execute(self, context):
self.copy_commit_id(context)
return {"FINISHED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, _, commit) = speckle.validate_commit_selection()
bpy.context.window_manager.clipboard = commit.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_commit_id"
},
)
class CopyModelId(bpy.types.Operator):
"""
Copy model id to clipboard
"""
bl_idname = "speckle.model_copy_id"
bl_label = "Copy model id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy model id to clipboard"
def execute(self, context):
self.copy_model_id(context)
return {"FINISHED"}
def copy_model_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
)
@deprecated
class CopyBranchName(bpy.types.Operator):
"""
Copy branch name to clipboard
"""
bl_idname = "speckle.branch_copy_name"
bl_label = "Copy branch name"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy branch name to clipboard"
def execute(self, context):
self.copy_branch_id(context)
return {"FINISHED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.name
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
Select Speckle objects that don't belong to any stream
@@ -632,79 +854,11 @@ class SelectOrphanObjects(bpy.types.Operator):
else:
o.select = False
return {"FINISHED"}
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
)
class CopyStreamId(bpy.types.Operator):
"""
Copy stream ID to clipboard
"""
bl_idname = "speckle.stream_copy_id"
bl_label = "Copy stream ID"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy stream ID to clipboard"
def execute(self, context):
try:
self.copy_stream_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
bpy.context.window_manager.clipboard = stream.id
class CopyCommitId(bpy.types.Operator):
"""
Copy commit ID to clipboard
"""
bl_idname = "speckle.commit_copy_id"
bl_label = "Copy commit ID"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy commit ID to clipboard"
def execute(self, context):
try:
self.copy_commit_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, _, commit) = speckle.validate_commit_selection()
bpy.context.window_manager.clipboard = commit.id
class CopyBranchName(bpy.types.Operator):
"""
Copy branch name to clipboard
"""
bl_idname = "speckle.branch_copy_name"
bl_label = "Copy branch name"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy branch name to clipboard"
def execute(self, context):
try:
self.copy_branch_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.name
return {"FINISHED"}
+121 -80
View File
@@ -1,15 +1,16 @@
"""
User account operators
"""
from typing import cast
from typing import List, cast
import bpy
from bpy.types import Context
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject, get_speckle
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream
from specklepy.api.credentials import get_local_accounts
from bpy_speckle.properties.scene import SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, restore_selection_state
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts, Account
from specklepy.logging import metrics
class ResetUsers(bpy.types.Operator):
"""
@@ -17,12 +18,20 @@ class ResetUsers(bpy.types.Operator):
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_label = "Reset Users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetUsers"
},
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
@@ -37,134 +46,166 @@ class ResetUsers(bpy.types.Operator):
class LoadUsers(bpy.types.Operator):
"""
Load all users from local user database
Loads all user accounts from the credentials in the local database.
See docs to add accounts via Manager
"""
bl_idname = "speckle.users_load"
bl_label = "Load users"
bl_label = "Load Users"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
def execute(self, context):
_report("Loading users...")
speckle = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
users = speckle.users
speckle = get_speckle(context)
users_list = speckle.users
ResetUsers.reset_ui(context)
profiles = get_local_accounts()
active_user_index = 0
for profile in profiles:
user = users.add()
user.server_name = profile.serverInfo.name or "Speckle Server"
user.server_url = profile.serverInfo.url
user.id = profile.userInfo.id
user.name = profile.userInfo.name
user.email = profile.userInfo.email
user.company = profile.userInfo.company or ""
try:
url = profile.serverInfo.url
assert(url)
client = SpeckleClient(
host=url,
use_ssl="https" in url,
)
client.authenticate_with_account(profile)
speckle_clients.append(client)
except Exception as ex:
_report(ex)
users.remove(len(users) - 1)
if profile.isDefault:
active_user_index = len(users) - 1
metrics.track(
"Connector Action",
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
for profile in profiles:
try:
add_user_account(profile, speckle)
except Exception as ex:
_report(f"Failed to authenticate user account {profile.userInfo.email} with server {profile.serverInfo.url}: {ex}")
users_list.remove(len(users_list) - 1)
continue
if profile.isDefault:
active_user_index = len(users_list) - 1
_report(f"Authenticated {len(users_list)}/{len(profiles)} accounts")
if active_user_index < len(users_list):
speckle.active_user = str(active_user_index)
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
if not users_list:
raise Exception("Zero valid user accounts were found, please ensure account is valid and the server is running")
return {"FINISHED"}
def add_user_account(account: Account, speckle: SpeckleSceneSettings) -> SpeckleUserObject:
"""Creates a new new SpeckleUserObject for the provided user Account and adds it to the SpeckleSceneSettings"""
users_list = speckle.users
URL = account.serverInfo.url
user = cast(SpeckleUserObject, users_list.add())
user.server_name = account.serverInfo.name or "Speckle Server"
user.server_url = URL
user.id = account.userInfo.id
user.name = account.userInfo.name
user.email = account.userInfo.email
user.company = account.userInfo.company or ""
assert(URL)
client = SpeckleClient(
host=URL,
use_ssl="https" in URL,
)
client.authenticate_with_account(account)
speckle_clients.append(client)
return user
def add_user_stream(user: SpeckleUserObject, stream: Stream):
s = user.streams.add()
"""Adds the provided Stream (with branch & commits) to the SpeckleUserObject"""
s = cast(SpeckleStreamObject, user.streams.add())
s.name = stream.name
s.id = stream.id
s.description = stream.description
if not stream.branches:
return
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in stream.branches.items:
branch = s.branches.add()
branch.name = b.name
if not b.commits:
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units # type: ignore
else:
s.units = "Meters"
_report(f"Adding stream {s.id} - {s.name}")
if stream.branches:
s.load_stream_branches(stream)
class LoadUserStreams(bpy.types.Operator):
"""
Load all available streams for active user user
(Re)Load all available projects for active user
"""
bl_idname = "speckle.load_user_streams"
bl_label = "Load user streams"
bl_label = "Load User's Projects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)load all available user streams"
bl_description = "(Re)Load all available projects for active user"
stream_limit: int = 20
branch_limit: int = 100
commits_limit: int = 10
def execute(self, context):
try:
self.add_stream_from_url(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.load_user_stream(context)
return {"FINISHED"}
def add_stream_from_url(self, context: Context) -> None:
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
user = speckle.validate_user_selection()
client = speckle_clients[int(speckle.active_user)]
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception(f"Failed to retrieve projects") from ex
if not streams:
_report("Failed to retrieve streams.")
_report("Zero projects found")
return
active_stream_id = None
if active_stream := user.get_active_stream():
active_stream_id = active_stream.id
elif len(user.streams) > 0:
active_stream_id = user.streams[0].id
user.streams.clear()
default_units = "Meters"
for s in streams:
for i, s in enumerate(streams):
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
load_branches = s.id == active_stream_id if active_stream_id else i == 0
if load_branches:
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit, commit_limit=10)
add_user_stream(user, sstream)
else:
add_user_stream(user, s)
restore_selection_state(speckle)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
)
+4 -5
View File
@@ -5,7 +5,7 @@ import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
send_or_receive: bpy.props.EnumProperty(
name="Mode",
@@ -13,7 +13,6 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="")
units: bpy.props.StringProperty(default="")
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
+3 -3
View File
@@ -13,6 +13,6 @@ class SpeckleObjectSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
object_id: bpy.props.StringProperty(default="")
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
+175 -60
View File
@@ -1,77 +1,113 @@
"""
Scene properties
"""
from typing import Optional, Tuple
from typing import Iterable, Optional, Tuple, Union, cast
from dataclasses import dataclass
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
PointerProperty,
)
from bpy_speckle.clients import speckle_clients
from specklepy.core.api.models import Stream
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="") # type: ignore
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="")
message: StringProperty(default="")
author_name: StringProperty(default="")
author_id: StringProperty(default="")
created_at: StringProperty(default="")
source_application: StringProperty(default="")
referenced_object: StringProperty(default="")
id: StringProperty(default="") # type: ignore
message: StringProperty(default="") # type: ignore
author_name: StringProperty(default="") # type: ignore
author_id: StringProperty(default="") # type: ignore
created_at: StringProperty(default="") # type: ignore
source_application: StringProperty(default="") # type: ignore
referenced_object: StringProperty(default="") # type: ignore
class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context):
if self.commits != None and len(self.commits) > 0:
COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
return [
(str(i), commit.id, commit.message, i)
for i, commit in enumerate(self.commits)
for i, commit in enumerate(COMMITS)
]
return [("0", "<none>", "<none>", 0)]
def commit_update_hook(self, context: bpy.types.Context):
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(self.commits, self.commit)
selection_state.selected_branch_id = self.id
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
name: StringProperty(default="main")
commits: CollectionProperty(type=SpeckleCommitObject)
name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
commit: EnumProperty(
name="Commit",
description="Active commit",
name="Version",
description="Selected model version",
items=get_commits,
)
update=commit_update_hook,
) # type: ignore
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
class SpeckleStreamObject(bpy.types.PropertyGroup):
def load_stream_branches(self, sstream: Stream):
self.branches.clear()
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in sstream.branches.items:
branch = cast(SpeckleBranchObject, self.branches.add())
branch.name = b.name
branch.id = b.id
branch.description = b.description or ""
if not b.commits:
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
def get_branches(self, context):
if self.branches:
BRANCHES = cast(Iterable[SpeckleBranchObject], self.branches)
return [
(str(i), branch.name, branch.name, i)
for i, branch in enumerate(self.branches)
(str(i), branch.name, branch.description, i)
for i, branch in enumerate(BRANCHES)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
def branch_update_hook(self, context: bpy.types.Context):
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(self.branches, self.branch)
# print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
name: StringProperty(default="SpeckleStream")
description: StringProperty(default="No description provided.")
id: StringProperty(default="")
units: StringProperty(default="Meters")
query: StringProperty(default="")
branches: CollectionProperty(type=SpeckleBranchObject)
name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
branch: EnumProperty(
name="Branch",
description="Active branch",
name="Model",
description="Selected Model",
items=get_branches,
)
update=branch_update_hook,
) # type: ignore
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
@@ -79,22 +115,35 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
id: StringProperty(default="")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
def fetch_stream_branches(self, context: bpy.types.Context, stream: SpeckleStreamObject):
speckle = context.scene.speckle
client = speckle_clients[int(speckle.active_user)]
sstream = client.stream.get(id=stream.id, branch_limit=100, commit_limit=10) # TODO: refactor magic numbers
stream.load_stream_branches(sstream)
def stream_update_hook(self, context: bpy.types.Context):
stream = SelectionState.get_item_by_index(self.streams, self.active_stream)
selection_state.selected_stream_id = stream.id
# print(f"stream_update_hook: {selection_state.selected_stream_id=}, {selection_state.selected_user_id=}")
if len(stream.branches) == 0: # do not reload on selection, same as the old behavior
self.fetch_stream_branches(context, stream)
server_name: StringProperty(default="SpeckleXYZ") # type: ignore
server_url: StringProperty(default="https://speckle.xyz") # type: ignore
id: StringProperty(default="") # type: ignore
name: StringProperty(default="Speckle User") # type: ignore
email: StringProperty(default="user@speckle.xyz") # type: ignore
company: StringProperty(default="SpeckleSystems") # type: ignore
streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore
active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
selected_index = int(self.active_stream)
if 0 <= selected_index < len(self.streams):
return self.streams[selected_index]
return None
class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context):
@@ -107,51 +156,55 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
name="Available streams",
description="Available streams associated with user.",
items=[],
)
) # type: ignore
users: CollectionProperty(type=SpeckleUserObject)
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users)
return [
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
for i, user in enumerate(self.users)
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
for i, user in enumerate(USERS)
]
def set_user(self, context):
bpy.ops.speckle.load_user_streams()
def user_update_hook(self, context):
bpy.ops.speckle.load_user_streams() # type: ignore
selection_state.selected_user_id = SelectionState.get_item_id_by_index(self.users, self.active_user)
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
update=set_user,
update=user_update_hook,
get=None,
set=None,
)
) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject)
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
scale: FloatProperty(default=0.001)
scale: FloatProperty(default=0.001) # type: ignore
user: StringProperty(
name="User",
description="Current user.",
description="Current user",
default="Speckle User",
)
) # type: ignore
receive_script: EnumProperty(
name="Receive script",
description="Script to run when receiving stream objects.",
description="Custom py script to execute when receiving objects. See docs for function signature.",
items=get_scripts,
)
) # type: ignore
send_script: EnumProperty(
name="Send script",
description="Script to run when sending stream objects.",
description="Custom py script to execute when sending objects. See docs for function signature",
items=get_scripts,
)
) # type: ignore
def get_active_user(self) -> Optional[SpeckleUserObject]:
if self.active_user is None:
return None
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
@@ -161,7 +214,7 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user selected/found")
raise SelectionException("No user account selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
@@ -169,7 +222,7 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
stream = user.get_active_stream()
if not stream:
raise SelectionException("No stream selected/found")
raise SelectionException("No project selected/found")
return (user, stream)
@@ -178,14 +231,14 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No branch selected/found")
raise SelectionException("No model selected/found")
return (user, stream, branch)
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
(user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit()
if commit is None:
raise SelectionException("No commit selected/found")
raise SelectionException("No model version selected/found")
return (user, stream, branch, commit)
@@ -193,4 +246,66 @@ class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
return context.scene.speckle #type: ignore
"""
Gets the speckle scene object
"""
return context.scene.speckle #type: ignore
@dataclass
class SelectionState:
selected_user_id : Optional[str] = None
selected_stream_id : Optional[str] = None
selected_branch_id : Optional[str] = None
selected_commit_id : Optional[str] = None
@staticmethod
def get_item_id_by_index(collection: bpy.types.PropertyGroup, index: Union[str, int]) -> Optional[str]:
if item := SelectionState.get_item_by_index(collection, index):
return item.id
return None
@staticmethod
def get_item_by_index(collection: bpy.types.PropertyGroup, index: Union[str, int]) -> Optional[bpy.types.PropertyGroup]:
items = collection.values()
i = int(index)
if 0 <= i <= len(items):
return items[i]
return None
@staticmethod
def get_item_index_by_id(collection: Iterable[SpeckleCommitObject], id: Optional[str]) -> Optional[str]:
for index, item in enumerate(collection):
if item.id == id:
return str(index)
return None
selection_state = SelectionState()
def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
# Restore branch selection state
if selection_state.selected_branch_id != None:
(active_user, active_stream) = speckle.validate_stream_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
if is_same_user:
active_user.active_stream = int(SelectionState.get_item_index_by_id(active_user.streams, selection_state.selected_stream_id))
active_stream = SelectionState.get_item_by_index(active_user.streams, active_user.active_stream)
if branch := SelectionState.get_item_index_by_id(active_stream.branches, selection_state.selected_branch_id):
active_stream.branch = branch
# Restore commit selection state
if selection_state.selected_commit_id != None:
(active_user, active_stream, active_branch) = speckle.validate_branch_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}, {active_branch.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
is_same_stream = active_stream.id == selection_state.selected_stream_id
is_same_branch = active_branch.id == selection_state.selected_branch_id
if is_same_user and is_same_stream and is_same_branch:
if commit := SelectionState.get_item_index_by_id(active_branch.commits, selection_state.selected_commit_id):
active_branch.commit = commit
@@ -1,83 +0,0 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
from attrs import define
from specklepy.objects.base import Base
ROOT: str = "__Root"
T = TypeVar('T')
PARENT_INFO = Tuple[Optional[str], str]
@define(slots=True)
class CommitObjectBuilder(ABC, Generic[T]):
converted: Dict[str, Base]
_parent_infos: Dict[str, Collection[PARENT_INFO]]
def __init__(self) -> None:
self.converted = {}
self._parent_infos = {}
@abstractmethod
def include_object(self, conversion_result: Base, native_object: T) -> None:
pass
def build_commit_object(self, root_commit_object: Base) -> None:
self.apply_relationships(self.converted.values(), root_commit_object)
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
if not app_id:
return
self._parent_infos[app_id] = parent_info
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
for c in to_add:
try:
self.apply_relationship(c, root_commit_object)
except Exception as ex:
print(f"Failed to add object {type(c)} to commit object: {ex}")
def apply_relationship(self, current: Base, root_commit_object: Base):
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
parents = self._parent_infos[current.applicationId]
for (parent_id, prop_name) in parents:
if not parent_id: continue
parent: Optional[Base]
if parent_id == ROOT:
parent = root_commit_object
else:
parent = self.converted[parent_id] if parent_id in self.converted else None
if not parent: continue
try:
elements = get_detached_prop(parent, prop_name)
if not isinstance(elements, list):
elements = []
set_detached_prop(parent, prop_name, elements)
elements.append(current)
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
return getattr(speckle_object, detached_prop_name, None)
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
setattr(speckle_object, detached_prop_name, value)
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
-122
View File
@@ -1,122 +0,0 @@
from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional, Set
from attrs import define
from typing_extensions import Protocol, final
from specklepy.objects import Base
class ITraversalRule(Protocol):
def get_members_to_traverse(self, o: Base) -> Set[str]:
"""Get the members to traverse."""
pass
def does_rule_hold(self, o: Base) -> bool:
"""Make sure the rule still holds."""
pass
@final
class DefaultRule:
def get_members_to_traverse(self, _) -> Set[str]:
return set()
def does_rule_hold(self, _) -> bool:
return True
# we're creating a local protected "singleton"
_default_rule = DefaultRule()
@final
@define(slots=True, frozen=True)
class TraversalContext:
current: Base
member_name: Optional[str] = None
parent: Optional["TraversalContext"] = None
@final
@define(slots=True, frozen=True)
class GraphTraversal:
_rules: List[ITraversalRule]
def traverse(self, root: Base) -> Iterator[TraversalContext]:
stack: List[TraversalContext] = []
stack.append(TraversalContext(root))
while len(stack) > 0:
head = stack.pop()
yield head
current = head.current
active_rule = self._get_active_rule_or_default_rule(current)
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if getattr(current, child_prop, None):
value = current[child_prop]
self._traverse_member_to_stack(
stack, value, child_prop, head
)
except KeyError as ex:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
def _traverse_member_to_stack(
stack: List[TraversalContext],
value: Any,
member_name: Optional[str] = None,
parent: Optional[TraversalContext] = None,
):
if isinstance(value, Base):
stack.append(TraversalContext(value, member_name, parent))
elif isinstance(value, list):
for obj in value:
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
elif isinstance(value, dict):
for obj in value.values():
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
@staticmethod
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
if isinstance(value, Base):
yield value
elif isinstance(value, list):
for obj in value:
for o in GraphTraversal.traverse_member(obj):
yield o
elif isinstance(value, dict):
for obj in value.values():
for o in GraphTraversal.traverse_member(obj):
yield o
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
return self._get_active_rule(o) or _default_rule
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
for rule in self._rules:
if rule.does_rule_hold(o):
return rule
return None
@final
@define(slots=True, frozen=True)
class TraversalRule:
_conditions: Collection[Callable[[Base], bool]]
_members_to_traverse: Callable[[Base], Iterable[str]]
def get_members_to_traverse(self, o: Base) -> Set[str]:
return set(self._members_to_traverse(o))
def does_rule_hold(self, o: Base) -> bool:
for condition in self._conditions:
if condition(o):
return True
return False
+3 -2
View File
@@ -10,8 +10,9 @@ from bpy.props import (
CollectionProperty,
EnumProperty,
)
from deprecated import deprecated
@deprecated
class OBJECT_PT_speckle(bpy.types.Panel):
bl_space_type = "PROPERTIES"
# bl_idname = 'OBJECT_PT_speckle'
@@ -28,7 +29,7 @@ class OBJECT_PT_speckle(bpy.types.Panel):
layout.active = ob.speckle.enabled
col = layout.column()
col.prop(ob.speckle, "send_or_receive", expand=True)
col.prop(ob.speckle, "stream_id", text="Stream ID")
col.prop(ob.speckle, "stream_id", text="Project ID")
col.prop(ob.speckle, "object_id", text="Object ID")
col.operator("speckle.update_object", text="Update")
col.operator("speckle.reset_object", text="Reset")
+24 -35
View File
@@ -4,20 +4,10 @@ Speckle UI elements for the 3d viewport
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from datetime import datetime
"""
Compatibility
TODO: evaluate if we should still support Blender <2.80
"""
from bpy_speckle.properties.scene import get_speckle
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
@@ -78,7 +68,7 @@ class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
"""
Speckle stream list
Speckle projects list
"""
def draw_item(self, context, layout, data, stream, active_data, active_propname):
@@ -94,7 +84,7 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
layout.label(text="Streams", icon_value=0)
layout.label(text="Projects", icon_value=0)
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
@@ -106,10 +96,10 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "User"
bl_label = "User Account"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
layout = self.layout
col = layout.column()
@@ -119,28 +109,28 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
else:
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text="{} ({})".format(user.server_name, user.server_url))
col.label(text="{} ({})".format(user.name, user.email))
col.label(text=f"{user.server_name} ({user.server_url})")
col.label(text=f"{user.name} ({user.email})")
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle Streams UI panel in the 3d viewport
Speckle projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Streams"
bl_label = "Projects"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
col.label(text="No Projects")
else:
user = speckle.users[int(speckle.active_user)]
col.template_list(
@@ -149,31 +139,31 @@ class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
row = col.row(align=True)
row.operator("speckle.add_stream_from_url", text="", icon="URL")
row.operator("speckle.create_stream", text="", icon="ADD")
row.operator("speckle.delete_stream", text="", icon="REMOVE")
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
"""
Speckle Active Streams UI panel in the 3d viewport
Speckle Active Projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Active stream"
bl_label = "Active Project"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
col.label(text="No projects")
else:
user = speckle.users[int(speckle.active_user)]
user = speckle.validate_user_selection()
#user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active stream.")
col.label(text="No active project")
else:
stream = user.streams[user.active_stream]
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
@@ -183,14 +173,14 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.separator()
row = col.row()
row.prop(stream, "branch", text="")
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
row.prop(stream, "branch", text="Model")
row.operator("speckle.model_copy_id", text="", icon="COPY_ID")
if len(stream.branches) > 0:
branch = stream.branches[int(stream.branch)]
row = col.row()
row.prop(branch, "commit", text="")
row.prop(branch, "commit", text="Version")
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
if len(branch.commits) > 0:
@@ -213,7 +203,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.label(text=f"{commit.author_name} ({commit.author_id})")
col.label(text=commit.source_application)
else:
col.label(text="No branches found!")
col.label(text="No models found!")
col.separator()
@@ -225,7 +215,6 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
subcol = row.column()
subcol.operator("speckle.send_stream_objects", text="Send")
subcol.prop(speckle, "send_script", text="")
area.prop(stream, "query", text="Filter")
col.separator()
@@ -246,7 +235,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
area.separator()
col.separator()
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
col.operator("speckle.view_stream_data_api", text="Open Model in Web")
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
Generated
+893 -671
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -7,18 +7,17 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.15.1"
specklepy = "^2.19.1"
attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
numpy = "^1.23.5"
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
fake-bpy-module-latest = "^20240524"
black = "23.11.0"
pylint = "^2.15.7"
ruff = "^0.0.166"
ruff = "^0.4.4"
[build-system]
requires = ["poetry-core>=1.0.0"]