Compare commits

...

89 Commits

Author SHA1 Message Date
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
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
Jedd Morgan 201ca5f26e Update blender_commit_object_builder.py 2023-07-22 01:02:23 +01:00
Jedd Morgan 89528437b1 Merge pull request #167 from specklesystems/vertex-color-uint
Fixed issue with uint32 vertex colours not parsing
2023-07-17 21:51:18 +01:00
Jedd Morgan 91bde24fe9 Fixed issue with uint32 vertex colors not parsing 2023-07-17 21:21:28 +01:00
Jedd Morgan 991b0f9ff1 Merge pull request #166 from specklesystems/2.15-deps-update
Fixed issue with hosted elements on nested instances not receiving or receiving twice
2023-07-06 15:20:58 +01:00
Jedd Morgan ee1715ff8a fixed circular import 2023-07-06 15:12:34 +01:00
Jedd Morgan 70ee09b9bb Fixed issue with non-convertable revit definitions 2023-07-06 13:57:44 +01:00
Jedd Morgan 83dd62d03f deps update 2023-07-06 13:16:50 +01:00
Jedd Morgan 94cc0ac3f7 fix(instance): Fixed issues with hosted and nested instances 2023-07-06 13:14:20 +01:00
Jedd Morgan 36cb94d3d7 fix(converter): ToSpeckle instances 2023-07-03 16:59:53 +01:00
Jedd Morgan c60baf78c5 deps: updated to specklepy 2.15.0 2023-06-27 16:09:04 +01:00
Jedd Morgan d72cfd3522 feat(view): Added support for receiving/sending view objects 2023-05-31 01:12:17 +01:00
Jedd Morgan a26618a4f7 Merge pull request #164 from specklesystems/2.14/bug-fixes
2.14/bug fixes
2023-05-29 13:39:06 +01:00
Jedd Morgan eaf370407d Updated lock 2023-05-29 13:37:49 +01:00
Jedd Morgan a2b50fe5a1 Added support for sending diffuse BSDF shader materials 2023-05-29 13:33:53 +01:00
Jedd Morgan 7e62f76841 fix: fixed bug with imperial units scaling on send 2023-05-29 12:40:54 +01:00
Jedd Morgan fc804f16d3 Fixed bug with circular referenced custom props 2023-05-29 12:40:34 +01:00
Jedd Morgan 6c7da24595 Merge pull request #163 from specklesystems/collections
Collections
2023-05-27 13:44:19 +01:00
Jedd Morgan b284d39328 removed normal re-calculation 2023-05-27 13:43:42 +01:00
Jedd Morgan 907185c9bb Object naming tweaks 2023-05-26 18:58:42 +01:00
Jedd Morgan a189a2e1c0 Various cleanup and bug fixes 2023-05-26 18:18:10 +01:00
Jedd Morgan 1fad926275 Remove empty collections from send 2023-05-25 21:01:34 +01:00
Jedd Morgan 99c147fe2f Sending collections (all collections regardless of contents) 2023-05-25 17:45:31 +01:00
Jedd Morgan e2adf710b3 commit object builder 2023-05-25 00:22:09 +01:00
Jedd Morgan 9509344533 Added traversal refactor and support for receiving collections 2023-05-18 22:15:35 +01:00
Jedd Morgan 6fabc6cae6 feat(converter): implemented view to native 2023-05-10 17:31:18 +01:00
Jedd Morgan c39298687d Merge pull request #160 from specklesystems/jrm/ismultiplayer
Added `isMultiplayer` property
2023-04-13 14:26:29 +01:00
Jedd Morgan bcdddbf930 Added isMultiplayer property 2023-04-13 14:24:17 +01:00
Jedd Morgan b5684e34f6 Merge pull request #159 from specklesystems/jrm/curve-fix
Removed merge vertices by distance from clean mesh
2023-04-05 12:51:27 +01:00
Jedd Morgan 2203fe98f8 Removed merge vertices by distance from clean mesh 2023-04-05 12:47:03 +01:00
Jedd Morgan bbfdf2863b Merge pull request #158 from specklesystems/jrm/curve-fix
Using new installer.py
2023-04-04 20:22:48 +01:00
Jedd Morgan f25f6cb16c Fixed some misc issues 2023-04-04 20:21:20 +01:00
Jedd Morgan 9e4e533ba8 Using new installer.py 2023-03-28 17:06:40 +01:00
Jedd Morgan 8db12ca9b9 Merge pull request #157 from specklesystems/jrm/curve-fix
Mesh area calc + minor cleanup
2023-03-28 16:47:59 +01:00
Jedd Morgan 366c864247 Mesh area calc + minor cleanup 2023-03-28 16:47:07 +01:00
Jedd Morgan 52136d3ef6 Merge pull request #155 from specklesystems/jrm/instances
Implemented support for new instances
2023-03-21 22:01:46 +00:00
Jedd Morgan fe764d7f0c removed old comment 2023-03-21 21:58:07 +00:00
Jedd Morgan 669dd67521 Added option to receive instances as linked duplicates 2023-03-21 21:45:53 +00:00
Jedd Morgan f74b2c37f0 Implemented support for new instances 2023-03-19 03:08:57 +00:00
Jedd Morgan ebb4e32fff Merge pull request #151 from specklesystems/jrm/curve-fix
Fixes many bugs with curves
2023-03-09 14:52:28 +00:00
Jedd Morgan 25903baf83 Set default mesh smoothing to True 2023-02-21 19:19:31 +00:00
Jedd Morgan cb6d6d7ad8 feat(converter): Added support for Ellipse and Circles 2023-02-21 19:17:14 +00:00
Jedd Morgan fd2687aa3c All non-bezier nurbs curves now work perfectly rhino->blend->rhino 2023-02-20 22:26:11 +00:00
Jedd Morgan f5c65068de feat(converter): Better curve support 2023-02-18 02:23:41 +00:00
Jedd Morgan 235b49d8c6 Merge pull request #150 from specklesystems/hotfix-authtoken
fix(auth): hotfix for serializing authTokens in blender file
2023-02-15 18:00:53 +00:00
Jedd Morgan a1ec137c67 minor cleanup 2023-02-15 16:40:49 +00:00
Jedd Morgan b95f621272 Aligned nurbs knot multiplicities with rhino curves 2023-02-15 16:37:48 +00:00
Jedd Morgan a1fcdad0e3 fix(auth): hotfix for serializing authTokens in blender file 2023-02-15 15:41:36 +00:00
Gergő Jedlicska 584e543964 makes sure to insert a path string into the sys path not a path object (#149) 2023-02-08 12:45:29 +01:00
Jedd Morgan ef20c5240c Merge pull request #148 from specklesystems/jrm/converter/fix
Custom Properties overflow fix
2023-02-05 17:20:17 +00:00
Jedd Morgan 9fe12a018a fix(ToNative): fixed issue with attaching custom properties that exceed int32 max value 2023-02-05 15:30:55 +00:00
25 changed files with 3388 additions and 2132 deletions
+37 -14
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:
@@ -109,6 +129,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 +237,7 @@ workflows:
filters: *build_filters
- build-installer-win:
context: innosetup
context: digicert-keylocker
name: Windows Installer Build
requires:
- package-connector
@@ -304,4 +327,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
+7 -3
View File
@@ -1,7 +1,7 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies()
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
from specklepy.logging import metrics
@@ -37,8 +37,12 @@ loading a Blender file
@persistent
def load_handler(dummy):
pass
#bpy.ops.speckle.users_load() #this is an expensive operation, one that forces the user to wait every time blender loads. Until we can do this non-blocking, we will make the user hit the refresh button each time.
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
#bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
"""
Permanent handle on callbacks
@@ -0,0 +1,120 @@
from typing import Dict, Optional, Tuple, Union
import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
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(native_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(native_object).__name__}:{native_object.name_full}"
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:
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":
converted_collection["colorTag"] = col.color_tag
return converted_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
super().__init__()
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
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)
conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
# if parent_collection:
# self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col)
if id in self._collections:
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
for c in col.children:
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection
self._collections[id] = converted_collection
return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
objects_to_build.remove(root_commit_object)
self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection))
# Kill unused collections
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
elements = col.elements
if not elements: return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
continue
if should_remove_unuseful_collection(c):
# c is not useful, kill it
del elements[i]
else:
# col has a child (c) with objects
should_remove_this_col = False
i += 1
continue
return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object):
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
+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] = []
-29
View File
@@ -1,29 +0,0 @@
from typing import Union
from bpy_speckle.convert.to_native import convert_to_native
from specklepy.objects.base import Base
def get_speckle_subobjects(attr: Union[dict, Base], scale: float, name: str) -> list:
subobjects = []
keys = attr.keys() if isinstance(attr, dict) else attr.get_dynamic_member_names()
for key in keys:
if isinstance(attr[key], dict):
subtype = attr[key].get("type", None)
if subtype:
name = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
elif hasattr(attr[key], "type"):
subtype = attr[key].type
if subtype:
name = "{}.{}".format(name, key)
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
return subobjects
+22
View File
@@ -0,0 +1,22 @@
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
+579 -232
View File
@@ -1,14 +1,28 @@
import math
from typing import Iterable, Union, Collection
from bpy_speckle.convert.to_speckle import transform_to_speckle
from bpy_speckle.functions import get_scale_length, _report
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
from specklepy.objects.geometry import *
from bpy.types import Object
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 (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Collection as SCollection,
Instance,
Transform,
BlockDefinition,
)
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle, Plane
from bpy.types import Object, Collection as BCollection
from .util import (
add_to_hierarchy,
get_render_material,
get_vertex_color_material,
render_material_to_native,
add_custom_properties,
add_vertices,
@@ -17,171 +31,187 @@ from .util import (
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
*SUPPORTED_CURVES,
transform_to_speckle,
BlockDefinition,
BlockInstance,
Instance,
)
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:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if type(speckle_object) in CAN_CONVERT_TO_NATIVE:
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
if getattr(speckle_object, alias, None):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
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
#TODO: Check usages handle exceptions
def convert_to_native(speckle_object: Base) -> Object:
def convert_to_native(speckle_object: Base) -> list[Object]:
speckle_type = type(speckle_object)
speckle_name = generate_object_name(speckle_object)
try:
scale = get_scale_factor(speckle_object)
obj_data: Optional[Union[bpy.types.ID, bpy.types.Object, mathutils.Matrix]] = None
converted: list[Object] = []
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
# convert elements/breps
if speckle_type not in CAN_CONVERT_TO_NATIVE:
(obj_data, converted) = display_value_to_native(speckle_object, speckle_name, scale)
converted: Union[bpy.types.ID, bpy.types.Object, None] = None
children: list[Object] = []
# convert supported geometry
elif isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, speckle_name, scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, speckle_name, scale)
elif isinstance(speckle_object, Transform):
obj_data = transform_to_native(speckle_object, scale)
elif isinstance(speckle_object, BlockDefinition):
obj_data = block_def_to_native(speckle_object)
elif isinstance(speckle_object, BlockInstance):
obj_data = block_instance_to_native(speckle_object, scale)
# convert elements/breps
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}")
# convert supported geometry
elif isinstance(speckle_object, Mesh):
converted = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
converted = icurve_to_native(speckle_object, object_name, scale)
elif "View" in speckle_object.speckle_type:
return view_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
converted = instance_to_native_object(speckle_object, scale)
elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"Unsupported type {speckle_type}")
return []
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return []
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
blender_object = (
obj_data
if isinstance(obj_data, Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
raise Exception(f"Unsupported type {speckle_type}")
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
for child in converted:
child.parent = blender_object
if not isinstance(converted, Object):
converted = create_new_object(converted, object_name)
converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore
add_custom_properties(speckle_object, converted)
for c in children:
c.parent = converted
converted.append(blender_object)
return converted
def generate_object_name(speckle_object: Base) -> str:
prefix = (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or speckle_object.speckle_type.rsplit(':')[-1])
return f"{prefix} -- {speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue", "displayMesh", "@displayMesh", "elements", "@elements"]
def display_value_to_native(speckle_object: Base, name: str, scale: float) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
return _members_to_native(speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True)
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
return elements
def _members_to_native(speckle_object: Base, name: str, scale: float, members: Iterable[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts mesh displayValues as one mesh
Converts non-mesh displayValues as child Objects
Converts a given speckle_object by converting specified members
if combineMeshes == True
Converts mesh members as one mesh
Converts non-mesh members as child Objects
if combineMeshes == False
Converts all members as child objects (first item of the returned tuple will be None)
:returns: converted mesh, and any other converted child objects (may happen if members contained non-meshes)
"""
meshes: list[Mesh] = []
elements: list[Base] = []
others: list[Base] = []
#NOTE: raw Mesh elements will be treated like displayValues, which is not ideal, but no connector sends raw Mesh elements so its fine
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
for alias in members:
display = getattr(speckle_object, alias, None)
count = 0
max_depth = 255
def seperate(value: Any) -> None:
nonlocal meshes, elements, count, max_depth
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if isinstance(value, Mesh):
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
elements.append(value)
others.append(value)
elif isinstance(value, list):
count += 1
if(count > max_depth):
return
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
separate(x)
seperate(display)
return False
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?")
converted: list[Object] = []
children: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
for item in elements:
item.parent_speckle_type = speckle_object.speckle_type
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
for item in others:
try:
blender_object = convert_to_native(item)
children.append(blender_object)
except Exception as ex:
_report(f"Failed to convert display value {item}: {ex}")
if not elements and not meshes:
_report(f"Unsupported type {speckle_object.speckle_type}")
return (mesh, children)
return (mesh, converted)
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam: bpy.types.Camera
if name in bpy.data.cameras.keys():
native_cam = bpy.data.cameras[name]
else:
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)
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))
right = forward.cross(up).normalized()
cam_obj.matrix_world = MMatrix((
(right.x, up.x, -forward.x, tx),
(right.y, up.y, -forward.y, ty),
(right.z, up.z, -forward.z, tz),
(0, 0, 0, 1 )
))
return cam_obj
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
@@ -197,12 +227,20 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
if not mesh.vertices: continue
add_faces(mesh, bm, offset, i)
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
try:
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
elif mesh.colors:
native_material = get_vertex_color_material()
blender_mesh.materials.append(native_material)
except Exception as ex:
_report(f"Failed converting render material for {name}: {ex}")
offset += len(mesh.vertices) // 3
@@ -211,10 +249,15 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
# Third pass, add vertex instance data
for mesh in meshes:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
try:
add_colors(mesh, bm)
except Exception as ex:
_report(f"Skipping converting vertex colors for {name}: {ex}")
try:
add_uv_coords(mesh, bm)
except Exception as ex:
_report(f"Skipping converting uv coordinates for {name}: {ex}")
bm.to_mesh(blender_mesh)
bm.free()
@@ -222,7 +265,13 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
return blender_mesh
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
"""
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not speckle_curve.end: return []
line = blender_curve.splines.new("POLY")
line.points.add(1)
@@ -233,97 +282,95 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
1,
)
if speckle_curve.end:
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (value := scurve.value): return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed or False
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return [line]
return []
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if value := scurve.value:
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return [polyline]
return []
return [polyline]
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if points := scurve.points:
N = len(points) // 3
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (points := scurve.points): return []
if not scurve.degree: raise Exception("curve is missing degree")
if not scurve.weights: raise Exception("curve is missing weights")
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed or False
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
nurbs = bcurve.splines.new("NURBS")
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.order_u = scurve.degree + 1
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return [nurbs]
return []
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc
# TODO: improve Blender representation of arc - check autocad test stream
if not rcurve.radius: raise Exception("curve is missing radius")
if not rcurve.startAngle: raise Exception("curve is missing startAngle")
if not rcurve.endAngle: raise Exception("curve is missing endAngle")
plane = rcurve.plane
if not plane:
return None
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore
# Get start and end vectors, centre point, angles, etc.
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
@@ -339,7 +386,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = mathutils.Quaternion(normal, step)
stepQuat = MQuaternion(normal, step) # type: ignore
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -366,11 +413,11 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
"""
Convert Polycurve object
"""
segments = scurve.segments
if not scurve.segments: raise Exception("curve is missing segments")
curves = []
for seg in segments:
for seg in scurve.segments:
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
@@ -379,48 +426,111 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
_report(f"Unsupported curve type: {speckle_type}")
return curves
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> List[bpy.types.Spline]:
if not ellipse.plane: raise Exception("curve is missing plane")
radX: float
radY: float
if isinstance(ellipse, Ellipse):
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius")
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
else:
if not ellipse.radius: raise Exception("curve is missing radius")
radX = ellipse.radius * units_scale
radY = ellipse.radius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(ellipse.plane, units_scale)
spline = bcurve.splines.new("BEZIER")
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
splines: List[bpy.types.Spline]
# single curves
if isinstance(speckle_curve, Line):
spline = line_to_native(speckle_curve, blender_curve, scale)
splines = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve,Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
splines = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
splines = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
spline = arc_to_native(speckle_curve, blender_curve, scale)
splines = [spline] if spline else []
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
splines = ellipse_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return [spline] if spline is not None else []
return splines
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> Optional[bpy.types.Curve]:
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
raise Exception(f"Unsupported curve type: {curve_type}")
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12
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)
return blender_curve
def transform_to_native(transform: Transform, scale: float) -> mathutils.Matrix:
mat = mathutils.Matrix(
"""
Transforms and Instances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
transform.value[:4],
transform.value[4:8],
@@ -433,38 +543,275 @@ def transform_to_native(transform: Transform, scale: float) -> mathutils.Matrix:
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
def block_def_to_native(definition: BlockDefinition) -> bpy.types.Collection:
native_def = bpy.data.collections.get(definition.name)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
if not instance.definition: raise Exception("Instance is missing a definition")
name_prefix = (
_get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Object:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
definition = instance.definition
if not definition.id: raise Exception("Instance is missing a valid definition")
name = _get_instance_name(instance)
native_instance: Optional[Object] = None
converted_objects: Dict[str, Union[Object, BCollection]] = {}
traversal_root: Base = definition
if not can_convert_to_native(definition):
# 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 convertible
traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition
_deep_conversion(traversal_root, converted_objects, False)
if not native_instance:
assert(can_convert_to_native(definition))
if not definition.id in converted_objects:
raise Exception("Definition was not converted")
converted = converted_objects[definition.id]
if not isinstance(converted, Object):
raise Exception("Definition was not converted to an Object")
native_instance = converted
instance_transform = transform_to_native(instance.transform, scale)
native_instance.matrix_world = instance_transform
return native_instance
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
instance_transform = transform_to_native(instance.transform, scale)
native_instance = create_new_object(None, name)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = instance_transform
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
if native_def:
return native_def
native_def = bpy.data.collections.new(definition.name)
native_def = create_new_collection(name)
native_def["applicationId"] = definition.applicationId
for geo in definition.geometry:
if b_obj := convert_to_native(geo):
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
converted_objects = {}
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)
return native_def
def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool):
traversal_func = get_default_traversal_func(can_convert_to_native)
def block_instance_to_native(instance: BlockInstance, scale: float) -> bpy.types.Object:
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition)
for item in traversal_func.traverse(root):
current: Base = item.current
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")
native_instance = bpy.data.objects.new(name, None)
add_custom_properties(instance, native_instance)
native_instance["name"] = getattr(instance, 'name', None) or instance.blockDefinition.name
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
#Convert the object!
converted_data_type: str
converted: Union[Object, BCollection, None]
if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
converted = collection_to_native(current)
converted_data_type = "COLLECTION"
else:
converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
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:
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
except Exception as ex:
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
def collection_to_native(collection: SCollection) -> BCollection:
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
ret = get_or_create_collection(name)
color = getattr(collection, "colorTag", None)
if color:
ret.color_tag = color
return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
#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
return new_collection
"""
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 _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_SPECKLE_SEPARATOR)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
name = _get_friendly_object_name(speckle_object)
if name:
prefix = _truncate_object_name(name)
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
+321 -140
View File
@@ -1,62 +1,96 @@
from typing import Dict, Iterable, Optional, Tuple
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy
from bpy.types import Depsgraph, Material, MeshPolygon, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy.types import (
Depsgraph,
MeshPolygon,
Object,
Curve as NCurve,
Mesh as NMesh,
Camera as NCamera,
)
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
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_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
from bpy_speckle.functions import _report
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
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", "FONT", "SURFACE", "META")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
"""
Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
:param units: The desired final units to send
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
:return: The Converted blender object
"""
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
speckle_objects = []
# speckle_material = material_to_speckle_old(blender_object) #TODO: What about curves with materials...
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
blender_object = cast(Object, (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
))
converted: Optional[Base] = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
elif blender_type == "CURVE":
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object, scale)
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:
return None
raise Exception("Conversion returned None")
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return converted
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float = 1.0) -> List[Mesh]:
#if data.loop_triangles is None or len(data.loop_triangles) < 1:
# data.calc_loop_triangles()
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
@@ -66,8 +100,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = blender_object.matrix_world
scaled_vertices = [tuple(transform @ x.co * scale) for x in data.vertices]
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
@@ -75,14 +109,17 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
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] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
@@ -94,7 +131,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
m_texcoords.extend([vt.uv.x, vt.uv.y])
uv = cast(MVector, vt.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
@@ -104,7 +142,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=UNITS,
units=Units,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
@@ -117,31 +156,30 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
return submeshes
def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points = []
points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * scale))
points.append(tuple(matrix @ bp.co * scale))
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * scale))
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * scale),
tuple(matrix @ spline.bezier_points[0].handle_left * scale),
tuple(matrix @ spline.bezier_points[0].co * scale),
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
)
)
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
@@ -155,99 +193,193 @@ def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: floa
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
points=flattend_points,
periodic= not spline.use_endpoint_u,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
rational=False,
rational=True,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
flattend_points = []
for row in points: flattend_points.extend(row)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
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
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])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=[pt.weight for pt in spline.points],
periodic= not spline.use_endpoint_u,
points=flattened_points,
weights=weights,
knots=knots,
rational=False,
rational=is_rational,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = cast(Vector, matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * UnitsScale)
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
flattend_points = []
for row in points: flattend_points.extend(row)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * UnitsScale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_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_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_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
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
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),
area=0,
units=UNITS,
units=Units,
)
def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0) -> Optional[List[Base]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b = Base()
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
if meshes:
b["@displayValue"] = meshes
if blender_object.type != "CURVE":
return None
b["name"] = to_speckle_name(blender_object)
b["@elements"] = curves
return b
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
assert(blender_object.type == "CURVE")
mat = blender_object.matrix_world
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
curves = []
matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = []
curves: List[Base] = []
#TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh(), scale)
curves.extend(mesh)
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "POLY":
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
return curves
return (meshes, curves)
def anything_to_speckle_mesh(blender_object: Object) -> Base:
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
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"
if blender_object.type != "MESH":
@@ -260,13 +392,13 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * scale)
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value, # magic (flatten list of tuples)
value=value,
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -283,78 +415,127 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes is True and blender_mat.node_tree.nodes.get(
"Principled BSDF"
):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
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_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
return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat
#TODO: Support more shaders
else:
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
if data.type != 'PERSP':
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = cast(MVector, matrix.col[1].xyz)
forwards = cast(MVector, -matrix.col[2].xyz)
translation = matrix.translation
def material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
view.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards)
view.target = vector_to_speckle_point(forwards) #TODO: do these need to be scaled?
view.units = Units
view.isOrthogonal = False
return view
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
def vector_to_speckle_point(xyz: MVector) -> Point:
return Point(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
return material_to_speckle(blender_mat)
def vector_to_speckle(xyz: MVector) -> Vector:
return Vector(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def transform_to_speckle(blender_transform: Iterable[Iterable[float]], scale=1.0) -> Transform:
value = [y for x in blender_transform for y in x]
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
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):
value[i] *= scale
value[i] *= UnitsScale
return Transform(value=value, units=UNITS)
return Transform(value=value, units=Units)
def block_def_to_speckle(blender_definition: bpy.types.Collection, scale=1.0) -> BlockDefinition:
geometry = []
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometryBuilder = BlenderCommitObjectBuilder()
for geo in blender_definition.objects:
geometry.extend(convert_to_speckle(geo, scale, UNITS, None))
try:
c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex:
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
except Exception as ex:
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
block_def = BlockDefinition(
units=UNITS,
name=blender_definition.name,
geometry=geometry,
basePoint=Point(units=UNITS),
units=Units,
name=to_speckle_name(blender_definition),
geometry=dummyRoot["@elements"],
basePoint=Point(units=Units),
)
blender_props = get_blender_custom_properties(blender_definition)
block_def.applicationId = blender_props.pop("applicationId", None)
# blender_props = get_blender_custom_properties(blender_definition)
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
return block_def
def block_instance_to_speckle(blender_instance: Object, scale=1.0) -> BlockInstance:
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection, scale
blender_instance.instance_collection
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=blender_instance.name,
units=UNITS,
name=to_speckle_name(blender_instance),
units=Units,
)
def empty_to_speckle(blender_object: Object, scale=1.0) -> Optional[BlockInstance]:
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it
try:
geo = blender_object.instance_collection.objects.items()
return block_instance_to_speckle(blender_object, scale)
except AttributeError as err:
_report(
f"No instance collection found in empty. Skipping object {blender_object.name}"
)
return None
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
# Empty -> Block
return block_instance_to_speckle(blender_object)
else:
# Empty -> Point
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
return wrapper
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
return Point(x = transformed_pos.x,
y = transformed_pos.y,
z = transformed_pos.z)
+241 -60
View File
@@ -1,29 +1,19 @@
import math
from typing import Optional, Tuple
from typing import Any, Dict, Optional, Tuple, Union, cast
from bmesh.types import BMesh
import bpy, struct, idprop
import bpy, idprop
from specklepy.objects.base import Base
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
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
from specklepy.objects.graph_traversal.traversal import TraversalContext
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
@@ -42,6 +32,16 @@ def to_argb_int(rgba_color: list[float]) -> int:
return int.from_bytes(int_color, byteorder="big", signed=True)
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
try:
#Expected c types: float, int, string, float[], int[]
blender_object[key] = value
except (OverflowError, TypeError) as ex:
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
except Exception as ex:
#TODO: Log this as it's unexpected!!!
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
@@ -58,18 +58,18 @@ def add_custom_properties(speckle_object: Base, blender_object: Object):
continue
if isinstance(val, (int, str, float)):
blender_object[key] = val
set_custom_property(key, val, blender_object)
elif key == "properties" and isinstance(val, Base):
val["applicationId"] = None
add_custom_properties(val, blender_object)
elif isinstance(val, list):
items = [item for item in val if not isinstance(item, Base)]
if items:
blender_object[key] = items
set_custom_property(key, items, blender_object)
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
blender_object[k] = v
set_custom_property(k, v, blender_object)
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
@@ -87,29 +87,56 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # 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"
return blender_mat
_vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material:
global _vertex_color_material
#see https://stackoverflow.com/a/69807985
if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if not "VERTEX_COLOR" in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object and convert it to a blender material"""
"""Trys to get a RenderMaterial on given speckle_object"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not isinstance(speckle_mat, RenderMaterial):
return None
return speckle_mat
if isinstance(speckle_mat, RenderMaterial):
return speckle_mat
return None
@@ -128,7 +155,7 @@ def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = False):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
@@ -159,10 +186,8 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
if len(scolors) > 0:
for i in range(len(scolors)):
col = int(scolors[i])
(a, r, g, b) = [
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
]
argb = int(scolors[i])
(a, r, g, b) = argb_split(argb)
colors.append(
(
float(r) / 255.0,
@@ -180,6 +205,13 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF
green = (argb >> 8) & 0xFF
blue = argb & 0xFF
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
@@ -225,8 +257,9 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth=1000):
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"):
@@ -238,31 +271,27 @@ def get_blender_custom_properties(obj, max_depth=1000):
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * (4 + macro_knotsu(nu))
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
@@ -270,13 +299,13 @@ def make_knots(nu: bpy.types.Spline) -> list[float]:
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1:
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2:
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -289,24 +318,176 @@ def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> N
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(pts_order):
knots[a] = a
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def makecyclicknots(knots: list[float], point_count: int, order: int) -> None:
order2 = order - 1
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 #type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
knots[nu.point_count_u]
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
#/* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv #type: ignore
sum_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
sum_index += 1
coord_index += stride
u += ustep
return coord_array
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:
link_object_to_collection_nested(child, col)
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
parent_collection: Optional[BCollection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, BCollection):
parent_collection = c
break
else: #isinstance(c, Object):
parent_object = parent_object or c
if a == order2:
knots[point_count + order - 2] += 1.0
nextParent = nextParent.parent
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
set_parent(converted, parent_object, preserve_transform)
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform :
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
child.parent = parent
+26 -53
View File
@@ -1,32 +1,12 @@
from bpy_speckle.clients import speckle_clients
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
"""
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
"""
@@ -34,39 +14,32 @@ 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
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
"""
Client, user, and stream functions
"""
def _check_speckle_client_user_stream(scene):
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
"""
Verify that there is a valid user and stream
Traversal func for traversing a speckle commit object
"""
speckle = scene.speckle
user = (
speckle.users[int(speckle.active_user)]
if len(speckle.users) > int(speckle.active_user)
else None
ignore_rule = TraversalRule(
[
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 _: [],
)
if user is None:
print("No users loaded.")
stream = (
user.streams[user.active_stream]
if len(user.streams) > user.active_stream
else None
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
if stream is None:
print("Account contains no streams.")
return (user, stream)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
+118 -34
View File
@@ -1,29 +1,118 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
import bpy
import sys
_user_data_env_var = "SPECKLE_USERDATA_PATH"
print("Starting Speckle Blender installation")
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def modules_path() -> Path:
modules_path = Path(bpy.utils.script_path_user(), "addons", "modules")
modules_path.mkdir(exist_ok=True, parents=True)
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[1] != modules_path:
sys.path.insert(1, modules_path)
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
return modules_path
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
print(f"Found blender modules path {modules_path()}")
def is_pip_available() -> bool:
try:
@@ -34,7 +123,7 @@ def is_pip_available() -> bool:
def ensure_pip() -> None:
print("Installing pip... "),
print("Installing pip... ")
from subprocess import run
@@ -43,7 +132,7 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception("Failed to install pip.")
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
@@ -53,11 +142,11 @@ def get_requirements_path() -> Path:
return path
def install_requirements() -> None:
def install_requirements(host_application: str) -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = modules_path()
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
@@ -67,7 +156,12 @@ def install_requirements() -> None:
PYTHON_PATH,
"-m",
"pip",
"-q",
"--disable-pip-version-check",
"install",
"--prefer-binary",
"--ignore-installed",
"--no-compile",
"-t",
str(path),
"-r",
@@ -78,20 +172,16 @@ def install_requirements() -> None:
)
if completed_process.returncode != 0:
print("Please try manually installing speckle-blender")
raise Exception(
"""
Failed to install speckle-blender.
See console for manual install instruction.
"""
)
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
def install_dependencies() -> None:
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
install_requirements()
install_requirements(host_application)
def _import_dependencies() -> None:
@@ -110,19 +200,13 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies() -> None:
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies()
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Found all dependencies, proceed with loading")
print("Successfully found dependencies")
except ImportError:
raise Exception(
"Cannot automatically ensure Speckle dependencies. Please restart Blender!"
)
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
if __name__ == "__main__":
ensure_dependencies()
+4 -3
View File
@@ -1,4 +1,4 @@
from .users import LoadUsers, LoadUserStreams
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
@@ -15,24 +15,26 @@ from .streams import (
SelectOrphanObjects,
)
from .streams import (
UpdateGlobal,
AddStreamFromURL,
CreateStream,
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
]
operator_classes.extend([DeleteCommit])
@@ -53,7 +55,6 @@ operator_classes.extend(
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
UpdateGlobal,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
+31 -22
View File
@@ -3,24 +3,27 @@ Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream
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):
"""
Delete 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
@@ -28,36 +31,42 @@ class DeleteCommit(bpy.types.Operator):
col.prop(self, "are_you_sure")
def invoke(self, context, event):
speckle = get_speckle(context)
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
if not self.are_you_sure:
_report("Cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
speckle = context.scene.speckle
self.delete_commit(context)
return {"FINISHED"}
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
(_, stream, branch, commit) = speckle.validate_commit_selection()
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
return {"FINISHED"}
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_commit"
},
)
if not deleted:
raise Exception("Delete operation failed")
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"}
+61 -13
View File
@@ -4,14 +4,16 @@ Object operators
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
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
@@ -20,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
@@ -28,7 +30,6 @@ class UpdateObject(bpy.types.Operator):
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
active = context.active_object
_report(active)
@@ -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,9 +141,17 @@ 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
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
@@ -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"}
File diff suppressed because it is too large Load Diff
+147 -67
View File
@@ -1,138 +1,218 @@
"""
User account operators
"""
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 SpeckleSceneSettings
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream, User
from specklepy.api.credentials import get_local_accounts
from datetime import datetime
from bpy_speckle.properties.scene import SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
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):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
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()
return {"FINISHED"}
@staticmethod
def reset_ui(context: Context):
speckle = get_speckle(context)
speckle.users.clear()
speckle_clients.clear()
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 : SpeckleSceneSettings = context.scene.speckle
users = speckle.users
speckle = get_speckle(context)
users_list = speckle.users
speckle.users.clear()
speckle_clients.clear()
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.name = profile.userInfo.name
user.email = profile.userInfo.email
user.company = profile.userInfo.company or ""
user.authToken = profile.token
try:
client = SpeckleClient(
host=profile.serverInfo.url,
use_ssl="https" in profile.serverInfo.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_index = int(speckle.active_user)
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
def add_user_stream(user: User, stream: Stream):
s = user.streams.add()
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):
"""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
_report(f"Adding stream {s.id} - {s.name}")
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 = cast(SpeckleBranchObject, s.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 = branch.commits.add()
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 = datetime.strftime(c.createdAt, "%Y-%m-%d %H:%M:%S.%f%Z")
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units
else:
s.units = "Meters"
commit.referenced_object = c.referencedObject
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):
speckle = context.scene.speckle
self.load_user_stream(context)
return {"FINISHED"}
if len(speckle.users) > 0:
user = speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
if not streams:
_report("Failed to retrieve streams.")
return
user = speckle.validate_user_selection()
user.streams.clear()
client = speckle_clients[int(speckle.active_user)]
try:
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("Zero projects found")
return
default_units = "Meters"
user.streams.clear()
for s in streams:
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
for s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit, commit_limit=10)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
return {"FINISHED"}
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"CANCELLED"}
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
+118 -52
View File
@@ -1,82 +1,100 @@
"""
Scene properties
"""
from typing import Iterable, Optional, Tuple, cast
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
PointerProperty,
)
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="abc")
message: StringProperty(default="A simple commit")
author_name: StringProperty(default="Author name")
author_id: StringProperty(default="Author ID")
created_at: StringProperty(default="Today")
source_application: StringProperty(default="Unknown")
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)]
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,
)
) # 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 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)]
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,
)
) # type: ignore
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
authToken: StringProperty(default="", subtype='PASSWORD')
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
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) # 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):
@@ -89,46 +107,94 @@ 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()
bpy.ops.speckle.load_user_streams() # type: ignore
active_user: EnumProperty(
items=get_users,
name="User",
description="Select user",
name="Account",
description="Select account",
update=set_user,
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]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user account selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No project selected/found")
return (user, stream)
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
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 model version selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
"""
Gets the speckle scene object
"""
return context.scene.speckle #type: ignore
+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
+920 -723
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -7,17 +7,17 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.12.0"
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 = "^20221006"
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"]